Compare commits

...
103 Commits
Author SHA1 Message Date
takatostandGitHub 17f22347ae bump to 0.6.13 (#6078) 2024-07-08 23:23:07 +08:00
AIxGEEKandGitHub 22aaf8960b fix: Inconsistency Between Actual and Debug Input Variables (#6055) 2024-07-08 22:27:55 +08:00
0046ef7707 refactor: revamp picker block (#4227)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2024-07-08 21:56:09 +08:00
takatostandGitHub 68b1d063f7 chore: remove tsne unused code (#6077) 2024-07-08 21:25:19 +08:00
AIxGEEKandGitHub 5e6c3001bd fix: relative in overflow div (#5998) 2024-07-08 18:35:12 +08:00
takatostandGitHub 7ed4e963aa chore(action): move docker login above Set up QEMU in build-push action workflow (#6073) 2024-07-08 18:32:29 +08:00
Chenhe GuandGitHub 001d868cbd remove clunky welcome message (#6069) 2024-07-08 18:17:41 +08:00
Xiao LeyandGitHub 6610b4cee5 feat: add request_params field to jina_reader tool (#5610) 2024-07-08 18:11:50 +08:00
JyongandGitHub cbbe28f40d fix azure stream download (#6063) 2024-07-08 17:13:16 +08:00
JoelandGitHub 603187393a chore: hide tracing introduce detail (#6049) 2024-07-08 10:50:18 +08:00
WaffleGitHubox01024@163.com <Waffle>
411e938e3b Address the issue of the absence of poetry in the development container. (#6036)
Co-authored-by: ox01024@163.com <Waffle>
2024-07-08 09:44:07 +08:00
75pyandGitHub 610da4f662 Fix authorization header validation to handle bearer types correctly - "authorization config header is required" error (#6040) 2024-07-08 00:09:59 +08:00
crazywoolaandGitHub 3ec80f9dda Fix/6034 get random order of categories in explore and workflow is missing in zh hant (#6043) 2024-07-07 17:06:47 +08:00
91c5818236 Modify slack webhook url validation to allow workflow (#6041) (#6042)
Co-authored-by: Shunsuke Mabuchi <mabuchs@amazon.co.jp>
2024-07-07 14:09:20 +08:00
-LAN-andGitHub c436454cd4 fix(configs): Update pydantic settings in config files (#6023) 2024-07-07 12:18:15 +08:00
YeuolyandGitHub a877d4831d Fix/incorrect parameter extractor memory (#6038) 2024-07-07 12:17:34 +08:00
takatostandGitHub d522308a29 chore: optimize memory fetch performance (#6039) 2024-07-07 08:54:24 +08:00
sinoandGitHub 85744b72e5 feat: support moonshot and glm base models for volcengine provider (#6029) 2024-07-07 01:17:33 +08:00
Cherilyn BurenandGitHub f0b7051e1a Optimize db config (#6011) 2024-07-07 01:06:51 +08:00
Masashi TomookaandGitHub 3b23d6764f fix: token count includes base64 string of input images (#5868) 2024-07-06 16:53:32 +08:00
Bowen LiangandGitHub 9b7c74a5d9 chore: skip pip upgrade preparation in api dockerfile (#5999) 2024-07-06 14:17:34 +08:00
-LAN-andGitHub 4d105d7bd7 feat(*): Swtich to dify_config. (#6025) 2024-07-06 12:05:13 +08:00
非法操作andGitHub eee779a923 fix: the input field of tool panel not worked as expected (#6003) 2024-07-06 09:54:30 +08:00
ab847c81fa Add 2 firecrawl tools : Scrape and Search (#6016)
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-07-06 09:45:39 +08:00
-LAN-andGitHub b217ee414f test(test_rerank): Remove duplicate test cases. (#6024) 2024-07-06 09:44:50 +08:00
takatostandGitHub 23dc6edb99 chore: optimize memory messages fetch count limit (#6021) 2024-07-06 03:25:38 +08:00
takatostandGitHub 79df8825c8 Revert "feat: knowledge admin role" (#6018) 2024-07-05 21:31:34 +08:00
K8sCatandGitHub 71c50b7e20 feat: add Llama 3 and Mixtral model options to ddgo_ai.yaml (#5979)
Signed-off-by: K8sCat <k8scat@gmail.com>
2024-07-05 21:11:15 +08:00
af98fd29bf fix: add status_code 304 (#6000)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: crazywoola <427733928@qq.com>
2024-07-05 21:10:33 +08:00
crazywoolaandGitHub cddea83e65 6014 i18n add support for spanish (#6017) 2024-07-05 21:05:33 +08:00
9f16739518 [Feature] Support loading for mermaid. (#6004)
Co-authored-by: 靖谦 <jingqian@kaiwu.cloud>
2024-07-05 21:01:50 +08:00
JoeandGitHub 3f0da88ff7 fix: update workflow trace query (#6010) 2024-07-05 18:37:26 +08:00
cc63af8e72 Removed firecrawl-py, fixed and improved firecrawl tool (#5896)
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-07-05 18:04:51 +08:00
非法操作andGitHub bf2268b0af fix API tool's schema not support array (#6006) 2024-07-05 17:11:59 +08:00
xielongandGitHub 00b4cc3cd4 feat: implement forgot password feature (#5534) 2024-07-05 13:38:51 +08:00
f546db5437 fix: document truncation and loss in notion document sync (#5631)
Co-authored-by: Aurelius Huang <cm.huang@aftership.com>
2024-07-05 11:48:17 +08:00
orangeclkandGitHub f8aaa57f31 feat: add retry mechanism for zhipuai (#5926) 2024-07-05 10:49:18 +08:00
cabcf94be3 fix: TENCENT_VECTOR_DB_REPLICAS can be set to 0 (#5968)
Co-authored-by: jianglin <jianglin@wangxiaobao.com>
2024-07-05 08:32:28 +08:00
legaoandGitHub 2d6624cf9e typo: Update README.md (#5987) 2024-07-04 22:50:27 +08:00
-LAN-andGitHub 02982df0d4 fix: Fix some type error in http executor. (#5915) 2024-07-04 19:34:37 +08:00
-LAN-andGitHub 421a24c38d refactor(api/app.py): Simplify the retrieval of debug settings. (#5916) 2024-07-04 18:19:06 +08:00
-LAN-andGitHub d7f75d17cc Chore/remove-unused-code (#5917) 2024-07-04 18:18:26 +08:00
5d9ad430af feat: knowledge admin role (#5965)
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: jyong <718720800@qq.com>
2024-07-04 16:21:40 +08:00
非法操作andGitHub 46eca01fa3 fix: no json output vars in front-page tool (#5943) 2024-07-04 16:15:56 +08:00
AIxGEEKandGitHub 4d9c22bfc6 refactor: optimize-the-performance-of-var-reference-picker (#5918) 2024-07-04 15:33:36 +08:00
JoelandGitHub 52e59cf4df chore: enchance firecrawl user experience (#5958) 2024-07-04 15:26:38 +08:00
JoeandGitHub 688b8fe114 fix: langfuse logical operator error (#5948) 2024-07-04 13:47:15 +08:00
longzhihunandGitHub aecdfa2d5c feat: add claude3 function calling (#5889) 2024-07-03 22:21:02 +08:00
-LAN-andGitHub cb8feb732f refactor: Create a dify_config with Pydantic. (#5938) 2024-07-03 21:09:23 +08:00
orangeclkandGitHub c490bdfbf9 fix: zhipuai pytest correction (#5934) 2024-07-03 19:19:33 +08:00
-LAN-andGitHub e7494d632c docs(api/core/tools/docs/en_US/tool_scale_out.md): Format by markdownlint. (#5903) 2024-07-03 13:13:38 +08:00
takatostandGitHub e3006f98c9 chore: remove dify SaaS URL in default configs (#5888) 2024-07-02 22:49:18 +08:00
crazywoolaandGitHub b34baf1e3a feat: pr template (#5886) 2024-07-02 22:38:17 +08:00
quicksandandGitHub 372dc7ac1a fix bug : TencentVectorDBConfig Add TENCENT_VECTOR_DB_DATABASE (#5879) 2024-07-02 21:31:14 +08:00
-LAN-andGitHub 66a62e6c13 refactor(api/core/app/apps/base_app_generator.py): improve input validation and sanitization in BaseAppGenerator (#5866) 2024-07-02 18:58:07 +08:00
JoelandGitHub 04c0a9ad45 chore: click area that trigger showing tracing config is too large (#5878) 2024-07-02 18:28:41 +08:00
0944ca9d91 Fix/remove tsne position test (#5858)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
2024-07-02 17:57:42 +08:00
d468f8b75c Fix/docker env namings (#5857)
Co-authored-by: dahuahua <38651850@qq.com>
2024-07-02 16:14:34 +08:00
Masashi TomookaandGitHub 6e256507d3 doc: docker-compose won't start due to wrong README (#5859) 2024-07-02 16:10:22 +08:00
JoelandGitHub a33ce09e6e fix:retieval setting document link 404 (#5861) 2024-07-02 16:02:17 +08:00
quicksandandGitHub 9f973bb703 chore:remove .env.example duplicate key (#5853) 2024-07-02 15:10:18 +08:00
JoelandGitHub 6d0a605c5f fix: not show opening question if the opening message is empty (#5856) 2024-07-02 15:04:02 +08:00
a948bf6ee8 fix: react.js error 185 maximum update depth exceeded in streaming responses during conversation (#5849)
Co-authored-by: 林星宇 <xy@udxx.cn>
2024-07-02 14:26:56 +08:00
Jimm ChenandGitHub b8999e367a Ensure *.sh are LF-style, so that they can be used directly by Docker for Windows (#5793) 2024-07-02 13:38:18 +08:00
JoeandGitHub 59ad091e69 feat: add export permission (#5841) 2024-07-02 13:37:37 +08:00
JoeandGitHub 598e030a7e feat: update LangfuseConfig host config (#5846) 2024-07-02 13:14:07 +08:00
774a17cedf fix:unable to select workplace at the bottom (#5785)
Co-authored-by: wxfanghongtai <wxfanghongtai@gf.com.cn>
2024-07-02 13:10:50 +08:00
JoelandGitHub d889e1b233 fix: output variable name may be duplicate (#5845) 2024-07-02 13:02:59 +08:00
-LAN-andGitHub 32d85fb896 chore: Update some type hints in config. (#5833) 2024-07-02 08:50:02 +08:00
JyongandGitHub af308b99a3 sync delete app table record when delete app (#5819) 2024-07-02 08:48:29 +08:00
Nam VuandGitHub 49d9c60a53 chore: update i18n for #5811 (#5838) 2024-07-02 08:47:53 +08:00
ed83df972f Chore/remove extra docker middleware variables (#5836)
Co-authored-by: dahuahua <38651850@qq.com>
2024-07-01 23:34:00 +08:00
3124728e03 Fix/docker nginx https config (#5832)
Co-authored-by: dahuahua <38651850@qq.com>
2024-07-01 23:15:26 +08:00
掌柜的andGitHub af469ea5bd add provision scripts repo link for azure to readme (#5820) 2024-07-01 20:44:47 +08:00
quicksandandGitHub 2a27568537 Enhance: tools wecom bot support markdown message (#5791) 2024-07-01 18:19:47 +08:00
hymvpandGitHub 1d3e96ffa6 add support oracle oci object storage (#5616) 2024-07-01 17:21:44 +08:00
JoeandGitHub b7b1396c51 fix: ops trace slow db (#5812) 2024-07-01 17:09:53 +08:00
JyongandGitHub 71bcf75d9a Feat/add delete knowledge confirm (#5810) 2024-07-01 17:06:51 +08:00
Charles ZhouandGitHub 850c2273ee feat: Nominatim OpenStreetMap search tool (#5789) 2024-07-01 16:34:32 +08:00
JoelandGitHub 78d41a27cc feat: knowledge used by app can still be removed (#5811) 2024-07-01 16:14:49 +08:00
takatostandGitHub 0f8625cac2 fix: ssrf proxy and nginx entrypoint command in docker-compose files (#5803) 2024-07-01 14:48:27 +08:00
Charles ZhouandGitHub cb09dbef66 feat: correctly delete applications using Celery workers (#5787) 2024-07-01 14:21:17 +08:00
zxhlyhandGitHub 5692f9b33b fix: signin url (#5800) 2024-07-01 14:13:32 +08:00
fdfbbde10d [seanguo] modify bedrock Claude3 invoke method to converse API (#5768)
Co-authored-by: Chenhe Gu <guchenhe@gmail.com>
2024-07-01 04:36:13 +08:00
Chenhe GuandGitHub a27462d58b Chore/improve docker compose (#5784) 2024-07-01 01:11:33 +08:00
takatostandGitHub 91da622df5 chore: merge CODE_EXECUTION_API_KEY into SANDBOX_API_KEY in the docker-compose.yaml (#5779) 2024-06-30 21:39:48 +08:00
takatostandGitHub 373b5047fd chore: fulfill default value in docker compose yaml (#5778) 2024-06-30 21:17:53 +08:00
takatostandGitHub 36610b6acf fix: can’t change exec permissions after mounting docker-entrypoint.sh for nginx and ssrf-proxy services causing startup failures (#5776) 2024-06-30 20:18:53 +08:00
eab0ac3a13 chore: remove port expose in docker compose (#5754)
Co-authored-by: Chenhe Gu <guchenhe@gmail.com>
2024-06-30 10:31:31 +08:00
JoeandGitHub f637ae4794 fix: langsmith message_trace end_user_data session_id error (#5759) 2024-06-30 01:12:16 +08:00
JoeandGitHub ffb07eb24b fix: workflow trace none type error (#5758) 2024-06-29 23:32:52 +08:00
YeuolyandGitHub f101fcd0e7 fix: missing process data in parameter extractor (#5755) 2024-06-29 23:29:43 +08:00
Chenhe GuandGitHub fc0f75d13b Docs/add docker dotenv notes (#5750) 2024-06-29 22:09:59 +08:00
takatostandGitHub 1e045a0187 fix: slow sql of ops tracing (#5749) 2024-06-29 20:28:30 +08:00
takatostandGitHub cdf64d4ee2 Update docker-compose.yaml (#5745) 2024-06-29 18:35:32 +08:00
takatostandGitHub 8fd75e6965 bump to 0.6.12-fix1 (#5743) 2024-06-29 17:43:20 +08:00
takatostandGitHub 0b8faade6f fix: env SMTP_PORT is empty caused err when launching (#5742) 2024-06-29 17:34:12 +08:00
takatostandGitHub d56cedfc67 fix: app config does not use empty string in the env (#5741) 2024-06-29 17:15:25 +08:00
takatostandGitHub 906857b28a fix: couldn't log in or resetup after a failed setup (#5739) 2024-06-29 17:07:21 +08:00
Nam VuandGitHub 9513155fa4 chore: support both $$ and $ latex format (#5723) 2024-06-29 11:24:25 +08:00
Chenhe GuandGitHub a6356be348 Rename README to README.md (#5727) 2024-06-29 00:53:14 +08:00
Chenhe GuandGitHub f33ef92f0c Chore/set entrypoint scripts permissions (#5726) 2024-06-29 00:48:34 +08:00
Chenhe GuandGitHub d435230059 add README for new docker/ directory (#5724) 2024-06-29 00:29:44 +08:00
255 changed files with 8434 additions and 3389 deletions
+2 -1
View File
@@ -1,9 +1,10 @@
#!/bin/bash
cd web && npm install
pipx install poetry
echo 'alias start-api="cd /workspaces/dify/api && flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && npm run dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc
+7
View File
@@ -0,0 +1,7 @@
# Ensure that .sh scripts use LF as line separator, even if they are checked out
# to Windows(NTFS) file-system, by a user of Docker for Window.
# These .sh scripts will be run from the Container after `docker compose up -d`.
# If they appear to be CRLF style, Dash from the Container will fail to execute
# them.
*.sh text eol=lf
+16 -14
View File
@@ -1,13 +1,21 @@
# Checklist:
> [!IMPORTANT]
> Please review the checklist below before submitting your pull request.
- [ ] Please open an issue before creating a PR or link to an existing issue
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. Close issue syntax: `Fixes #<issue number>`, see [documentation](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more details.
Fixes # (issue)
Fixes
## Type of Change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
@@ -15,18 +23,12 @@ Please delete options that are not relevant.
- [ ] Improvement, including but not limited to code refactoring, performance optimization, and UI/UX improvement
- [ ] Dependency upgrade
# How Has This Been Tested?
# Testing Instructions
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] TODO
- [ ] Test A
- [ ] Test B
# Suggested Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings
- [ ] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
- [ ] `optional` I have made corresponding changes to the documentation
- [ ] `optional` I have added tests that prove my fix is effective or that my feature works
- [ ] `optional` New and existing unit tests pass locally with my changes
+3
View File
@@ -60,6 +60,9 @@ jobs:
cp docker/.env.example docker/.env
cp docker/middleware.env.example docker/middleware.env
- name: Expose Service Ports
run: sh .github/workflows/expose_service_ports.sh
- name: Set up Sandbox
uses: hoverkraft-tech/compose-action@v2.0.0
with:
+6 -6
View File
@@ -48,18 +48,18 @@ jobs:
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
+5
View File
@@ -38,6 +38,11 @@ jobs:
- name: Install dependencies
run: poetry install -C api
- name: Prepare middleware env
run: |
cd docker
cp middleware.env.example middleware.env
- name: Set up Middlewares
uses: hoverkraft-tech/compose-action@v2.0.0
with:
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
yq eval '.services.weaviate.ports += ["8080:8080"]' -i docker/docker-compose.yaml
yq eval '.services.qdrant.ports += ["6333:6333"]' -i docker/docker-compose.yaml
yq eval '.services.chroma.ports += ["8000:8000"]' -i docker/docker-compose.yaml
yq eval '.services["milvus-standalone"].ports += ["19530:19530"]' -i docker/docker-compose.yaml
yq eval '.services.pgvector.ports += ["5433:5432"]' -i docker/docker-compose.yaml
yq eval '.services["pgvecto-rs"].ports += ["5431:5432"]' -i docker/docker-compose.yaml
echo "Ports exposed for sandbox, weaviate, qdrant, chroma, milvus, pgvector, pgvecto-rs."
+2
View File
@@ -174,3 +174,5 @@ sdks/python-client/dify_client.egg-info
.vscode/*
!.vscode/launch.json
pyrightconfig.json
.idea/
+1 -2
View File
@@ -13,7 +13,6 @@
"jinja": true,
"env": {
"FLASK_APP": "app.py",
"FLASK_DEBUG": "1",
"GEVENT_SUPPORT": "True"
},
"args": [
@@ -48,7 +47,7 @@
"--loglevel",
"info",
"-Q",
"dataset,generation,mail,ops_trace"
"dataset,generation,mail,ops_trace,app_deletion"
]
},
]
+5
View File
@@ -192,6 +192,11 @@ If you'd like to configure a highly-available setup, there are community-contrib
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### Using Terraform for Deployment
##### Azure Global
Deploy Dify to Azure with a single click using [terraform](https://www.terraform.io/).
- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform)
## Contributing
+6
View File
@@ -175,6 +175,12 @@ docker compose up -d
- [رسم بياني Helm من قبل @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### استخدام Terraform للتوزيع
##### Azure Global
استخدم [terraform](https://www.terraform.io/) لنشر Dify على Azure بنقرة واحدة.
- [Azure Terraform بواسطة @nikawang](https://github.com/nikawang/dify-azure-terraform)
## المساهمة
+6
View File
@@ -197,6 +197,12 @@ docker compose up -d
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### 使用 Terraform 部署
##### Azure Global
使用 [terraform](https://www.terraform.io/) 一键部署 Dify 到 Azure。
- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date)
+6
View File
@@ -199,6 +199,12 @@ Si desea configurar una configuración de alta disponibilidad, la comunidad prop
- [Gráfico Helm por @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### Uso de Terraform para el despliegue
##### Azure Global
Utiliza [terraform](https://www.terraform.io/) para desplegar Dify en Azure con un solo clic.
- [Azure Terraform por @nikawang](https://github.com/nikawang/dify-azure-terraform)
## Contribuir
+6
View File
@@ -197,6 +197,12 @@ Si vous souhaitez configurer une configuration haute disponibilité, la communau
- [Helm Chart par @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### Utilisation de Terraform pour le déploiement
##### Azure Global
Utilisez [terraform](https://www.terraform.io/) pour déployer Dify sur Azure en un clic.
- [Azure Terraform par @nikawang](https://github.com/nikawang/dify-azure-terraform)
## Contribuer
+6
View File
@@ -196,6 +196,12 @@ docker compose up -d
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### Terraformを使用したデプロイ
##### Azure Global
[terraform](https://www.terraform.io/) を使用して、AzureにDifyをワンクリックでデプロイします。
- [nikawangのAzure Terraform](https://github.com/nikawang/dify-azure-terraform)
## 貢献
+7
View File
@@ -197,6 +197,13 @@ If you'd like to configure a highly-available setup, there are community-contrib
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### Terraform atorlugu pilersitsineq
##### Azure Global
Atoruk [terraform](https://www.terraform.io/) Dify-mik Azure-mut ataatsikkut ikkussuilluarlugu.
- [Azure Terraform atorlugu @nikawang](https://github.com/nikawang/dify-azure-terraform)
## Contributing
For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
+6
View File
@@ -190,6 +190,12 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
#### Terraform을 사용한 배포
##### Azure Global
[terraform](https://www.terraform.io/)을 사용하여 Azure에 Dify를 원클릭으로 배포하세요.
- [nikawang의 Azure Terraform](https://github.com/nikawang/dify-azure-terraform)
## 기여
코드에 기여하고 싶은 분들은 [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요.
+7
View File
@@ -72,6 +72,13 @@ TENCENT_COS_SECRET_ID=your-secret-id
TENCENT_COS_REGION=your-region
TENCENT_COS_SCHEME=your-scheme
# OCI Storage configuration
OCI_ENDPOINT=your-endpoint
OCI_BUCKET_NAME=your-bucket-name
OCI_ACCESS_KEY=your-access-key
OCI_SECRET_KEY=your-secret-key
OCI_REGION=your-region
# CORS configuration
WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
+1 -2
View File
@@ -5,8 +5,7 @@ WORKDIR /app/api
# Install Poetry
ENV POETRY_VERSION=1.8.3
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir --upgrade poetry==${POETRY_VERSION}
RUN pip install --no-cache-dir poetry==${POETRY_VERSION}
# Configure Poetry
ENV POETRY_CACHE_DIR=/tmp/poetry_cache
+2 -1
View File
@@ -11,6 +11,7 @@
```bash
cd ../docker
cp middleware.env.example middleware.env
docker compose -f docker-compose.middleware.yaml -p dify up -d
cd ../api
```
@@ -66,7 +67,7 @@
10. If you need to debug local async processing, please start the worker service.
```bash
poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace
poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion
```
The started celery app handles the async tasks, e.g. dataset importing and documents indexing.
+5 -3
View File
@@ -1,8 +1,8 @@
import os
from configs.app_config import DifyConfig
from configs import dify_config
if not os.environ.get("DEBUG") or os.environ.get("DEBUG", "false").lower() != 'true':
if os.environ.get("DEBUG", "false").lower() != 'true':
from gevent import monkey
monkey.patch_all()
@@ -43,6 +43,8 @@ from extensions import (
from extensions.ext_database import db
from extensions.ext_login import login_manager
from libs.passport import PassportService
# TODO: Find a way to avoid importing models here
from models import account, dataset, model, source, task, tool, tools, web
from services.account_service import AccountService
@@ -81,7 +83,7 @@ def create_flask_app_with_configs() -> Flask:
with configs loaded from .env file
"""
dify_app = DifyApp(__name__)
dify_app.config.from_mapping(DifyConfig().model_dump())
dify_app.config.from_mapping(dify_config.model_dump())
# populate configs into system environment variables
for key, value in dify_app.config.items():
+2 -1
View File
@@ -8,6 +8,7 @@ import click
from flask import current_app
from werkzeug.exceptions import NotFound
from configs import dify_config
from constants.languages import languages
from core.rag.datasource.vdb.vector_factory import Vector
from core.rag.datasource.vdb.vector_type import VectorType
@@ -112,7 +113,7 @@ def reset_encrypt_key_pair():
After the reset, all LLM credentials will become invalid, requiring re-entry.
Only support SELF_HOSTED mode.
"""
if current_app.config['EDITION'] != 'SELF_HOSTED':
if dify_config.EDITION != 'SELF_HOSTED':
click.echo(click.style('Sorry, only support SELF_HOSTED mode.', fg='red'))
return
+3
View File
@@ -0,0 +1,3 @@
from .app_config import DifyConfig
dify_config = DifyConfig()
+29 -5
View File
@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, computed_field
from pydantic_settings import SettingsConfigDict
from configs.deploy import DeploymentConfig
from configs.enterprise import EnterpriseFeatureConfig
@@ -9,9 +10,6 @@ from configs.packaging import PackagingInfo
class DifyConfig(
# based on pydantic-settings
BaseSettings,
# Packaging info
PackagingInfo,
@@ -31,13 +29,39 @@ class DifyConfig(
# **Before using, please contact business@dify.ai by email to inquire about licensing matters.**
EnterpriseFeatureConfig,
):
DEBUG: bool = Field(default=False, description='whether to enable debug mode.')
model_config = SettingsConfigDict(
# read from dotenv format config file
env_file='.env',
env_file_encoding='utf-8',
env_ignore_empty=True,
frozen=True,
# ignore extra attributes
extra='ignore',
)
CODE_MAX_NUMBER: int = 9223372036854775807
CODE_MIN_NUMBER: int = -9223372036854775808
CODE_MAX_STRING_LENGTH: int = 80000
CODE_MAX_STRING_ARRAY_LENGTH: int = 30
CODE_MAX_OBJECT_ARRAY_LENGTH: int = 30
CODE_MAX_NUMBER_ARRAY_LENGTH: int = 1000
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: int = 300
HTTP_REQUEST_MAX_READ_TIMEOUT: int = 600
HTTP_REQUEST_MAX_WRITE_TIMEOUT: int = 600
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: int = 1024 * 1024 * 10
@computed_field
def HTTP_REQUEST_NODE_READABLE_MAX_BINARY_SIZE(self) -> str:
return f'{self.HTTP_REQUEST_NODE_MAX_BINARY_SIZE / 1024 / 1024:.2f}MB'
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: int = 1024 * 1024
@computed_field
def HTTP_REQUEST_NODE_READABLE_MAX_TEXT_SIZE(self) -> str:
return f'{self.HTTP_REQUEST_NODE_MAX_TEXT_SIZE / 1024 / 1024:.2f}MB'
SSRF_PROXY_HTTP_URL: str | None = None
SSRF_PROXY_HTTPS_URL: str | None = None
+3 -2
View File
@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class DeploymentConfig(BaseModel):
class DeploymentConfig(BaseSettings):
"""
Deployment configs
"""
+3 -2
View File
@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class EnterpriseFeatureConfig(BaseModel):
class EnterpriseFeatureConfig(BaseSettings):
"""
Enterprise feature configs.
**Before using, please contact business@dify.ai by email to inquire about licensing matters.**
-2
View File
@@ -1,5 +1,3 @@
from pydantic import BaseModel
from configs.extra.notion_config import NotionConfig
from configs.extra.sentry_config import SentryConfig
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class NotionConfig(BaseModel):
class NotionConfig(BaseSettings):
"""
Notion integration configs
"""
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, NonNegativeFloat
from pydantic import Field, NonNegativeFloat
from pydantic_settings import BaseSettings
class SentryConfig(BaseModel):
class SentryConfig(BaseSettings):
"""
Sentry configs
"""
+34 -29
View File
@@ -1,11 +1,12 @@
from typing import Optional
from pydantic import AliasChoices, BaseModel, Field, NonNegativeInt, PositiveInt, computed_field
from pydantic import AliasChoices, Field, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings
from configs.feature.hosted_service import HostedServiceConfig
class SecurityConfig(BaseModel):
class SecurityConfig(BaseSettings):
"""
Secret Key configs
"""
@@ -17,8 +18,12 @@ class SecurityConfig(BaseModel):
default=None,
)
RESET_PASSWORD_TOKEN_EXPIRY_HOURS: PositiveInt = Field(
description='Expiry time in hours for reset token',
default=24,
)
class AppExecutionConfig(BaseModel):
class AppExecutionConfig(BaseSettings):
"""
App Execution configs
"""
@@ -28,7 +33,7 @@ class AppExecutionConfig(BaseModel):
)
class CodeExecutionSandboxConfig(BaseModel):
class CodeExecutionSandboxConfig(BaseSettings):
"""
Code Execution Sandbox configs
"""
@@ -43,36 +48,36 @@ class CodeExecutionSandboxConfig(BaseModel):
)
class EndpointConfig(BaseModel):
class EndpointConfig(BaseSettings):
"""
Module URL configs
"""
CONSOLE_API_URL: str = Field(
description='The backend URL prefix of the console API.'
'used to concatenate the login authorization callback or notion integration callback.',
default='https://cloud.dify.ai',
default='',
)
CONSOLE_WEB_URL: str = Field(
description='The front-end URL prefix of the console web.'
'used to concatenate some front-end addresses and for CORS configuration use.',
default='https://cloud.dify.ai',
default='',
)
SERVICE_API_URL: str = Field(
description='Service API Url prefix.'
'used to display Service API Base Url to the front-end.',
default='https://api.dify.ai',
default='',
)
APP_WEB_URL: str = Field(
description='WebApp Url prefix.'
'used to display WebAPP API Base Url to the front-end.',
default='https://udify.app',
default='',
)
class FileAccessConfig(BaseModel):
class FileAccessConfig(BaseSettings):
"""
File Access configs
"""
@@ -82,7 +87,7 @@ class FileAccessConfig(BaseModel):
'Url is signed and has expiration time.',
validation_alias=AliasChoices('FILES_URL', 'CONSOLE_API_URL'),
alias_priority=1,
default='https://cloud.dify.ai',
default='',
)
FILES_ACCESS_TIMEOUT: int = Field(
@@ -91,7 +96,7 @@ class FileAccessConfig(BaseModel):
)
class FileUploadConfig(BaseModel):
class FileUploadConfig(BaseSettings):
"""
File Uploading configs
"""
@@ -116,7 +121,7 @@ class FileUploadConfig(BaseModel):
)
class HttpConfig(BaseModel):
class HttpConfig(BaseSettings):
"""
HTTP configs
"""
@@ -136,7 +141,7 @@ class HttpConfig(BaseModel):
def CONSOLE_CORS_ALLOW_ORIGINS(self) -> list[str]:
return self.inner_CONSOLE_CORS_ALLOW_ORIGINS.split(',')
inner_WEB_API_CORS_ALLOW_ORIGINS: Optional[str] = Field(
inner_WEB_API_CORS_ALLOW_ORIGINS: str = Field(
description='',
validation_alias=AliasChoices('WEB_API_CORS_ALLOW_ORIGINS'),
default='*',
@@ -148,7 +153,7 @@ class HttpConfig(BaseModel):
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(',')
class InnerAPIConfig(BaseModel):
class InnerAPIConfig(BaseSettings):
"""
Inner API configs
"""
@@ -163,7 +168,7 @@ class InnerAPIConfig(BaseModel):
)
class LoggingConfig(BaseModel):
class LoggingConfig(BaseSettings):
"""
Logging configs
"""
@@ -195,7 +200,7 @@ class LoggingConfig(BaseModel):
)
class ModelLoadBalanceConfig(BaseModel):
class ModelLoadBalanceConfig(BaseSettings):
"""
Model load balance configs
"""
@@ -205,7 +210,7 @@ class ModelLoadBalanceConfig(BaseModel):
)
class BillingConfig(BaseModel):
class BillingConfig(BaseSettings):
"""
Platform Billing Configurations
"""
@@ -215,7 +220,7 @@ class BillingConfig(BaseModel):
)
class UpdateConfig(BaseModel):
class UpdateConfig(BaseSettings):
"""
Update configs
"""
@@ -225,7 +230,7 @@ class UpdateConfig(BaseModel):
)
class WorkflowConfig(BaseModel):
class WorkflowConfig(BaseSettings):
"""
Workflow feature configs
"""
@@ -246,7 +251,7 @@ class WorkflowConfig(BaseModel):
)
class OAuthConfig(BaseModel):
class OAuthConfig(BaseSettings):
"""
oauth configs
"""
@@ -276,7 +281,7 @@ class OAuthConfig(BaseModel):
)
class ModerationConfig(BaseModel):
class ModerationConfig(BaseSettings):
"""
Moderation in app configs.
"""
@@ -288,7 +293,7 @@ class ModerationConfig(BaseModel):
)
class ToolConfig(BaseModel):
class ToolConfig(BaseSettings):
"""
Tool configs
"""
@@ -299,7 +304,7 @@ class ToolConfig(BaseModel):
)
class MailConfig(BaseModel):
class MailConfig(BaseSettings):
"""
Mail Configurations
"""
@@ -355,7 +360,7 @@ class MailConfig(BaseModel):
)
class RagEtlConfig(BaseModel):
class RagEtlConfig(BaseSettings):
"""
RAG ETL Configurations.
"""
@@ -381,7 +386,7 @@ class RagEtlConfig(BaseModel):
)
class DataSetConfig(BaseModel):
class DataSetConfig(BaseSettings):
"""
Dataset configs
"""
@@ -392,7 +397,7 @@ class DataSetConfig(BaseModel):
)
class WorkspaceConfig(BaseModel):
class WorkspaceConfig(BaseSettings):
"""
Workspace configs
"""
@@ -403,7 +408,7 @@ class WorkspaceConfig(BaseModel):
)
class IndexingConfig(BaseModel):
class IndexingConfig(BaseSettings):
"""
Indexing configs.
"""
@@ -414,7 +419,7 @@ class IndexingConfig(BaseModel):
)
class ImageFormatConfig(BaseModel):
class ImageFormatConfig(BaseSettings):
MULTIMODAL_SEND_IMAGE_FORMAT: str = Field(
description='multi model send image format, support base64, url, default is base64',
default='base64',
+10 -9
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, NonNegativeInt
from pydantic import Field, NonNegativeInt
from pydantic_settings import BaseSettings
class HostedOpenAiConfig(BaseModel):
class HostedOpenAiConfig(BaseSettings):
"""
Hosted OpenAI service config
"""
@@ -68,7 +69,7 @@ class HostedOpenAiConfig(BaseModel):
)
class HostedAzureOpenAiConfig(BaseModel):
class HostedAzureOpenAiConfig(BaseSettings):
"""
Hosted OpenAI service config
"""
@@ -94,7 +95,7 @@ class HostedAzureOpenAiConfig(BaseModel):
)
class HostedAnthropicConfig(BaseModel):
class HostedAnthropicConfig(BaseSettings):
"""
Hosted Azure OpenAI service config
"""
@@ -125,7 +126,7 @@ class HostedAnthropicConfig(BaseModel):
)
class HostedMinmaxConfig(BaseModel):
class HostedMinmaxConfig(BaseSettings):
"""
Hosted Minmax service config
"""
@@ -136,7 +137,7 @@ class HostedMinmaxConfig(BaseModel):
)
class HostedSparkConfig(BaseModel):
class HostedSparkConfig(BaseSettings):
"""
Hosted Spark service config
"""
@@ -147,7 +148,7 @@ class HostedSparkConfig(BaseModel):
)
class HostedZhipuAIConfig(BaseModel):
class HostedZhipuAIConfig(BaseSettings):
"""
Hosted Minmax service config
"""
@@ -158,7 +159,7 @@ class HostedZhipuAIConfig(BaseModel):
)
class HostedModerationConfig(BaseModel):
class HostedModerationConfig(BaseSettings):
"""
Hosted Moderation service config
"""
@@ -174,7 +175,7 @@ class HostedModerationConfig(BaseModel):
)
class HostedFetchAppTemplateConfig(BaseModel):
class HostedFetchAppTemplateConfig(BaseSettings):
"""
Hosted Moderation service config
"""
+20 -7
View File
@@ -1,12 +1,14 @@
from typing import Any, Optional
from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt, computed_field
from pydantic import Field, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings
from configs.middleware.cache.redis_config import RedisConfig
from configs.middleware.storage.aliyun_oss_storage_config import AliyunOSSStorageConfig
from configs.middleware.storage.amazon_s3_storage_config import S3StorageConfig
from configs.middleware.storage.azure_blob_storage_config import AzureBlobStorageConfig
from configs.middleware.storage.google_cloud_storage_config import GoogleCloudStorageConfig
from configs.middleware.storage.oci_storage_config import OCIStorageConfig
from configs.middleware.storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig
from configs.middleware.vdb.chroma_config import ChromaConfig
from configs.middleware.vdb.milvus_config import MilvusConfig
@@ -21,7 +23,7 @@ from configs.middleware.vdb.tidb_vector_config import TiDBVectorConfig
from configs.middleware.vdb.weaviate_config import WeaviateConfig
class StorageConfig(BaseModel):
class StorageConfig(BaseSettings):
STORAGE_TYPE: str = Field(
description='storage type,'
' default to `local`,'
@@ -35,14 +37,14 @@ class StorageConfig(BaseModel):
)
class VectorStoreConfig(BaseModel):
class VectorStoreConfig(BaseSettings):
VECTOR_STORE: Optional[str] = Field(
description='vector store type',
default=None,
)
class KeywordStoreConfig(BaseModel):
class KeywordStoreConfig(BaseSettings):
KEYWORD_STORE: str = Field(
description='keyword store type',
default='jieba',
@@ -80,6 +82,11 @@ class DatabaseConfig:
default='',
)
DB_EXTRAS: str = Field(
description='db extras options. Example: keepalives_idle=60&keepalives=1',
default='',
)
SQLALCHEMY_DATABASE_URI_SCHEME: str = Field(
description='db uri scheme',
default='postgresql',
@@ -88,7 +95,12 @@ class DatabaseConfig:
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
db_extras = f"?client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else ""
db_extras = (
f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}"
if self.DB_CHARSET
else self.DB_EXTRAS
).strip("&")
db_extras = f"?{db_extras}" if db_extras else ""
return (f"{self.SQLALCHEMY_DATABASE_URI_SCHEME}://"
f"{self.DB_USERNAME}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}"
f"{db_extras}")
@@ -113,7 +125,7 @@ class DatabaseConfig:
default=False,
)
SQLALCHEMY_ECHO: bool = Field(
SQLALCHEMY_ECHO: bool | str = Field(
description='whether to enable SqlAlchemy echo',
default=False,
)
@@ -143,7 +155,7 @@ class CeleryConfig(DatabaseConfig):
@computed_field
@property
def CELERY_RESULT_BACKEND(self) -> str:
def CELERY_RESULT_BACKEND(self) -> str | None:
return 'db+{}'.format(self.SQLALCHEMY_DATABASE_URI) \
if self.CELERY_BACKEND == 'database' else self.CELERY_BROKER_URL
@@ -167,6 +179,7 @@ class MiddlewareConfig(
GoogleCloudStorageConfig,
TencentCloudCOSStorageConfig,
S3StorageConfig,
OCIStorageConfig,
# configs of vdb and vdb providers
VectorStoreConfig,
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt
from pydantic import Field, NonNegativeInt, PositiveInt
from pydantic_settings import BaseSettings
class RedisConfig(BaseModel):
class RedisConfig(BaseSettings):
"""
Redis configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class AliyunOSSStorageConfig(BaseModel):
class AliyunOSSStorageConfig(BaseSettings):
"""
Aliyun storage configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class S3StorageConfig(BaseModel):
class S3StorageConfig(BaseSettings):
"""
S3 storage configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class AzureBlobStorageConfig(BaseModel):
class AzureBlobStorageConfig(BaseSettings):
"""
Azure Blob storage configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class GoogleCloudStorageConfig(BaseModel):
class GoogleCloudStorageConfig(BaseSettings):
"""
Google Cloud storage configs
"""
@@ -0,0 +1,36 @@
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings
class OCIStorageConfig(BaseSettings):
"""
OCI storage configs
"""
OCI_ENDPOINT: Optional[str] = Field(
description='OCI storage endpoint',
default=None,
)
OCI_REGION: Optional[str] = Field(
description='OCI storage region',
default=None,
)
OCI_BUCKET_NAME: Optional[str] = Field(
description='OCI storage bucket name',
default=None,
)
OCI_ACCESS_KEY: Optional[str] = Field(
description='OCI storage access key',
default=None,
)
OCI_SECRET_KEY: Optional[str] = Field(
description='OCI storage secret key',
default=None,
)
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class TencentCloudCOSStorageConfig(BaseModel):
class TencentCloudCOSStorageConfig(BaseSettings):
"""
Tencent Cloud COS storage configs
"""
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class ChromaConfig(BaseModel):
class ChromaConfig(BaseSettings):
"""
Chroma configs
"""
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class MilvusConfig(BaseModel):
class MilvusConfig(BaseSettings):
"""
Milvus configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class OpenSearchConfig(BaseModel):
class OpenSearchConfig(BaseSettings):
"""
OpenSearch configs
"""
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class OracleConfig(BaseModel):
class OracleConfig(BaseSettings):
"""
ORACLE configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class PGVectorConfig(BaseModel):
class PGVectorConfig(BaseSettings):
"""
PGVector configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class PGVectoRSConfig(BaseModel):
class PGVectoRSConfig(BaseSettings):
"""
PGVectoRS configs
"""
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt
from pydantic import Field, NonNegativeInt, PositiveInt
from pydantic_settings import BaseSettings
class QdrantConfig(BaseModel):
class QdrantConfig(BaseSettings):
"""
Qdrant configs
"""
+3 -2
View File
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class RelytConfig(BaseModel):
class RelytConfig(BaseSettings):
"""
Relyt configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, NonNegativeInt, PositiveInt
from pydantic_settings import BaseSettings
class TencentVectorDBConfig(BaseModel):
class TencentVectorDBConfig(BaseSettings):
"""
Tencent Vector configs
"""
@@ -24,7 +25,7 @@ class TencentVectorDBConfig(BaseModel):
)
TENCENT_VECTOR_DB_USERNAME: Optional[str] = Field(
description='Tencent Vector password',
description='Tencent Vector username',
default=None,
)
@@ -38,7 +39,12 @@ class TencentVectorDBConfig(BaseModel):
default=1,
)
TENCENT_VECTOR_DB_REPLICAS: PositiveInt = Field(
TENCENT_VECTOR_DB_REPLICAS: NonNegativeInt = Field(
description='Tencent Vector replicas',
default=2,
)
TENCENT_VECTOR_DB_DATABASE: Optional[str] = Field(
description='Tencent Vector Database',
default=None,
)
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class TiDBVectorConfig(BaseModel):
class TiDBVectorConfig(BaseSettings):
"""
TiDB Vector configs
"""
@@ -1,9 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field, PositiveInt
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class WeaviateConfig(BaseModel):
class WeaviateConfig(BaseSettings):
"""
Weaviate configs
"""
+4 -3
View File
@@ -1,14 +1,15 @@
from pydantic import BaseModel, Field
from pydantic import Field
from pydantic_settings import BaseSettings
class PackagingInfo(BaseModel):
class PackagingInfo(BaseSettings):
"""
Packaging build information
"""
CURRENT_VERSION: str = Field(
description='Dify version',
default='0.6.12',
default='0.6.13',
)
COMMIT_SHA: str = Field(
+1 -1
View File
@@ -14,7 +14,7 @@ language_timezone_mapping = {
'vi-VN': 'Asia/Ho_Chi_Minh',
'ro-RO': 'Europe/Bucharest',
'pl-PL': 'Europe/Warsaw',
'hi-IN': 'Asia/Kolkata'
'hi-IN': 'Asia/Kolkata',
}
languages = list(language_timezone_mapping.keys())
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -30,7 +30,7 @@ from .app import (
)
# Import auth controllers
from .auth import activate, data_source_bearer_auth, data_source_oauth, login, oauth
from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth
# Import billing controllers
from .billing import billing
+4
View File
@@ -190,6 +190,10 @@ class AppExportApi(Resource):
@get_app_model
def get(self, app_model):
"""Export app"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
app_service = AppService()
return {
@@ -6,6 +6,7 @@ from flask_login import current_user
from flask_restful import Resource
from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.console import api
from libs.login import login_required
from libs.oauth_data_source import NotionOAuth
@@ -16,11 +17,11 @@ from ..wraps import account_initialization_required
def get_oauth_providers():
with current_app.app_context():
notion_oauth = NotionOAuth(client_id=current_app.config.get('NOTION_CLIENT_ID'),
client_secret=current_app.config.get(
'NOTION_CLIENT_SECRET'),
redirect_uri=current_app.config.get(
'CONSOLE_API_URL') + '/console/api/oauth/data-source/callback/notion')
if not dify_config.NOTION_CLIENT_ID or not dify_config.NOTION_CLIENT_SECRET:
return {}
notion_oauth = NotionOAuth(client_id=dify_config.NOTION_CLIENT_ID,
client_secret=dify_config.NOTION_CLIENT_SECRET,
redirect_uri=dify_config.CONSOLE_API_URL + '/console/api/oauth/data-source/callback/notion')
OAUTH_PROVIDERS = {
'notion': notion_oauth
@@ -39,8 +40,10 @@ class OAuthDataSource(Resource):
print(vars(oauth_provider))
if not oauth_provider:
return {'error': 'Invalid provider'}, 400
if current_app.config.get('NOTION_INTEGRATION_TYPE') == 'internal':
internal_secret = current_app.config.get('NOTION_INTERNAL_SECRET')
if dify_config.NOTION_INTEGRATION_TYPE == 'internal':
internal_secret = dify_config.NOTION_INTERNAL_SECRET
if not internal_secret:
return {'error': 'Internal secret is not set'},
oauth_provider.save_internal_access_token(internal_secret)
return { 'data': '' }
else:
@@ -60,13 +63,13 @@ class OAuthDataSourceCallback(Resource):
if 'code' in request.args:
code = request.args.get('code')
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?type=notion&code={code}')
return redirect(f'{dify_config.CONSOLE_WEB_URL}?type=notion&code={code}')
elif 'error' in request.args:
error = request.args.get('error')
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?type=notion&error={error}')
return redirect(f'{dify_config.CONSOLE_WEB_URL}?type=notion&error={error}')
else:
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?type=notion&error=Access denied')
return redirect(f'{dify_config.CONSOLE_WEB_URL}?type=notion&error=Access denied')
class OAuthDataSourceBinding(Resource):
+25
View File
@@ -5,3 +5,28 @@ class ApiKeyAuthFailedError(BaseHTTPException):
error_code = 'auth_failed'
description = "{message}"
code = 500
class InvalidEmailError(BaseHTTPException):
error_code = 'invalid_email'
description = "The email address is not valid."
code = 400
class PasswordMismatchError(BaseHTTPException):
error_code = 'password_mismatch'
description = "The passwords do not match."
code = 400
class InvalidTokenError(BaseHTTPException):
error_code = 'invalid_or_expired_token'
description = "The token is invalid or has expired."
code = 400
class PasswordResetRateLimitExceededError(BaseHTTPException):
error_code = 'password_reset_rate_limit_exceeded'
description = "Password reset rate limit exceeded. Try again later."
code = 429
@@ -0,0 +1,107 @@
import base64
import logging
import secrets
from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.auth.error import (
InvalidEmailError,
InvalidTokenError,
PasswordMismatchError,
PasswordResetRateLimitExceededError,
)
from controllers.console.setup import setup_required
from extensions.ext_database import db
from libs.helper import email as email_validate
from libs.password import hash_password, valid_password
from models.account import Account
from services.account_service import AccountService
from services.errors.account import RateLimitExceededError
class ForgotPasswordSendEmailApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('email', type=str, required=True, location='json')
args = parser.parse_args()
email = args['email']
if not email_validate(email):
raise InvalidEmailError()
account = Account.query.filter_by(email=email).first()
if account:
try:
AccountService.send_reset_password_email(account=account)
except RateLimitExceededError:
logging.warning(f"Rate limit exceeded for email: {account.email}")
raise PasswordResetRateLimitExceededError()
else:
# Return success to avoid revealing email registration status
logging.warning(f"Attempt to reset password for unregistered email: {email}")
return {"result": "success"}
class ForgotPasswordCheckApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('token', type=str, required=True, nullable=False, location='json')
args = parser.parse_args()
token = args['token']
reset_data = AccountService.get_reset_password_data(token)
if reset_data is None:
return {'is_valid': False, 'email': None}
return {'is_valid': True, 'email': reset_data.get('email')}
class ForgotPasswordResetApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('token', type=str, required=True, nullable=False, location='json')
parser.add_argument('new_password', type=valid_password, required=True, nullable=False, location='json')
parser.add_argument('password_confirm', type=valid_password, required=True, nullable=False, location='json')
args = parser.parse_args()
new_password = args['new_password']
password_confirm = args['password_confirm']
if str(new_password).strip() != str(password_confirm).strip():
raise PasswordMismatchError()
token = args['token']
reset_data = AccountService.get_reset_password_data(token)
if reset_data is None:
raise InvalidTokenError()
AccountService.revoke_reset_password_token(token)
salt = secrets.token_bytes(16)
base64_salt = base64.b64encode(salt).decode()
password_hashed = hash_password(new_password, salt)
base64_password_hashed = base64.b64encode(password_hashed).decode()
account = Account.query.filter_by(email=reset_data.get('email')).first()
account.password = base64_password_hashed
account.password_salt = base64_salt
db.session.commit()
return {'result': 'success'}
api.add_resource(ForgotPasswordSendEmailApi, '/forgot-password')
api.add_resource(ForgotPasswordCheckApi, '/forgot-password/validity')
api.add_resource(ForgotPasswordResetApi, '/forgot-password/resets')
+19 -19
View File
@@ -1,7 +1,7 @@
from typing import cast
import flask_login
from flask import current_app, request
from flask import request
from flask_restful import Resource, reqparse
import services
@@ -56,14 +56,14 @@ class LogoutApi(Resource):
class ResetPasswordApi(Resource):
@setup_required
def get(self):
parser = reqparse.RequestParser()
parser.add_argument('email', type=email, required=True, location='json')
args = parser.parse_args()
# parser = reqparse.RequestParser()
# parser.add_argument('email', type=email, required=True, location='json')
# args = parser.parse_args()
# import mailchimp_transactional as MailchimpTransactional
# from mailchimp_transactional.api_client import ApiClientError
account = {'email': args['email']}
# account = {'email': args['email']}
# account = AccountService.get_by_email(args['email'])
# if account is None:
# raise ValueError('Email not found')
@@ -71,22 +71,22 @@ class ResetPasswordApi(Resource):
# AccountService.update_password(account, new_password)
# todo: Send email
MAILCHIMP_API_KEY = current_app.config['MAILCHIMP_TRANSACTIONAL_API_KEY']
# MAILCHIMP_API_KEY = current_app.config['MAILCHIMP_TRANSACTIONAL_API_KEY']
# mailchimp = MailchimpTransactional(MAILCHIMP_API_KEY)
message = {
'from_email': 'noreply@example.com',
'to': [{'email': account.email}],
'subject': 'Reset your Dify password',
'html': """
<p>Dear User,</p>
<p>The Dify team has generated a new password for you, details as follows:</p>
<p><strong>{new_password}</strong></p>
<p>Please change your password to log in as soon as possible.</p>
<p>Regards,</p>
<p>The Dify Team</p>
"""
}
# message = {
# 'from_email': 'noreply@example.com',
# 'to': [{'email': account['email']}],
# 'subject': 'Reset your Dify password',
# 'html': """
# <p>Dear User,</p>
# <p>The Dify team has generated a new password for you, details as follows:</p>
# <p><strong>{new_password}</strong></p>
# <p>Please change your password to log in as soon as possible.</p>
# <p>Regards,</p>
# <p>The Dify Team</p>
# """
# }
# response = mailchimp.messages.send({
# 'message': message,
+21 -23
View File
@@ -6,6 +6,7 @@ import requests
from flask import current_app, redirect, request
from flask_restful import Resource
from configs import dify_config
from constants.languages import languages
from extensions.ext_database import db
from libs.helper import get_remote_ip
@@ -18,22 +19,24 @@ from .. import api
def get_oauth_providers():
with current_app.app_context():
github_oauth = GitHubOAuth(client_id=current_app.config.get('GITHUB_CLIENT_ID'),
client_secret=current_app.config.get(
'GITHUB_CLIENT_SECRET'),
redirect_uri=current_app.config.get(
'CONSOLE_API_URL') + '/console/api/oauth/authorize/github')
if not dify_config.GITHUB_CLIENT_ID or not dify_config.GITHUB_CLIENT_SECRET:
github_oauth = None
else:
github_oauth = GitHubOAuth(
client_id=dify_config.GITHUB_CLIENT_ID,
client_secret=dify_config.GITHUB_CLIENT_SECRET,
redirect_uri=dify_config.CONSOLE_API_URL + '/console/api/oauth/authorize/github',
)
if not dify_config.GOOGLE_CLIENT_ID or not dify_config.GOOGLE_CLIENT_SECRET:
google_oauth = None
else:
google_oauth = GoogleOAuth(
client_id=dify_config.GOOGLE_CLIENT_ID,
client_secret=dify_config.GOOGLE_CLIENT_SECRET,
redirect_uri=dify_config.CONSOLE_API_URL + '/console/api/oauth/authorize/google',
)
google_oauth = GoogleOAuth(client_id=current_app.config.get('GOOGLE_CLIENT_ID'),
client_secret=current_app.config.get(
'GOOGLE_CLIENT_SECRET'),
redirect_uri=current_app.config.get(
'CONSOLE_API_URL') + '/console/api/oauth/authorize/google')
OAUTH_PROVIDERS = {
'github': github_oauth,
'google': google_oauth
}
OAUTH_PROVIDERS = {'github': github_oauth, 'google': google_oauth}
return OAUTH_PROVIDERS
@@ -63,8 +66,7 @@ class OAuthCallback(Resource):
token = oauth_provider.get_access_token(code)
user_info = oauth_provider.get_user_info(token)
except requests.exceptions.HTTPError as e:
logging.exception(
f"An error occurred during the OAuth process with {provider}: {e.response.text}")
logging.exception(f'An error occurred during the OAuth process with {provider}: {e.response.text}')
return {'error': 'OAuth process failed'}, 400
account = _generate_account(provider, user_info)
@@ -81,7 +83,7 @@ class OAuthCallback(Resource):
token = AccountService.login(account, ip_address=get_remote_ip(request))
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}?console_token={token}')
return redirect(f'{dify_config.CONSOLE_WEB_URL}?console_token={token}')
def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> Optional[Account]:
@@ -101,11 +103,7 @@ def _generate_account(provider: str, user_info: OAuthUserInfo):
# Create account
account_name = user_info.name if user_info.name else 'Dify'
account = RegisterService.register(
email=user_info.email,
name=account_name,
password=None,
open_id=user_info.id,
provider=provider
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider
)
# Set interface language
@@ -226,6 +226,15 @@ class DatasetApi(Resource):
except services.errors.dataset.DatasetInUseError:
raise DatasetInUseError()
class DatasetUseCheckApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, dataset_id):
dataset_id_str = str(dataset_id)
dataset_is_using = DatasetService.dataset_use_check(dataset_id_str)
return {'is_using': dataset_is_using}, 200
class DatasetQueryApi(Resource):
@@ -562,6 +571,7 @@ class DatasetErrorDocs(Resource):
api.add_resource(DatasetListApi, '/datasets')
api.add_resource(DatasetApi, '/datasets/<uuid:dataset_id>')
api.add_resource(DatasetUseCheckApi, '/datasets/<uuid:dataset_id>/use-check')
api.add_resource(DatasetQueryApi, '/datasets/<uuid:dataset_id>/queries')
api.add_resource(DatasetErrorDocs, '/datasets/<uuid:dataset_id>/error-docs')
api.add_resource(DatasetIndexingEstimateApi, '/datasets/indexing-estimate')
+5 -17
View File
@@ -3,11 +3,10 @@ from functools import wraps
from flask import current_app, request
from flask_restful import Resource, reqparse
from extensions.ext_database import db
from libs.helper import email, get_remote_ip, str_len
from libs.password import valid_password
from models.model import DifySetup
from services.account_service import AccountService, RegisterService, TenantService
from services.account_service import RegisterService, TenantService
from . import api
from .error import AlreadySetupError, NotInitValidateError, NotSetupError
@@ -51,28 +50,17 @@ class SetupApi(Resource):
required=True, location='json')
args = parser.parse_args()
# Register
account = RegisterService.register(
# setup
RegisterService.setup(
email=args['email'],
name=args['name'],
password=args['password']
password=args['password'],
ip_address=get_remote_ip(request)
)
TenantService.create_owner_tenant_if_not_exist(account)
setup()
AccountService.update_last_login(account, ip_address=get_remote_ip(request))
return {'result': 'success'}, 201
def setup():
dify_setup = DifySetup(
version=current_app.config['CURRENT_VERSION']
)
db.session.add(dify_setup)
def setup_required(view):
@wraps(view)
def decorated(*args, **kwargs):
@@ -245,6 +245,8 @@ class AccountIntegrateApi(Resource):
return {'data': integrate_data}
# Register API resources
api.add_resource(AccountInitApi, '/account/init')
api.add_resource(AccountProfileApi, '/account/profile')
+4
View File
@@ -114,6 +114,10 @@ class VariableEntity(BaseModel):
default: Optional[str] = None
hint: Optional[str] = None
@property
def name(self) -> str:
return self.variable
class ExternalDataVariableEntity(BaseModel):
"""
+48 -44
View File
@@ -1,52 +1,56 @@
from collections.abc import Mapping
from typing import Any, Optional
from core.app.app_config.entities import AppConfig, VariableEntity
class BaseAppGenerator:
def _get_cleaned_inputs(self, user_inputs: dict, app_config: AppConfig):
if user_inputs is None:
user_inputs = {}
filtered_inputs = {}
def _get_cleaned_inputs(self, user_inputs: Optional[Mapping[str, Any]], app_config: AppConfig) -> Mapping[str, Any]:
user_inputs = user_inputs or {}
# Filter input variables from form configuration, handle required fields, default values, and option values
variables = app_config.variables
for variable_config in variables:
variable = variable_config.variable
if (variable not in user_inputs
or user_inputs[variable] is None
or (isinstance(user_inputs[variable], str) and user_inputs[variable] == '')):
if variable_config.required:
raise ValueError(f"{variable} is required in input form")
else:
filtered_inputs[variable] = variable_config.default if variable_config.default is not None else ""
continue
value = user_inputs[variable]
if value is not None:
if variable_config.type != VariableEntity.Type.NUMBER and not isinstance(value, str):
raise ValueError(f"{variable} in input form must be a string")
elif variable_config.type == VariableEntity.Type.NUMBER and isinstance(value, str):
if '.' in value:
value = float(value)
else:
value = int(value)
if variable_config.type == VariableEntity.Type.SELECT:
options = variable_config.options if variable_config.options is not None else []
if value not in options:
raise ValueError(f"{variable} in input form must be one of the following: {options}")
elif variable_config.type in [VariableEntity.Type.TEXT_INPUT, VariableEntity.Type.PARAGRAPH]:
if variable_config.max_length is not None:
max_length = variable_config.max_length
if len(value) > max_length:
raise ValueError(f'{variable} in input form must be less than {max_length} characters')
if value and isinstance(value, str):
filtered_inputs[variable] = value.replace('\x00', '')
else:
filtered_inputs[variable] = value if value is not None else None
filtered_inputs = {var.name: self._validate_input(inputs=user_inputs, var=var) for var in variables}
filtered_inputs = {k: self._sanitize_value(v) for k, v in filtered_inputs.items()}
return filtered_inputs
def _validate_input(self, *, inputs: Mapping[str, Any], var: VariableEntity):
user_input_value = inputs.get(var.name)
if var.required and not user_input_value:
raise ValueError(f'{var.name} is required in input form')
if not var.required and not user_input_value:
# TODO: should we return None here if the default value is None?
return var.default or ''
if (
var.type
in (
VariableEntity.Type.TEXT_INPUT,
VariableEntity.Type.SELECT,
VariableEntity.Type.PARAGRAPH,
)
and user_input_value
and not isinstance(user_input_value, str)
):
raise ValueError(f"(type '{var.type}') {var.name} in input form must be a string")
if var.type == VariableEntity.Type.NUMBER and isinstance(user_input_value, str):
# may raise ValueError if user_input_value is not a valid number
try:
if '.' in user_input_value:
return float(user_input_value)
else:
return int(user_input_value)
except ValueError:
raise ValueError(f"{var.name} in input form must be a valid number")
if var.type == VariableEntity.Type.SELECT:
options = var.options or []
if user_input_value not in options:
raise ValueError(f'{var.name} in input form must be one of the following: {options}')
elif var.type in (VariableEntity.Type.TEXT_INPUT, VariableEntity.Type.PARAGRAPH):
if var.max_length and user_input_value and len(user_input_value) > var.max_length:
raise ValueError(f'{var.name} in input form must be less than {var.max_length} characters')
return user_input_value
def _sanitize_value(self, value: Any) -> Any:
if isinstance(value, str):
return value.replace('\x00', '')
return value
+2 -4
View File
@@ -94,7 +94,6 @@ class WorkflowAppGenerator(BaseAppGenerator):
application_generate_entity=application_generate_entity,
invoke_from=invoke_from,
stream=stream,
call_depth=call_depth,
)
def _generate(
@@ -104,7 +103,6 @@ class WorkflowAppGenerator(BaseAppGenerator):
application_generate_entity: WorkflowAppGenerateEntity,
invoke_from: InvokeFrom,
stream: bool = True,
call_depth: int = 0
) -> Union[dict, Generator[dict, None, None]]:
"""
Generate App response.
@@ -166,10 +164,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
"""
if not node_id:
raise ValueError('node_id is required')
if args.get('inputs') is None:
raise ValueError('inputs is required')
extras = {
"auto_generate_conversation_name": False
}
@@ -1,7 +1,6 @@
import os
import requests
from configs import dify_config
from models.api_based_extension import APIBasedExtensionPoint
@@ -31,10 +30,10 @@ class APIBasedExtensionRequestor:
try:
# proxy support for security
proxies = None
if os.environ.get("SSRF_PROXY_HTTP_URL") and os.environ.get("SSRF_PROXY_HTTPS_URL"):
if dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
proxies = {
'http': os.environ.get("SSRF_PROXY_HTTP_URL"),
'https': os.environ.get("SSRF_PROXY_HTTPS_URL"),
'http': dify_config.SSRF_PROXY_HTTP_URL,
'https': dify_config.SSRF_PROXY_HTTPS_URL,
}
response = requests.request(
+1 -1
View File
@@ -186,7 +186,7 @@ class MessageFileParser:
}
response = requests.head(url, headers=headers, allow_redirects=True)
if response.status_code == 200:
if response.status_code in {200, 304}:
return True, ""
else:
return False, "URL does not exist."
@@ -1,5 +1,4 @@
import logging
import os
import time
from enum import Enum
from threading import Lock
@@ -9,6 +8,7 @@ from httpx import get, post
from pydantic import BaseModel
from yarl import URL
from configs import dify_config
from core.helper.code_executor.entities import CodeDependency
from core.helper.code_executor.javascript.javascript_transformer import NodeJsTemplateTransformer
from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTransformer
@@ -18,8 +18,8 @@ from core.helper.code_executor.template_transformer import TemplateTransformer
logger = logging.getLogger(__name__)
# Code Executor
CODE_EXECUTION_ENDPOINT = os.environ.get('CODE_EXECUTION_ENDPOINT', 'http://sandbox:8194')
CODE_EXECUTION_API_KEY = os.environ.get('CODE_EXECUTION_API_KEY', 'dify-sandbox')
CODE_EXECUTION_ENDPOINT = dify_config.CODE_EXECUTION_ENDPOINT
CODE_EXECUTION_API_KEY = dify_config.CODE_EXECUTION_API_KEY
CODE_EXECUTION_TIMEOUT= (10, 60)
+26 -12
View File
@@ -12,7 +12,8 @@ from core.model_runtime.entities.message_entities import (
UserPromptMessage,
)
from extensions.ext_database import db
from models.model import AppMode, Conversation, Message
from models.model import AppMode, Conversation, Message, MessageFile
from models.workflow import WorkflowRun
class TokenBufferMemory:
@@ -30,33 +31,46 @@ class TokenBufferMemory:
app_record = self.conversation.app
# fetch limited messages, and return reversed
query = db.session.query(Message).filter(
query = db.session.query(
Message.id,
Message.query,
Message.answer,
Message.created_at,
Message.workflow_run_id
).filter(
Message.conversation_id == self.conversation.id,
Message.answer != ''
).order_by(Message.created_at.desc())
if message_limit and message_limit > 0:
messages = query.limit(message_limit).all()
message_limit = message_limit if message_limit <= 500 else 500
else:
messages = query.all()
message_limit = 500
messages = query.limit(message_limit).all()
messages = list(reversed(messages))
message_file_parser = MessageFileParser(
tenant_id=app_record.tenant_id,
app_id=app_record.id
)
prompt_messages = []
for message in messages:
files = message.message_files
files = db.session.query(MessageFile).filter(MessageFile.message_id == message.id).all()
if files:
file_extra_config = None
if self.conversation.mode not in [AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value]:
file_extra_config = FileUploadConfigManager.convert(message.app_model_config.to_dict())
file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config)
else:
file_extra_config = FileUploadConfigManager.convert(
message.workflow_run.workflow.features_dict,
is_vision=False
)
if message.workflow_run_id:
workflow_run = (db.session.query(WorkflowRun)
.filter(WorkflowRun.id == message.workflow_run_id).first())
if workflow_run:
file_extra_config = FileUploadConfigManager.convert(
workflow_run.workflow.features_dict,
is_vision=False
)
if file_extra_config:
file_objs = message_file_parser.transform_message_files(
@@ -136,4 +150,4 @@ class TokenBufferMemory:
message = f"{role}: {m.content}"
string_messages.append(message)
return "\n".join(string_messages)
return "\n".join(string_messages)
@@ -5,6 +5,8 @@ model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
@@ -5,6 +5,8 @@ model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
@@ -5,6 +5,8 @@ model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
@@ -5,6 +5,8 @@ model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
@@ -1,22 +1,14 @@
# standard import
import base64
import json
import logging
import mimetypes
import time
from collections.abc import Generator
from typing import Optional, Union, cast
# 3rd import
import boto3
import requests
from anthropic import AnthropicBedrock, Stream
from anthropic.types import (
ContentBlockDeltaEvent,
Message,
MessageDeltaEvent,
MessageStartEvent,
MessageStopEvent,
MessageStreamEvent,
)
from botocore.config import Config
from botocore.exceptions import (
ClientError,
@@ -27,7 +19,8 @@ from botocore.exceptions import (
)
from cohere import ChatMessage
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
# local import
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta
from core.model_runtime.entities.message_entities import (
AssistantPromptMessage,
ImagePromptMessageContent,
@@ -36,9 +29,9 @@ from core.model_runtime.entities.message_entities import (
PromptMessageTool,
SystemPromptMessage,
TextPromptMessageContent,
ToolPromptMessage,
UserPromptMessage,
)
from core.model_runtime.entities.model_entities import PriceType
from core.model_runtime.errors.invoke import (
InvokeAuthorizationError,
InvokeBadRequestError,
@@ -73,10 +66,10 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
:param user: unique user id
:return: full response or stream response chunk generator result
"""
# invoke anthropic models via anthropic official SDK
# TODO: consolidate different invocation methods for models based on base model capabilities
# invoke anthropic models via boto3 client
if "anthropic" in model:
return self._generate_anthropic(model, credentials, prompt_messages, model_parameters, stop, stream, user)
return self._generate_anthropic(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools)
# invoke Cohere models via boto3 client
if "cohere.command-r" in model:
return self._generate_cohere_chat(model, credentials, prompt_messages, model_parameters, stop, stream, user, tools)
@@ -159,7 +152,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
def _generate_anthropic(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None) -> Union[LLMResult, Generator]:
stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, tools: Optional[list[PromptMessageTool]] = None,) -> Union[LLMResult, Generator]:
"""
Invoke Anthropic large language model
@@ -171,48 +164,35 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
:param stream: is stream response
:return: full response or stream response chunk generator result
"""
# use Anthropic official SDK references
# - https://docs.anthropic.com/claude/reference/claude-on-amazon-bedrock
# - https://github.com/anthropics/anthropic-sdk-python
client = AnthropicBedrock(
aws_access_key=credentials.get("aws_access_key_id"),
aws_secret_key=credentials.get("aws_secret_access_key"),
aws_region=credentials["aws_region"],
)
bedrock_client = boto3.client(service_name='bedrock-runtime',
aws_access_key_id=credentials.get("aws_access_key_id"),
aws_secret_access_key=credentials.get("aws_secret_access_key"),
region_name=credentials["aws_region"])
extra_model_kwargs = {}
if stop:
extra_model_kwargs['stop_sequences'] = stop
system, prompt_message_dicts = self._convert_converse_prompt_messages(prompt_messages)
inference_config, additional_model_fields = self._convert_converse_api_model_parameters(model_parameters, stop)
# Notice: If you request the current version of the SDK to the bedrock server,
# you will get the following error message and you need to wait for the service or SDK to be updated.
# Response: Error code: 400
# {'message': 'Malformed input request: #: subject must not be valid against schema
# {"required":["messages"]}#: extraneous key [metadata] is not permitted, please reformat your input and try again.'}
# TODO: Open in the future when the interface is properly supported
# if user:
# ref: https://github.com/anthropics/anthropic-sdk-python/blob/e84645b07ca5267066700a104b4d8d6a8da1383d/src/anthropic/resources/messages.py#L465
# extra_model_kwargs['metadata'] = message_create_params.Metadata(user_id=user)
parameters = {
'modelId': model,
'messages': prompt_message_dicts,
'inferenceConfig': inference_config,
'additionalModelRequestFields': additional_model_fields,
}
system, prompt_message_dicts = self._convert_claude_prompt_messages(prompt_messages)
if system and len(system) > 0:
parameters['system'] = system
if system:
extra_model_kwargs['system'] = system
response = client.messages.create(
model=model,
messages=prompt_message_dicts,
stream=stream,
**model_parameters,
**extra_model_kwargs
)
if tools:
parameters['toolConfig'] = self._convert_converse_tool_config(tools=tools)
if stream:
return self._handle_claude_stream_response(model, credentials, response, prompt_messages)
response = bedrock_client.converse_stream(**parameters)
return self._handle_converse_stream_response(model, credentials, response, prompt_messages)
else:
response = bedrock_client.converse(**parameters)
return self._handle_converse_response(model, credentials, response, prompt_messages)
return self._handle_claude_response(model, credentials, response, prompt_messages)
def _handle_claude_response(self, model: str, credentials: dict, response: Message,
def _handle_converse_response(self, model: str, credentials: dict, response: dict,
prompt_messages: list[PromptMessage]) -> LLMResult:
"""
Handle llm chat response
@@ -223,17 +203,16 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
:param prompt_messages: prompt messages
:return: full response chunk generator result
"""
# transform assistant message to prompt message
assistant_prompt_message = AssistantPromptMessage(
content=response.content[0].text
content=response['output']['message']['content'][0]['text']
)
# calculate num tokens
if response.usage:
if response['usage']:
# transform usage
prompt_tokens = response.usage.input_tokens
completion_tokens = response.usage.output_tokens
prompt_tokens = response['usage']['inputTokens']
completion_tokens = response['usage']['outputTokens']
else:
# calculate num tokens
prompt_tokens = self.get_num_tokens(model, credentials, prompt_messages)
@@ -242,17 +221,15 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
# transform usage
usage = self._calc_response_usage(model, credentials, prompt_tokens, completion_tokens)
# transform response
response = LLMResult(
model=response.model,
result = LLMResult(
model=model,
prompt_messages=prompt_messages,
message=assistant_prompt_message,
usage=usage
usage=usage,
)
return result
return response
def _handle_claude_stream_response(self, model: str, credentials: dict, response: Stream[MessageStreamEvent],
def _handle_converse_stream_response(self, model: str, credentials: dict, response: dict,
prompt_messages: list[PromptMessage], ) -> Generator:
"""
Handle llm chat stream response
@@ -271,129 +248,143 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
output_tokens = 0
finish_reason = None
index = 0
tool_calls: list[AssistantPromptMessage.ToolCall] = []
tool_use = {}
for chunk in response:
if isinstance(chunk, MessageStartEvent):
return_model = chunk.message.model
input_tokens = chunk.message.usage.input_tokens
elif isinstance(chunk, MessageDeltaEvent):
output_tokens = chunk.usage.output_tokens
finish_reason = chunk.delta.stop_reason
elif isinstance(chunk, MessageStopEvent):
for chunk in response['stream']:
if 'messageStart' in chunk:
return_model = model
elif 'messageStop' in chunk:
finish_reason = chunk['messageStop']['stopReason']
elif 'contentBlockStart' in chunk:
tool = chunk['contentBlockStart']['start']['toolUse']
tool_use['toolUseId'] = tool['toolUseId']
tool_use['name'] = tool['name']
elif 'metadata' in chunk:
input_tokens = chunk['metadata']['usage']['inputTokens']
output_tokens = chunk['metadata']['usage']['outputTokens']
usage = self._calc_response_usage(model, credentials, input_tokens, output_tokens)
yield LLMResultChunk(
model=return_model,
prompt_messages=prompt_messages,
delta=LLMResultChunkDelta(
index=index + 1,
index=index,
message=AssistantPromptMessage(
content=''
content='',
tool_calls=tool_calls
),
finish_reason=finish_reason,
usage=usage
)
)
elif isinstance(chunk, ContentBlockDeltaEvent):
chunk_text = chunk.delta.text if chunk.delta.text else ''
full_assistant_content += chunk_text
assistant_prompt_message = AssistantPromptMessage(
content=chunk_text if chunk_text else '',
)
index = chunk.index
yield LLMResultChunk(
model=model,
prompt_messages=prompt_messages,
delta=LLMResultChunkDelta(
index=index,
message=assistant_prompt_message,
elif 'contentBlockDelta' in chunk:
delta = chunk['contentBlockDelta']['delta']
if 'text' in delta:
chunk_text = delta['text'] if delta['text'] else ''
full_assistant_content += chunk_text
assistant_prompt_message = AssistantPromptMessage(
content=chunk_text if chunk_text else '',
)
)
index = chunk['contentBlockDelta']['contentBlockIndex']
yield LLMResultChunk(
model=model,
prompt_messages=prompt_messages,
delta=LLMResultChunkDelta(
index=index+1,
message=assistant_prompt_message,
)
)
elif 'toolUse' in delta:
if 'input' not in tool_use:
tool_use['input'] = ''
tool_use['input'] += delta['toolUse']['input']
elif 'contentBlockStop' in chunk:
if 'input' in tool_use:
tool_call = AssistantPromptMessage.ToolCall(
id=tool_use['toolUseId'],
type='function',
function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=tool_use['name'],
arguments=tool_use['input']
)
)
tool_calls.append(tool_call)
tool_use = {}
except Exception as ex:
raise InvokeError(str(ex))
def _convert_converse_api_model_parameters(self, model_parameters: dict, stop: Optional[list[str]] = None) -> tuple[dict, dict]:
inference_config = {}
additional_model_fields = {}
if 'max_tokens' in model_parameters:
inference_config['maxTokens'] = model_parameters['max_tokens']
def _calc_claude_response_usage(self, model: str, credentials: dict, prompt_tokens: int, completion_tokens: int) -> LLMUsage:
"""
Calculate response usage
if 'temperature' in model_parameters:
inference_config['temperature'] = model_parameters['temperature']
if 'top_p' in model_parameters:
inference_config['topP'] = model_parameters['temperature']
:param model: model name
:param credentials: model credentials
:param prompt_tokens: prompt tokens
:param completion_tokens: completion tokens
:return: usage
"""
# get prompt price info
prompt_price_info = self.get_price(
model=model,
credentials=credentials,
price_type=PriceType.INPUT,
tokens=prompt_tokens,
)
if stop:
inference_config['stopSequences'] = stop
if 'top_k' in model_parameters:
additional_model_fields['top_k'] = model_parameters['top_k']
return inference_config, additional_model_fields
# get completion price info
completion_price_info = self.get_price(
model=model,
credentials=credentials,
price_type=PriceType.OUTPUT,
tokens=completion_tokens
)
# transform usage
usage = LLMUsage(
prompt_tokens=prompt_tokens,
prompt_unit_price=prompt_price_info.unit_price,
prompt_price_unit=prompt_price_info.unit,
prompt_price=prompt_price_info.total_amount,
completion_tokens=completion_tokens,
completion_unit_price=completion_price_info.unit_price,
completion_price_unit=completion_price_info.unit,
completion_price=completion_price_info.total_amount,
total_tokens=prompt_tokens + completion_tokens,
total_price=prompt_price_info.total_amount + completion_price_info.total_amount,
currency=prompt_price_info.currency,
latency=time.perf_counter() - self.started_at
)
return usage
def _convert_claude_prompt_messages(self, prompt_messages: list[PromptMessage]) -> tuple[str, list[dict]]:
def _convert_converse_prompt_messages(self, prompt_messages: list[PromptMessage]) -> tuple[str, list[dict]]:
"""
Convert prompt messages to dict list and system
"""
system = ""
first_loop = True
system = []
for message in prompt_messages:
if isinstance(message, SystemPromptMessage):
message.content=message.content.strip()
if first_loop:
system=message.content
first_loop=False
else:
system+="\n"
system+=message.content
system.append({"text": message.content})
prompt_message_dicts = []
for message in prompt_messages:
if not isinstance(message, SystemPromptMessage):
prompt_message_dicts.append(self._convert_claude_prompt_message_to_dict(message))
prompt_message_dicts.append(self._convert_prompt_message_to_dict(message))
return system, prompt_message_dicts
def _convert_claude_prompt_message_to_dict(self, message: PromptMessage) -> dict:
def _convert_converse_tool_config(self, tools: Optional[list[PromptMessageTool]] = None) -> dict:
tool_config = {}
configs = []
if tools:
for tool in tools:
configs.append(
{
"toolSpec": {
"name": tool.name,
"description": tool.description,
"inputSchema": {
"json": tool.parameters
}
}
}
)
tool_config["tools"] = configs
return tool_config
def _convert_prompt_message_to_dict(self, message: PromptMessage) -> dict:
"""
Convert PromptMessage to dict
"""
if isinstance(message, UserPromptMessage):
message = cast(UserPromptMessage, message)
if isinstance(message.content, str):
message_dict = {"role": "user", "content": message.content}
message_dict = {"role": "user", "content": [{'text': message.content}]}
else:
sub_messages = []
for message_content in message.content:
if message_content.type == PromptMessageContentType.TEXT:
message_content = cast(TextPromptMessageContent, message_content)
sub_message_dict = {
"type": "text",
"text": message_content.data
}
sub_messages.append(sub_message_dict)
@@ -404,24 +395,24 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
try:
image_content = requests.get(message_content.data).content
mime_type, _ = mimetypes.guess_type(message_content.data)
base64_data = base64.b64encode(image_content).decode('utf-8')
except Exception as ex:
raise ValueError(f"Failed to fetch image data from url {message_content.data}, {ex}")
else:
data_split = message_content.data.split(";base64,")
mime_type = data_split[0].replace("data:", "")
base64_data = data_split[1]
image_content = base64.b64decode(base64_data)
if mime_type not in ["image/jpeg", "image/png", "image/gif", "image/webp"]:
raise ValueError(f"Unsupported image type {mime_type}, "
f"only support image/jpeg, image/png, image/gif, and image/webp")
sub_message_dict = {
"type": "image",
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_data
"image": {
"format": mime_type.replace('image/', ''),
"source": {
"bytes": image_content
}
}
}
sub_messages.append(sub_message_dict)
@@ -429,10 +420,32 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
message_dict = {"role": "user", "content": sub_messages}
elif isinstance(message, AssistantPromptMessage):
message = cast(AssistantPromptMessage, message)
message_dict = {"role": "assistant", "content": message.content}
if message.tool_calls:
message_dict = {
"role": "assistant", "content":[{
"toolUse": {
"toolUseId": message.tool_calls[0].id,
"name": message.tool_calls[0].function.name,
"input": json.loads(message.tool_calls[0].function.arguments)
}
}]
}
else:
message_dict = {"role": "assistant", "content": [{'text': message.content}]}
elif isinstance(message, SystemPromptMessage):
message = cast(SystemPromptMessage, message)
message_dict = {"role": "system", "content": message.content}
message_dict = [{'text': message.content}]
elif isinstance(message, ToolPromptMessage):
message = cast(ToolPromptMessage, message)
message_dict = {
"role": "user",
"content": [{
"toolResult": {
"toolUseId": message.tool_call_id,
"content": [{"json": {"text": message.content}}]
}
}]
}
else:
raise ValueError(f"Got unknown type {message}")
@@ -451,11 +464,13 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
"""
prefix = model.split('.')[0]
model_name = model.split('.')[1]
if isinstance(prompt_messages, str):
prompt = prompt_messages
else:
prompt = self._convert_messages_to_prompt(prompt_messages, prefix, model_name)
return self._get_num_tokens_by_gpt2(prompt)
def validate_credentials(self, model: str, credentials: dict) -> None:
@@ -539,11 +554,16 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
content = message.content
if isinstance(message, UserPromptMessage):
message_text = f"{human_prompt_prefix} {content} {human_prompt_postfix}"
body = content
if (isinstance(content, list)):
body = "".join([c.data for c in content if c.type == PromptMessageContentType.TEXT])
message_text = f"{human_prompt_prefix} {body} {human_prompt_postfix}"
elif isinstance(message, AssistantPromptMessage):
message_text = f"{ai_prompt} {content}"
elif isinstance(message, SystemPromptMessage):
message_text = content
elif isinstance(message, ToolPromptMessage):
message_text = f"{human_prompt_prefix} {message.content}"
else:
raise ValueError(f"Got unknown type {message}")
@@ -111,5 +111,71 @@ ModelConfigs = {
'mode': 'chat',
},
'features': [],
},
'Moonshot-v1-8k': {
'req_params': {
'max_prompt_tokens': 8192,
'max_new_tokens': 4096,
},
'model_properties': {
'context_size': 8192,
'mode': 'chat',
},
'features': [],
},
'Moonshot-v1-32k': {
'req_params': {
'max_prompt_tokens': 32768,
'max_new_tokens': 16384,
},
'model_properties': {
'context_size': 32768,
'mode': 'chat',
},
'features': [],
},
'Moonshot-v1-128k': {
'req_params': {
'max_prompt_tokens': 131072,
'max_new_tokens': 65536,
},
'model_properties': {
'context_size': 131072,
'mode': 'chat',
},
'features': [],
},
'GLM3-130B': {
'req_params': {
'max_prompt_tokens': 8192,
'max_new_tokens': 4096,
},
'model_properties': {
'context_size': 8192,
'mode': 'chat',
},
'features': [],
},
'GLM3-130B-Fin': {
'req_params': {
'max_prompt_tokens': 8192,
'max_new_tokens': 4096,
},
'model_properties': {
'context_size': 8192,
'mode': 'chat',
},
'features': [],
},
'Mistral-7B': {
'req_params': {
'max_prompt_tokens': 8192,
'max_new_tokens': 2048,
},
'model_properties': {
'context_size': 8192,
'mode': 'chat',
},
'features': [],
}
}
@@ -120,12 +120,6 @@ model_credential_schema:
show_on:
- variable: __model_type
value: llm
- label:
en_US: Skylark2-pro-4k
value: Skylark2-pro-4k
show_on:
- variable: __model_type
value: llm
- label:
en_US: Llama3-8B
value: Llama3-8B
@@ -138,6 +132,42 @@ model_credential_schema:
show_on:
- variable: __model_type
value: llm
- label:
en_US: Moonshot-v1-8k
value: Moonshot-v1-8k
show_on:
- variable: __model_type
value: llm
- label:
en_US: Moonshot-v1-32k
value: Moonshot-v1-32k
show_on:
- variable: __model_type
value: llm
- label:
en_US: Moonshot-v1-128k
value: Moonshot-v1-128k
show_on:
- variable: __model_type
value: llm
- label:
en_US: GLM3-130B
value: GLM3-130B
show_on:
- variable: __model_type
value: llm
- label:
en_US: GLM3-130B-Fin
value: GLM3-130B-Fin
show_on:
- variable: __model_type
value: llm
- label:
en_US: Mistral-7B
value: Mistral-7B
show_on:
- variable: __model_type
value: llm
- label:
en_US: Doubao-embedding
value: Doubao-embedding
@@ -181,7 +211,7 @@ model_credential_schema:
zh_Hans: 模型上下文长度
en_US: Model Context Size
type: text-input
default: '4096'
default: "4096"
placeholder:
zh_Hans: 输入您的模型上下文长度
en_US: Enter your Model Context Size
@@ -195,7 +225,7 @@ model_credential_schema:
label:
zh_Hans: 最大 token 上限
en_US: Upper Bound for Max Tokens
default: '4096'
default: "4096"
type: text-input
placeholder:
zh_Hans: 输入您的模型最大 token 上限
@@ -29,10 +29,8 @@ class ZhipuAI(HttpClient):
http_client: httpx.Client | None = None,
custom_headers: Mapping[str, str] | None = None
) -> None:
# if api_key is None:
# api_key = os.environ.get("ZHIPUAI_API_KEY")
if api_key is None:
raise ZhipuAIError("未提供api_key,请通过参数或环境变量提供")
raise ZhipuAIError("No api_key provided, please provide it through parameters or environment variables")
self.api_key = api_key
if base_url is None:
@@ -7,6 +7,8 @@ from typing import Any, Union, cast
import httpx
import pydantic
from httpx import URL, Timeout
from tenacity import retry
from tenacity.stop import stop_after_attempt
from . import _errors
from ._base_type import NOT_GIVEN, Body, Data, Headers, NotGiven, Query, RequestFiles, ResponseT
@@ -221,6 +223,7 @@ class HttpClient:
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
@retry(stop=stop_after_attempt(ZHIPUAI_DEFAULT_MAX_RETRIES))
def request(
self,
*,
-21
View File
@@ -1,21 +0,0 @@
from typing import Any, Literal
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
if PYDANTIC_V2:
from pydantic_core import Url as Url
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.model_dump(mode=mode, **kwargs)
else:
from pydantic import AnyUrl as Url # noqa: F401
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.dict(**kwargs)
+6 -10
View File
@@ -8,16 +8,20 @@ from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6
from pathlib import Path, PurePath
from re import Pattern
from types import GeneratorType
from typing import Any, Optional, Union
from typing import Any, Literal, Optional, Union
from uuid import UUID
from pydantic import BaseModel
from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr
from pydantic_core import Url
from pydantic_extra_types.color import Color
from ._compat import PYDANTIC_V2, Url, _model_dump
def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any:
return model.model_dump(mode=mode, **kwargs)
# Taken from Pydantic v1 as is
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
@@ -109,12 +113,6 @@ def jsonable_encoder(
if isinstance(obj, encoder_type):
return encoder_instance(obj)
if isinstance(obj, BaseModel):
# TODO: remove when deprecating Pydantic v1
encoders: dict[Any, Any] = {}
if not PYDANTIC_V2:
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
if custom_encoder:
encoders.update(custom_encoder)
obj_dict = _model_dump(
obj,
mode="json",
@@ -131,8 +129,6 @@ def jsonable_encoder(
obj_dict,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
# TODO: remove when deprecating Pydantic v1
custom_encoder=encoders,
sqlalchemy_safe=sqlalchemy_safe,
)
if dataclasses.is_dataclass(obj):
+2 -2
View File
@@ -27,8 +27,8 @@ class LangfuseConfig(BaseTracingConfig):
def set_value(cls, v, info: ValidationInfo):
if v is None or v == "":
v = 'https://api.langfuse.com'
if not v.startswith('https://'):
raise ValueError('host must start with https://')
if not v.startswith('https://') and not v.startswith('http://'):
raise ValueError('host must start with https:// or http://')
return v
+20 -4
View File
@@ -107,9 +107,21 @@ class LangFuseDataTrace(BaseTraceInstance):
# through workflow_run_id get all_nodes_execution
workflow_nodes_executions = (
db.session.query(WorkflowNodeExecution)
db.session.query(
WorkflowNodeExecution.id,
WorkflowNodeExecution.tenant_id,
WorkflowNodeExecution.app_id,
WorkflowNodeExecution.title,
WorkflowNodeExecution.node_type,
WorkflowNodeExecution.status,
WorkflowNodeExecution.inputs,
WorkflowNodeExecution.outputs,
WorkflowNodeExecution.created_at,
WorkflowNodeExecution.elapsed_time,
WorkflowNodeExecution.process_data,
WorkflowNodeExecution.execution_metadata,
)
.filter(WorkflowNodeExecution.workflow_run_id == trace_info.workflow_run_id)
.order_by(WorkflowNodeExecution.index.desc())
.all()
)
@@ -121,7 +133,9 @@ class LangFuseDataTrace(BaseTraceInstance):
node_type = node_execution.node_type
status = node_execution.status
if node_type == "llm":
inputs = json.loads(node_execution.process_data).get("prompts", {})
inputs = json.loads(node_execution.process_data).get(
"prompts", {}
) if node_execution.process_data else {}
else:
inputs = json.loads(node_execution.inputs) if node_execution.inputs else {}
outputs = (
@@ -213,7 +227,9 @@ class LangFuseDataTrace(BaseTraceInstance):
end_user_data: EndUser = db.session.query(EndUser).filter(
EndUser.id == message_data.from_end_user_id
).first()
user_id = end_user_data.session_id
if end_user_data is not None:
user_id = end_user_data.session_id
metadata["user_id"] = user_id
trace_data = LangfuseTrace(
id=message_id,
@@ -100,9 +100,21 @@ class LangSmithDataTrace(BaseTraceInstance):
# through workflow_run_id get all_nodes_execution
workflow_nodes_executions = (
db.session.query(WorkflowNodeExecution)
db.session.query(
WorkflowNodeExecution.id,
WorkflowNodeExecution.tenant_id,
WorkflowNodeExecution.app_id,
WorkflowNodeExecution.title,
WorkflowNodeExecution.node_type,
WorkflowNodeExecution.status,
WorkflowNodeExecution.inputs,
WorkflowNodeExecution.outputs,
WorkflowNodeExecution.created_at,
WorkflowNodeExecution.elapsed_time,
WorkflowNodeExecution.process_data,
WorkflowNodeExecution.execution_metadata,
)
.filter(WorkflowNodeExecution.workflow_run_id == trace_info.workflow_run_id)
.order_by(WorkflowNodeExecution.index.desc())
.all()
)
@@ -114,7 +126,9 @@ class LangSmithDataTrace(BaseTraceInstance):
node_type = node_execution.node_type
status = node_execution.status
if node_type == "llm":
inputs = json.loads(node_execution.process_data).get("prompts", {})
inputs = json.loads(node_execution.process_data).get(
"prompts", {}
) if node_execution.process_data else {}
else:
inputs = json.loads(node_execution.inputs) if node_execution.inputs else {}
outputs = (
@@ -181,13 +195,15 @@ class LangSmithDataTrace(BaseTraceInstance):
message_id = message_data.id
user_id = message_data.from_account_id
metadata["user_id"] = user_id
if message_data.from_end_user_id:
end_user_data: EndUser = db.session.query(EndUser).filter(
EndUser.id == message_data.from_end_user_id
).first().session_id
end_user_id = end_user_data.session_id
metadata["end_user_id"] = end_user_id
metadata["user_id"] = user_id
).first()
if end_user_data is not None:
end_user_id = end_user_data.session_id
metadata["end_user_id"] = end_user_id
message_run = LangSmithRunModel(
input_tokens=trace_info.message_tokens,
+9 -2
View File
@@ -352,10 +352,17 @@ class TraceTask:
query = workflow_run_inputs.get("query") or workflow_run_inputs.get("sys.query") or ""
# get workflow_app_log_id
workflow_app_log_data = db.session.query(WorkflowAppLog).filter_by(workflow_run_id=workflow_run.id).first()
workflow_app_log_data = db.session.query(WorkflowAppLog).filter_by(
tenant_id=tenant_id,
app_id=workflow_run.app_id,
workflow_run_id=workflow_run.id
).first()
workflow_app_log_id = str(workflow_app_log_data.id) if workflow_app_log_data else None
# get message_id
message_data = db.session.query(Message.id).filter_by(workflow_run_id=workflow_run_id).first()
message_data = db.session.query(Message.id).filter_by(
conversation_id=conversation_id,
workflow_run_id=workflow_run_id
).first()
message_id = str(message_data.id) if message_data else None
metadata = {
+15 -16
View File
@@ -140,11 +140,10 @@ class NotionExtractor(BaseExtractor):
def _get_notion_block_data(self, page_id: str) -> list[str]:
result_lines_arr = []
cur_block_id = page_id
start_cursor = None
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=page_id)
while True:
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id)
query_dict: dict[str, Any] = {}
query_dict: dict[str, Any] = {} if not start_cursor else {'start_cursor': start_cursor}
res = requests.request(
"GET",
block_url,
@@ -153,7 +152,7 @@ class NotionExtractor(BaseExtractor):
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
},
json=query_dict
params=query_dict
)
data = res.json()
for result in data["results"]:
@@ -191,16 +190,16 @@ class NotionExtractor(BaseExtractor):
if data["next_cursor"] is None:
break
else:
cur_block_id = data["next_cursor"]
start_cursor = data["next_cursor"]
return result_lines_arr
def _read_block(self, block_id: str, num_tabs: int = 0) -> str:
"""Read a block."""
result_lines_arr = []
cur_block_id = block_id
start_cursor = None
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=block_id)
while True:
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id)
query_dict: dict[str, Any] = {}
query_dict: dict[str, Any] = {} if not start_cursor else {'start_cursor': start_cursor}
res = requests.request(
"GET",
@@ -210,7 +209,7 @@ class NotionExtractor(BaseExtractor):
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
},
json=query_dict
params=query_dict
)
data = res.json()
if 'results' not in data or data["results"] is None:
@@ -249,7 +248,7 @@ class NotionExtractor(BaseExtractor):
if data["next_cursor"] is None:
break
else:
cur_block_id = data["next_cursor"]
start_cursor = data["next_cursor"]
result_lines = "\n".join(result_lines_arr)
return result_lines
@@ -258,10 +257,10 @@ class NotionExtractor(BaseExtractor):
"""Read table rows."""
done = False
result_lines_arr = []
cur_block_id = block_id
start_cursor = None
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=block_id)
while not done:
block_url = BLOCK_CHILD_URL_TMPL.format(block_id=cur_block_id)
query_dict: dict[str, Any] = {}
query_dict: dict[str, Any] = {} if not start_cursor else {'start_cursor': start_cursor}
res = requests.request(
"GET",
@@ -271,7 +270,7 @@ class NotionExtractor(BaseExtractor):
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
},
json=query_dict
params=query_dict
)
data = res.json()
# get table headers text
@@ -300,7 +299,7 @@ class NotionExtractor(BaseExtractor):
done = True
break
else:
cur_block_id = data["next_cursor"]
start_cursor = data["next_cursor"]
result_lines = "\n".join(result_lines_arr)
return result_lines
+28 -18
View File
@@ -5,6 +5,7 @@ Here, we will use GoogleSearch as an example to demonstrate how to quickly integ
## 1. Prepare the Tool Provider yaml
### Introduction
This yaml declares a new tool provider, and includes information like the provider's name, icon, author, and other details that are fetched by the frontend for display.
### Example
@@ -28,9 +29,11 @@ identity: # Basic information of the tool provider
- search
```
- The `identity` field is mandatory, it contains the basic information of the tool provider, including author, name, label, description, icon, etc.
- The icon needs to be placed in the `_assets` folder of the current module, you can refer to [here](../../provider/builtin/google/_assets/icon.svg).
- The `tags` field is optional, it is used to classify the provider, and the frontend can filter the provider according to the tag, for all tags, they have been listed below:
- The `identity` field is mandatory, it contains the basic information of the tool provider, including author, name, label, description, icon, etc.
- The icon needs to be placed in the `_assets` folder of the current module, you can refer to [here](../../provider/builtin/google/_assets/icon.svg).
- The `tags` field is optional, it is used to classify the provider, and the frontend can filter the provider according to the tag, for all tags, they have been listed below:
```python
class ToolLabelEnum(Enum):
SEARCH = 'search'
@@ -56,6 +59,7 @@ identity: # Basic information of the tool provider
Google, as a third-party tool, uses the API provided by SerpApi, which requires an API Key to use. This means that this tool needs a credential to use. For tools like `wikipedia`, there is no need to fill in the credential field, you can refer to [here](../../provider/builtin/wikipedia/wikipedia.yaml).
After configuring the credential field, the effect is as follows:
```yaml
identity:
author: Dify
@@ -87,6 +91,7 @@ credentials_for_provider: # Credential field
- `type`: Credential field type, currently can be either `secret-input`, `text-input`, or `select` , corresponding to password input box, text input box, and drop-down box, respectively. If set to `secret-input`, it will mask the input content on the frontend, and the backend will encrypt the input content.
## 3. Prepare Tool yaml
A provider can have multiple tools, each tool needs a yaml file to describe, this file contains the basic information, parameters, output, etc. of the tool.
Still taking GoogleSearch as an example, we need to create a `tools` module under the `google` module, and create `tools/google_search.yaml`, the content is as follows.
@@ -140,21 +145,22 @@ parameters: # Parameter list
- The `identity` field is mandatory, it contains the basic information of the tool, including name, author, label, description, etc.
- `parameters` Parameter list
- `name` Parameter name, unique, no duplication with other parameters
- `type` Parameter type, currently supports `string`, `number`, `boolean`, `select`, `secret-input` four types, corresponding to string, number, boolean, drop-down box, and encrypted input box, respectively. For sensitive information, we recommend using `secret-input` type
- `required` Required or not
- In `llm` mode, if the parameter is required, the Agent is required to infer this parameter
- In `form` mode, if the parameter is required, the user is required to fill in this parameter on the frontend before the conversation starts
- `options` Parameter options
- In `llm` mode, Dify will pass all options to LLM, LLM can infer based on these options
- In `form` mode, when `type` is `select`, the frontend will display these options
- `default` Default value
- `label` Parameter label, for frontend display
- `human_description` Introduction for frontend display, supports multiple languages
- `llm_description` Introduction passed to LLM, in order to make LLM better understand this parameter, we suggest to write as detailed information about this parameter as possible here, so that LLM can understand this parameter
- `form` Form type, currently supports `llm`, `form` two types, corresponding to Agent self-inference and frontend filling
- `name` Parameter name, unique, no duplication with other parameters
- `type` Parameter type, currently supports `string`, `number`, `boolean`, `select`, `secret-input` four types, corresponding to string, number, boolean, drop-down box, and encrypted input box, respectively. For sensitive information, we recommend using `secret-input` type
- `required` Required or not
- In `llm` mode, if the parameter is required, the Agent is required to infer this parameter
- In `form` mode, if the parameter is required, the user is required to fill in this parameter on the frontend before the conversation starts
- `options` Parameter options
- In `llm` mode, Dify will pass all options to LLM, LLM can infer based on these options
- In `form` mode, when `type` is `select`, the frontend will display these options
- `default` Default value
- `label` Parameter label, for frontend display
- `human_description` Introduction for frontend display, supports multiple languages
- `llm_description` Introduction passed to LLM, in order to make LLM better understand this parameter, we suggest to write as detailed information about this parameter as possible here, so that LLM can understand this parameter
- `form` Form type, currently supports `llm`, `form` two types, corresponding to Agent self-inference and frontend filling
## 4. Add Tool Logic
After completing the tool configuration, we can start writing the tool code that defines how it is invoked.
Create `google_search.py` under the `google/tools` module, the content is as follows.
@@ -176,7 +182,7 @@ class GoogleSearchTool(BuiltinTool):
query = tool_parameters['query']
result_type = tool_parameters['result_type']
api_key = self.runtime.credentials['serpapi_api_key']
# TODO: search with serpapi
# Search with serpapi
result = SerpAPI(api_key).run(query, result_type=result_type)
if result_type == 'text':
@@ -185,12 +191,15 @@ class GoogleSearchTool(BuiltinTool):
```
### Parameters
The overall logic of the tool is in the `_invoke` method, this method accepts two parameters: `user_id` and `tool_parameters`, which represent the user ID and tool parameters respectively
### Return Data
When the tool returns, you can choose to return one message or multiple messages, here we return one message, using `create_text_message` and `create_link_message` can create a text message or a link message.
## 5. Add Provider Code
Finally, we need to create a provider class under the provider module to implement the provider's credential verification logic. If the credential verification fails, it will throw a `ToolProviderCredentialValidationError` exception.
Create `google.py` under the `google` module, the content is as follows.
@@ -227,8 +236,9 @@ class GoogleProvider(BuiltinToolProviderController):
```
## Completion
After the above steps are completed, we can see this tool on the frontend, and it can be used in the Agent.
Of course, because google_search needs a credential, before using it, you also need to input your credentials on the frontend.
![Alt text](../zh_Hans/images/index/image-2.png)
![Alt text](../zh_Hans/images/index/image-2.png)
+1
View File
@@ -7,6 +7,7 @@
- azuredalle
- stability
- wikipedia
- nominatim
- yahoo
- arxiv
- pubmed
@@ -31,6 +31,12 @@ parameters:
- value: claude-3-haiku
label:
en_US: Claude 3
- value: llama-3-70b
label:
en_US: Llama 3
- value: mixtral-8x7b
label:
en_US: Mixtral
default: gpt-3.5
label:
en_US: Choose Model
@@ -6,7 +6,7 @@ identity:
zh_CN: Firecrawl
description:
en_US: Firecrawl API integration for web crawling and scraping.
zh_CN: Firecrawl API 集成,用于网页爬取和数据抓取。
zh_Hans: Firecrawl API 集成,用于网页爬取和数据抓取。
icon: icon.svg
tags:
- search
@@ -17,20 +17,22 @@ credentials_for_provider:
required: true
label:
en_US: Firecrawl API Key
zh_CN: Firecrawl API 密钥
zh_Hans: Firecrawl API 密钥
placeholder:
en_US: Please input your Firecrawl API key
zh_CN: 请输入您的 Firecrawl API 密钥
zh_Hans: 请输入您的 Firecrawl API 密钥,如果是自托管版本,可以随意填写密钥
help:
en_US: Get your Firecrawl API key from your Firecrawl account settings.
zh_CN: 从您的 Firecrawl 账户设置中获取 Firecrawl API 密钥。
en_US: Get your Firecrawl API key from your Firecrawl account settings.If you are using a self-hosted version, you may enter any key at your convenience.
zh_Hans: 从您的 Firecrawl 账户设置中获取 Firecrawl API 密钥。如果是自托管版本,可以随意填写密钥。
url: https://www.firecrawl.dev/account
base_url:
type: text-input
required: false
label:
en_US: Firecrawl server's Base URL
zh_Hans: Firecrawl服务器的API URL
pt_BR: Firecrawl server's Base URL
placeholder:
en_US: https://www.firecrawl.dev
zh_HansL: https://www.firecrawl.dev
pt_BR: https://www.firecrawl.dev
@@ -1,98 +1,93 @@
import time
from collections.abc import Mapping
from typing import Any
import requests
from requests.exceptions import HTTPError
class FirecrawlApp:
def __init__(self, api_key=None, base_url=None):
def __init__(self, api_key: str | None = None, base_url: str | None = None):
self.api_key = api_key
self.base_url = base_url or 'https://api.firecrawl.dev'
if self.api_key is None and self.base_url == 'https://api.firecrawl.dev':
raise ValueError('No API key provided')
if not self.api_key:
raise ValueError("API key is required")
def scrape_url(self, url, params=None) -> dict:
def _prepare_headers(self, idempotency_key: str | None = None):
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {self.api_key}'
}
json_data = {'url': url}
if params:
json_data.update(params)
response = requests.post(
f'{self.base_url}/v0/scrape',
headers=headers,
json=json_data
)
if response.status_code == 200:
response = response.json()
if response['success'] == True:
return response['data']
else:
raise Exception(f'Failed to scrape URL. Error: {response["error"]}')
if idempotency_key:
headers['Idempotency-Key'] = idempotency_key
return headers
elif response.status_code in [402, 409, 500]:
error_message = response.json().get('error', 'Unknown error occurred')
raise Exception(f'Failed to scrape URL. Status code: {response.status_code}. Error: {error_message}')
else:
raise Exception(f'Failed to scrape URL. Status code: {response.status_code}')
def crawl_url(self, url, params=None, wait_until_done=True, timeout=2) -> str:
headers = self._prepare_headers()
json_data = {'url': url}
if params:
json_data.update(params)
response = self._post_request(f'{self.base_url}/v0/crawl', json_data, headers)
if response.status_code == 200:
job_id = response.json().get('jobId')
if wait_until_done:
return self._monitor_job_status(job_id, headers, timeout)
else:
return {'jobId': job_id}
else:
self._handle_error(response, 'start crawl job')
def check_crawl_status(self, job_id) -> dict:
headers = self._prepare_headers()
response = self._get_request(f'{self.base_url}/v0/crawl/status/{job_id}', headers)
if response.status_code == 200:
return response.json()
else:
self._handle_error(response, 'check crawl status')
def _prepare_headers(self):
return {
'Content-Type': 'application/json',
'Authorization': f'Bearer {self.api_key}'
}
def _post_request(self, url, data, headers):
return requests.post(url, headers=headers, json=data)
def _get_request(self, url, headers):
return requests.get(url, headers=headers)
def _monitor_job_status(self, job_id, headers, timeout):
while True:
status_response = self._get_request(f'{self.base_url}/v0/crawl/status/{job_id}', headers)
if status_response.status_code == 200:
status_data = status_response.json()
if status_data['status'] == 'completed':
if 'data' in status_data:
return status_data['data']
else:
raise Exception('Crawl job completed but no data was returned')
elif status_data['status'] in ['active', 'paused', 'pending', 'queued']:
if timeout < 2:
timeout = 2
time.sleep(timeout) # Wait for the specified timeout before checking again
def _request(
self,
method: str,
url: str,
data: Mapping[str, Any] | None = None,
headers: Mapping[str, str] | None = None,
retries: int = 3,
backoff_factor: float = 0.3,
) -> Mapping[str, Any] | None:
for i in range(retries):
try:
response = requests.request(method, url, json=data, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
if i < retries - 1:
time.sleep(backoff_factor * (2 ** i))
else:
raise Exception(f'Crawl job failed or was stopped. Status: {status_data["status"]}')
else:
self._handle_error(status_response, 'check crawl status')
raise
return None
def _handle_error(self, response, action):
if response.status_code in [402, 409, 500]:
error_message = response.json().get('error', 'Unknown error occurred')
raise Exception(f'Failed to {action}. Status code: {response.status_code}. Error: {error_message}')
else:
raise Exception(f'Unexpected error occurred while trying to {action}. Status code: {response.status_code}')
def scrape_url(self, url: str, **kwargs):
endpoint = f'{self.base_url}/v0/scrape'
headers = self._prepare_headers()
data = {'url': url, **kwargs}
response = self._request('POST', endpoint, data, headers)
if response is None:
raise HTTPError("Failed to scrape URL after multiple retries")
return response
def search(self, query: str, **kwargs):
endpoint = f'{self.base_url}/v0/search'
headers = self._prepare_headers()
data = {'query': query, **kwargs}
response = self._request('POST', endpoint, data, headers)
if response is None:
raise HTTPError("Failed to perform search after multiple retries")
return response
def crawl_url(
self, url: str, wait: bool = False, poll_interval: int = 5, idempotency_key: str | None = None, **kwargs
):
endpoint = f'{self.base_url}/v0/crawl'
headers = self._prepare_headers(idempotency_key)
data = {'url': url, **kwargs}
response = self._request('POST', endpoint, data, headers)
if response is None:
raise HTTPError("Failed to initiate crawl after multiple retries")
job_id: str = response['jobId']
if wait:
return self._monitor_job_status(job_id=job_id, poll_interval=poll_interval)
return job_id
def check_crawl_status(self, job_id: str):
endpoint = f'{self.base_url}/v0/crawl/status/{job_id}'
headers = self._prepare_headers()
response = self._request('GET', endpoint, headers=headers)
if response is None:
raise HTTPError(f"Failed to check status for job {job_id} after multiple retries")
return response
def _monitor_job_status(self, job_id: str, poll_interval: int):
while True:
status = self.check_crawl_status(job_id)
if status['status'] == 'completed':
return status
elif status['status'] == 'failed':
raise HTTPError(f'Job {job_id} failed: {status["error"]}')
time.sleep(poll_interval)
@@ -1,3 +1,4 @@
import json
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
@@ -7,7 +8,6 @@ from core.tools.tool.builtin_tool import BuiltinTool
class CrawlTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
# initialize the app object with the api key
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url'])
options = {
@@ -21,29 +21,16 @@ class CrawlTool(BuiltinTool):
}
}
# crawl the url
crawl_result = app.crawl_url(
url=tool_parameters['url'],
params=options,
wait_until_done=True,
wait=True
)
# reformat crawl result
crawl_output = "**Crawl Result**\n\n"
try:
for result in crawl_result:
crawl_output += f"**- Title:** {result.get('metadata', {}).get('title', '')}\n"
crawl_output += f"**- Description:** {result.get('metadata', {}).get('description', '')}\n"
crawl_output += f"**- URL:** {result.get('metadata', {}).get('ogUrl', '')}\n\n"
crawl_output += f"**- Web Content:**\n{result.get('markdown', '')}\n\n"
crawl_output += "---\n\n"
except Exception as e:
crawl_output += f"An error occurred: {str(e)}\n"
crawl_output += f"**- Title:** {result.get('metadata', {}).get('title', '')}\n"
crawl_output += f"**- Description:** {result.get('metadata', {}).get('description','')}\n"
crawl_output += f"**- URL:** {result.get('metadata', {}).get('ogUrl', '')}\n\n"
crawl_output += f"**- Web Content:**\n{result.get('markdown', '')}\n\n"
crawl_output += "---\n\n"
if not isinstance(crawl_result, str):
crawl_result = json.dumps(crawl_result, ensure_ascii=False, indent=4)
return self.create_text_message(crawl_output)
if not crawl_result:
return self.create_text_message("Crawl request failed.")
return self.create_text_message(crawl_result)
@@ -0,0 +1,26 @@
import json
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp
from core.tools.tool.builtin_tool import BuiltinTool
class ScrapeTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url'])
crawl_result = app.scrape_url(
url=tool_parameters['url'],
wait=True
)
if isinstance(crawl_result, dict):
result_message = json.dumps(crawl_result, ensure_ascii=False, indent=4)
else:
result_message = str(crawl_result)
if not crawl_result:
return self.create_text_message("Scrape request failed.")
return self.create_text_message(result_message)
@@ -0,0 +1,23 @@
identity:
name: scrape
author: ahasasjeb
label:
en_US: Scrape
zh_Hans: 抓取
description:
human:
en_US: Extract data from a single URL.
zh_Hans: 从单个URL抓取数据。
llm: This tool is designed to scrape URL and output the content in Markdown format.
parameters:
- name: url
type: string
required: true
label:
en_US: URL to scrape
zh_Hans: 要抓取的URL
human_description:
en_US: The URL of the website to scrape and extract data from.
zh_Hans: 要抓取并提取数据的网站URL。
llm_description: The URL of the website that needs to be crawled. This is a required parameter.
form: llm
@@ -0,0 +1,26 @@
import json
from typing import Any, Union
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.firecrawl.firecrawl_appx import FirecrawlApp
from core.tools.tool.builtin_tool import BuiltinTool
class SearchTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
app = FirecrawlApp(api_key=self.runtime.credentials['firecrawl_api_key'], base_url=self.runtime.credentials['base_url'])
crawl_result = app.search(
query=tool_parameters['keyword'],
wait=True
)
if isinstance(crawl_result, dict):
result_message = json.dumps(crawl_result, ensure_ascii=False, indent=4)
else:
result_message = str(crawl_result)
if not crawl_result:
return self.create_text_message("Search request failed.")
return self.create_text_message(result_message)
@@ -0,0 +1,23 @@
identity:
name: search
author: ahasasjeb
label:
en_US: Search
zh_Hans: 搜索
description:
human:
en_US: Search, and output in Markdown format
zh_Hans: 搜索,并且以Markdown格式输出
llm: This tool can perform online searches and convert the results to Markdown format.
parameters:
- name: keyword
type: string
required: true
label:
en_US: keyword
zh_Hans: 关键词
human_description:
en_US: Input keywords to use Firecrawl API for search.
zh_Hans: 输入关键词即可使用Firecrawl API进行搜索。
llm_description: Efficiently extract keywords from user text.
form: llm
@@ -1,3 +1,4 @@
import json
from typing import Any, Union
from yarl import URL
@@ -26,6 +27,15 @@ class JinaReaderTool(BuiltinTool):
if 'api_key' in self.runtime.credentials and self.runtime.credentials.get('api_key'):
headers['Authorization'] = "Bearer " + self.runtime.credentials.get('api_key')
request_params = tool_parameters.get('request_params')
if request_params is not None and request_params != '':
try:
request_params = json.loads(request_params)
if not isinstance(request_params, dict):
raise ValueError("request_params must be a JSON object")
except (json.JSONDecodeError, ValueError) as e:
raise ValueError(f"Invalid request_params: {e}")
target_selector = tool_parameters.get('target_selector')
if target_selector is not None and target_selector != '':
headers['X-Target-Selector'] = target_selector
@@ -53,7 +63,8 @@ class JinaReaderTool(BuiltinTool):
response = ssrf_proxy.get(
str(URL(self._jina_reader_endpoint + url)),
headers=headers,
timeout=(10, 60)
params=request_params,
timeout=(10, 60),
)
if tool_parameters.get('summary', False):
@@ -25,6 +25,22 @@ parameters:
pt_BR: used for linking to webpages
llm_description: url for scraping
form: llm
- name: request_params
type: string
required: false
label:
en_US: Request params
zh_Hans: 请求参数
pt_BR: Request params
human_description:
en_US: |
request parameters, format: {"key1": "value1", "key2": "value2"}
zh_Hans: |
请求参数,格式:{"key1": "value1", "key2": "value2"}
pt_BR: |
request parameters, format: {"key1": "value1", "key2": "value2"}
llm_description: request parameters
form: llm
- name: target_selector
type: string
required: false
@@ -0,0 +1,277 @@
<svg xmlns="http://www.w3.org/2000/svg" height="256" width="256" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="c">
<stop stop-color="#2d3335" offset="0"/>
<stop stop-color="#4c464a" offset=".5"/>
<stop stop-color="#384042" offset="1"/>
</linearGradient>
<linearGradient id="a">
<stop offset="0"/>
<stop stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="u" y2="-179.97" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="9" y1="-94.672" x1="94.25"/>
<linearGradient id="v" y2="-30.438" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="35.75" y1="-114.44" x1="50.75"/>
<linearGradient id="w" y2="-74.625" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="115.25" y1="-163.12" x1="77.625"/>
<linearGradient id="x" y2="7.063" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="125.72" y1="-50.438" x1="56.5"/>
<linearGradient id="y" y2="-146.8" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="180.75" y1="-163.3" x1="35"/>
<linearGradient id="z" y2="-41.609" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="179" y1="-141.11" x1="57"/>
<linearGradient id="A" y2="39.75" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="208.25" y1="-26.5" x1="156"/>
<linearGradient id="B" y2="-77.297" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="235" y1="-169.3" x1="142.75"/>
<linearGradient id="m" y2="-113.72" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="166.5" y1="-161.36" x1="214.25"/>
<linearGradient id="b">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="n" y2="-62.469" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="57.656" y1="-126.5" x1="53.25"/>
<linearGradient id="o" y2="-17.125" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="75.719" y1="6.625" x1="39.5"/>
<linearGradient id="p" y2="-121.44" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="85.312" y1="-162.34" x1="102"/>
<linearGradient id="q" y2="-14.75" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="63.5" y1="-78.094" x1="117.75"/>
<linearGradient id="r" y2="-60.75" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="133.94" y1="-125.12" x1="180.69"/>
<linearGradient id="s" y2="33.375" xlink:href="#b" gradientUnits="userSpaceOnUse" x2="130.22" y1="-23.5" x1="170.75"/>
<linearGradient id="t" y2="-5.656" xlink:href="#b" gradientUnits="userSpaceOnUse" y1="-76" x2="181.75" x1="213.5"/>
<linearGradient id="l" y2="-213.25" xlink:href="#a" gradientUnits="userSpaceOnUse" x2="9" gradientTransform="translate(0 -10)" y1="72.064" x1="210.17"/>
<linearGradient id="M" y2="150.38" xlink:href="#c" gradientUnits="userSpaceOnUse" x2="133" gradientTransform="matrix(-1 0 0 1 46 0)" y1="150.38" x1="123"/>
<linearGradient id="L" y2="143.36" xlink:href="#c" gradientUnits="userSpaceOnUse" x2="130.88" gradientTransform="translate(-210)" y1="134.36" x1="128"/>
<linearGradient id="O" y2="150.38" gradientUnits="userSpaceOnUse" y1="150.38" gradientTransform="matrix(-2 0 0 1.7699 174 -86.65)" x2="133" x1="123">
<stop stop-color="#2d3335" offset="0"/>
<stop stop-color="#4c464a" offset=".5"/>
<stop stop-color="#384042" offset="1"/>
</linearGradient>
<linearGradient id="N" y2="186.5" gradientUnits="userSpaceOnUse" x2="136" gradientTransform="translate(-210)" y1="186.5" x1="120">
<stop stop-color="#f9e295" offset="0"/>
<stop stop-color="#f7dd84" offset=".125"/>
<stop stop-color="#fff" offset=".206"/>
<stop stop-color="#f4ce51" offset=".301"/>
<stop stop-color="#f9e7aa" offset=".341"/>
<stop stop-color="#efbb0e" offset="1"/>
</linearGradient>
<linearGradient id="P" y2="186.5" gradientUnits="userSpaceOnUse" y1="186.5" gradientTransform="translate(-210 67)" x2="136" x1="120">
<stop stop-color="#f9e295" offset="0"/>
<stop stop-color="#f7dd84" offset=".125"/>
<stop stop-color="#fff" offset=".206"/>
<stop stop-color="#f4ce51" offset=".301"/>
<stop stop-color="#f9e7aa" offset=".341"/>
<stop stop-color="#efbb0e" offset="1"/>
</linearGradient>
<linearGradient id="J" y2="186.5" gradientUnits="userSpaceOnUse" x2="136" gradientTransform="rotate(45 104.976 51.258) scale(1.39197)" y1="186.5" x1="120">
<stop stop-color="#f9e295" offset="0"/>
<stop stop-color="#f7dd84" offset=".125"/>
<stop stop-color="#fff" offset=".206"/>
<stop stop-color="#f4ce51" offset=".301"/>
<stop stop-color="#f9e7aa" offset=".341"/>
<stop stop-color="#efbb0e" offset="1"/>
</linearGradient>
<linearGradient id="U" y2="150.38" gradientUnits="userSpaceOnUse" x2="133" gradientTransform="rotate(-135 332.789 -39.72) scale(2.7839 -2.4637)" y1="150.38" x1="123">
<stop stop-color="#2d3335" offset="0"/>
<stop stop-color="#4c464a" offset=".5"/>
<stop stop-color="#384042" offset="1"/>
</linearGradient>
<linearGradient id="T" y2="186.5" gradientUnits="userSpaceOnUse" x2="136" gradientTransform="rotate(45 217.552 97.892) scale(1.39197)" y1="186.5" x1="120">
<stop stop-color="#f9e295" offset="0"/>
<stop stop-color="#f7dd84" offset=".125"/>
<stop stop-color="#fff" offset=".206"/>
<stop stop-color="#f4ce51" offset=".301"/>
<stop stop-color="#f9e7aa" offset=".341"/>
<stop stop-color="#efbb0e" offset="1"/>
</linearGradient>
<linearGradient id="S" y2="150.38" xlink:href="#c" gradientUnits="userSpaceOnUse" x2="133" gradientTransform="scale(-1.39197 1.39197) rotate(-45 -28.292 379.342)" y1="150.38" x1="123"/>
<linearGradient id="R" y2="143.36" xlink:href="#c" gradientUnits="userSpaceOnUse" x2="130.88" gradientTransform="rotate(45 217.552 97.892) scale(1.39197)" y1="134.36" x1="128"/>
<linearGradient id="V" y2="150.38" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="167.73" gradientTransform="rotate(-135 332.789 -39.72) scale(2.7839 -2.4637)" x2="133" x1="108"/>
<linearGradient id="W" y2="137.2" gradientUnits="userSpaceOnUse" x2="179.96" gradientTransform="translate(0 -10)" y1="29.815" x1="126.64">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="Z" y2="137.2" gradientUnits="userSpaceOnUse" y1="29.815" gradientTransform="matrix(-.50295 .1984 .1972 -.43254 276.16 123.42)" x2="179.96" x1="126.64">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="G" y2="-213.25" gradientUnits="userSpaceOnUse" x2="9" gradientTransform="translate(0 192)" y1="72.064" x1="210.17">
<stop offset="0"/>
<stop stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="h" y2="4.302" gradientUnits="userSpaceOnUse" x2="122" y1="245.45" x1="122">
<stop offset="0"/>
<stop stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="e" y2="-213.25" gradientUnits="userSpaceOnUse" x2="9" gradientTransform="matrix(1 -.08088 0 1 0 203.06)" y1="72.064" x1="210.17">
<stop offset="0"/>
<stop stop-opacity="0" offset="1"/>
</linearGradient>
<clipPath id="j" clipPathUnits="userSpaceOnUse">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
</clipPath>
<clipPath id="k" clipPathUnits="userSpaceOnUse">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
</clipPath>
<clipPath id="i" clipPathUnits="userSpaceOnUse">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
</clipPath>
<clipPath id="D" clipPathUnits="userSpaceOnUse">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
</clipPath>
<clipPath id="E" clipPathUnits="userSpaceOnUse">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
</clipPath>
<clipPath id="F" clipPathUnits="userSpaceOnUse">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
</clipPath>
<clipPath id="C" clipPathUnits="userSpaceOnUse">
<path d="M123.62 141.06c20.832 20.243 54.607 19.289 75.439-2.132s20.832-55.196 0-75.439-54.607-19.289-75.439 2.132-20.832 55.196 0 75.439z" fill="url(#d)"/>
</clipPath>
<clipPath id="H" clipPathUnits="userSpaceOnUse">
<path opacity=".039" d="M9 22.578s7.5 12.393 11.75 26.8C25 63.784 27 74.373 27 74.373s-5.5 13.195-8.25 25.417C16 112.01 13 123.25 13 123.25s5.75 15.535 9.25 29.252S26 176.449 26 176.449s-4 10.574-7.5 24.857-5 31.154-5 31.154 9.25-2.748 28.5-1.055 32.25 3.392 32.25 3.392 12.75-3.782 24-8.191 16.25-7.815 16.25-7.815 5.5.055 22.5 4.43c17 4.376 29.25 6.135 29.25 6.135s13-3.802 26-7.853 26.5-10.143 26.5-10.143-.75-4.94 4.25-24.844 8.75-28.708 8.75-28.708-.514-4.455-3.75-19.447c-1.75-8.108-2.25-4.068-2.25-4.068s16.264-26.155 16.5-40.334c.25-15.02-7.25-36.413-7.25-36.413s-4.75-14.866-7.5-29.143c-2.75-14.278-8.25-25.083-8.25-25.083S195 14.281 187.5 16.388c-7.5 2.107-21 6.948-21 6.948s-9.75-3.461-22-6.72c-12.25-3.26-29.75-3.094-29.75-3.094s-3.25 3.763-22 9.78c-18.75 6.016-27.5 7.973-27.5 7.973s-18.5-7.504-31.5-8.952-24-.06-24.75.252z" fill="url(#e)"/>
</clipPath>
<radialGradient id="K" gradientUnits="userSpaceOnUse" cy="86" cx="128" gradientTransform="matrix(1.0213 0 0 -1.0213 -212.72 173.83)" r="47">
<stop stop-color="#d0e9f2" stop-opacity="0" offset="0"/>
<stop stop-color="#d0e9f2" stop-opacity="0" offset=".839"/>
<stop stop-color="#d0e9f2" stop-opacity=".282" offset=".943"/>
<stop stop-color="#d0e9f2" offset="1"/>
</radialGradient>
<radialGradient id="Q" gradientUnits="userSpaceOnUse" cy="86" cx="128" gradientTransform="scale(1.4216 -1.4216) rotate(-45 -51.091 19.464)" r="47">
<stop stop-color="#d0e9f2" stop-opacity="0" offset="0"/>
<stop stop-color="#d0e9f2" stop-opacity="0" offset=".839"/>
<stop stop-color="#d0e9f2" stop-opacity=".282" offset=".943"/>
<stop stop-color="#d0e9f2" offset="1"/>
</radialGradient>
<radialGradient id="Y" gradientUnits="userSpaceOnUse" cy="72.588" cx="159.61" gradientTransform="matrix(1.2125 0 0 .83915 -46.348 24.528)" r="38.417">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity=".93" offset=".5"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</radialGradient>
<radialGradient id="X" gradientUnits="userSpaceOnUse" cx="159.61" cy="72.588" r="38.417" gradientTransform="matrix(1.2125 0 0 .83915 -45.92 25.814)">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</radialGradient>
<radialGradient id="d" gradientUnits="userSpaceOnUse" cy="86" cx="128" gradientTransform="matrix(.80254 .77986 .80254 -.82522 -10.402 73.423)" r="47">
<stop stop-color="#d0e9f2" stop-opacity="0" offset="0"/>
<stop stop-color="#d0e9f2" stop-opacity="0" offset=".839"/>
<stop stop-color="#d0e9f2" stop-opacity=".282" offset=".943"/>
<stop stop-color="#d0e9f2" offset="1"/>
</radialGradient>
<filter id="I">
<feGaussianBlur stdDeviation="4.287"/>
</filter>
<filter id="f">
<feGaussianBlur stdDeviation="6.311"/>
</filter>
<filter id="g">
<feGaussianBlur stdDeviation="4.457"/>
</filter>
</defs>
<path opacity=".7" d="M174.28 35.875c-17.6 0-35.19 6.639-48.72 19.906-.16.164-.33.303-.5.469-22.05 22.056-26.206 55.25-12.47 81.5l-2.28 1.63c2.25 4.23 4.94 8.3 8.1 12.12l-13.32 13.34c-1.34-.58-2.78-1.03-4.56-1.37l-2.842 2.84c-1.11-.48-2.139-.81-3.063-.87L30.5 229.56c-.037 1.1.226 2.19.656 3.28l-.531.54-1.094 1.09c.716 3.74 2.011 6.82 5.469 10.28s6.42 4.63 10.281 5.47l1.094-1.1.719-.71c1.088.46 2.13.78 3.094.84l64.122-64.13c-.24-.97-.62-1.93-1.03-2.9l3-3c-.25-1.73-.68-3.2-1.31-4.6l13.28-13.28c3.82 3.16 7.89 5.85 12.13 8.1l1.62-2.28c26.25 13.74 59.44 9.58 81.5-12.47.17-.17.31-.34.47-.5 26.69-27.22 26.55-70.923-.47-97.94-13.59-13.59-31.41-20.375-49.22-20.375zm-.5 3.281c.11.002.21-.002.31 0a67.89 67.89 0 0 1 5.72.375c.1.01.19.021.28.031l.29.032c.14.017.29.044.43.062.9.11 1.8.228 2.69.375.62.102 1.23.224 1.84.344.35.066.69.116 1.04.187.72.153 1.43.324 2.15.5.31.077.63.138.94.219.07.019.14.043.22.063.95.253 1.89.516 2.84.812.07.023.15.04.22.063.95.3 1.91.625 2.84.969.06.019.11.042.16.062.95.352 1.9.728 2.84 1.125.06.022.11.041.16.063.94.4 1.88.836 2.81 1.281l.13.062c.93.451 1.86.91 2.78 1.407.03.017.06.044.09.062.93.503 1.84 1.013 2.75 1.562.03.02.06.044.1.063.9.551 1.8 1.122 2.68 1.719.55.369 1.09.769 1.63 1.156.36.261.73.512 1.09.781.02.014.05.018.06.031.88.656 1.75 1.36 2.6 2.063a70.875 70.875 0 0 1 5.06 4.594c1.62 1.62 3.14 3.3 4.56 5.031.01.011.03.02.03.031.71.855 1.41 1.715 2.07 2.594.28.379.54.773.81 1.156.37.517.74 1.038 1.09 1.563.02.024.05.038.07.062.59.89 1.16 1.778 1.71 2.688.02.032.05.061.07.094a75.45 75.45 0 0 1 1.56 2.75c.02.032.04.06.06.093.5.921.96 1.844 1.41 2.781l.06.126c.45.929.88 1.868 1.28 2.812l.06.156c.4.936.78 1.895 1.13 2.844l.06.156c.35.949.67 1.885.97 2.844l.06.219c.3.941.56 1.894.82 2.843l.06.219c.08.31.14.627.22.938.17.72.35 1.432.5 2.156.07.342.12.688.18 1.031.12.617.25 1.225.35 1.844.15.893.26 1.791.37 2.688.02.145.05.291.07.437l.03.281.03.282c.1.952.19 1.912.25 2.872.06.94.11 1.9.12 2.85.01.1 0 .2 0 .31.02 1.05.01 2.1-.03 3.15-.03.94-.08 1.86-.15 2.79-.01.12-.02.25-.04.37-.07.92-.16 1.84-.28 2.75-.12.96-.27 1.93-.43 2.88-.02.08-.02.16-.04.25-.02.12-.03.25-.06.37-.16.91-.36 1.82-.56 2.72-.03.13-.06.27-.09.41a86.1 86.1 0 0 1-.69 2.65c-.17.59-.35 1.17-.53 1.75-.18.57-.34 1.13-.53 1.69-.3.86-.61 1.71-.94 2.56-.06.16-.13.32-.19.47-.04.11-.08.23-.12.34-.36.88-.73 1.73-1.13 2.6-.38.82-.77 1.63-1.19 2.44-.08.15-.16.31-.25.46-.41.8-.83 1.6-1.28 2.38-.09.17-.18.34-.28.5-.2.34-.42.66-.62 1-.37.59-.75 1.17-1.13 1.75 0 .01.01.02 0 .03-.49.75-1 1.49-1.53 2.22-.46.64-.95 1.25-1.44 1.87-.19.26-.36.53-.56.79l-.41.5c-.16.2-.33.39-.5.59-.55.67-1.13 1.32-1.72 1.97-.6.67-1.23 1.35-1.87 2-.16.16-.31.34-.47.5-21.13 21.13-52.26 24.49-77.19 11.15l.13-.18c-3.71-1.9-7.26-4.14-10.63-6.78-.54-.43-1.06-.87-1.59-1.32-.51-.43-1.03-.86-1.53-1.31-.09-.08-.19-.16-.28-.25a86.97 86.97 0 0 1-2.41-2.31c-.79-.8-1.56-1.59-2.31-2.41-.09-.09-.17-.19-.25-.28-.45-.5-.88-1.02-1.31-1.53-.45-.53-.89-1.05-1.32-1.59a65.831 65.831 0 0 1-6.78-10.63l-.19.13c-13.33-24.93-9.97-56.061 11.16-77.191.16-.163.34-.308.5-.469.65-.64 1.33-1.269 2-1.875.65-.588 1.3-1.162 1.97-1.719.27-.227.54-.465.81-.687.35-.278.72-.512 1.06-.782.63-.483 1.24-.98 1.88-1.437.74-.53 1.5-1.036 2.25-1.531.58-.383 1.16-.764 1.75-1.125.45-.274.89-.551 1.34-.813.69-.395 1.37-.757 2.07-1.125a61.343 61.343 0 0 1 3.37-1.687c.87-.397 1.72-.769 2.59-1.125.16-.062.32-.096.47-.157.97-.384 1.93-.758 2.91-1.093.56-.192 1.12-.356 1.69-.532.58-.181 1.16-.367 1.75-.531.88-.246 1.77-.48 2.65-.687.07-.017.15-.015.22-.032.42-.095.84-.193 1.25-.281.67-.141 1.36-.287 2.03-.406.09-.015.17-.017.25-.031.95-.164 1.92-.317 2.88-.438a65.183 65.183 0 0 1 9.06-.5z" transform="matrix(1 .06864 0 .8487 0 26.913)" filter="url(#f)" fill="#2d3335"/>
<path opacity=".5" d="M9 17.25s7.5 11 11.75 25.75S27 68.5 27 68.5s-5.5 12.75-8.25 24.75-5.75 25-5.75 25 5.75 16 9.25 30S26 170.5 26 170.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-2.5-3.75-17.75S218 121 218 121s1.75-12.5 6.75-25.75S235 68.5 235 68.5s-4.75-15.25-7.5-29.75S219.25 15 219.25 15 195 24 187.5 25.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z" transform="matrix(1 0 0 .84657 0 27.66)" filter="url(#g)" fill="url(#h)"/>
<path fill="#ceeeab" d="M9 2.25s7.5 13 11.75 27.75S27 55.5 27 55.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 157.5 26 157.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 106 218 106s1.75-10.5 6.75-23.75S235 55.5 235 55.5s-4.75-15.25-7.5-29.75S219.25 0 219.25 0 195 9 187.5 10.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5S9.75 2 9 2.25z"/>
<g transform="translate(0 -10)" clip-path="url(#i)">
<path fill="#a6dd8b" d="m110.75 5.5-4.25 7c-.75 9-2.25 13.75-8.5 16.5s-12.25 3.5-11 7.5 13 9.25 14.25 13 8.25 1.75 11 7 2 15.25-3.75 17.25-17.5.5-20.25 9.5-4.75 10.5-9 12.75-7.25 10.5-3.5 16.5 12.25-1.25 15-6.5S98 97.25 98 97.25h23.75l59-1.75 3.25-3.75s3.25 4 2.75 8.75-3.75 14.75.25 17.25 19.5-2 24-7-4.75-28.25-10.5-29.5-18.5-1.75-17-7 11.75 4.5 17.25 3.25 16.75-21 12-25.25-24.25-5.25-25.75-8 21-8 22.25-11-2.5-7.25-6.25-8.5S189 41 182 40.5s-20 5.75-17.25 11.5-6 11-14.5 6.5-24.75-13-21.75-24S150 15.75 150 15.75L110.75 5.5zM97.188 112.72c-1.595.15-4.469 4.69-4.938 7.03-.5 2.5-3.25 6.75-3.5 12.25s4.75 6.75 8.75 6.5 2.75-6.75 2-15c-.25 0-.5-10.25-2-10.75-.094-.03-.206-.04-.312-.03zm92.722 51.66c-6.34.29-6.63 7.46-7.41 8.87-1.25 2.25 2.25 13.75 2 18s-4.75 5.25-9.5 9.75.5 16 11.25 31l44.75 1.25-1-35s4.75-4.25-20.75-24.25c-9.56-7.5-15.54-9.8-19.34-9.62zM39.94 180.91c-4.03-.11-8.282 5.71-9.688 7.59-1.5 2-6.25 5-17 9.5l-14.5 34.25 53 4.25s1.75-11-4.25-15.75-17.25-5.75-18.25-13.25 9.25-10 13.75-14.25 2-11.25-2.25-12.25a3.728 3.728 0 0 0-.812-.09z"/>
</g>
<path fill="#aac3e7" d="M158.53 75.344c-4.76-.015-9.03.968-11.53 3.156-8 7-35 .75-48.5 7s-13.25 38-14.75 44.5-17.5 20.75-20 23.5-13.25 7.25-19.5 8.5-12.75 7.25-15.5 11c-2.021 2.76-7.406 6.45-10.125 8.22-.046.18-.079.35-.125.53-.134.53-.247 1.08-.375 1.63 4.98-1.61 11.18-8.18 16.625-13.63 6.25-6.25 20-7.75 27.75-11.5S76.75 138.5 89 134.5s21.25 11.75 24.25 18.5 1.75 12.75 3.75 17 11 11.75 11.5 13.5-5 6.5-6.25 8.5-10.5 7-11.75 8.75c-.97 1.35-1.92 6.77-2.31 9.22.41-.17.75-.31 1.12-.47.49-2.53 1.52-7.08 2.94-8.5 2-2 11.25-4.5 12.5-8.5s7-6.5 7-6.5 2.75 4 16 14c8.83 6.67 12.76 15.53 14.41 20.72.67.17 1.53.41 2.03.53-1.22-4.32-4.84-16.24-8.94-20.75-5-5.5-18.5-10.75-22.75-22S108 144.25 115 138.25s16.5-4 28.5 7.5 46.25 5.75 57.75 3.75c9.95-1.73 20.83 14.88 23.91 26.03.59-2.09 1.12-4 1.65-5.75-1.74-1.92-3.69-4.62-5.31-8.28-3.75-8.5-12-13.25-12-13.25s8.75-5 14.75-7.75c1.62-.74 3.01-1.68 4.19-2.66-.16-.77-.26-1.25-.44-2.09l-.09-.44c-4.77 5.56-19.24 9.62-21.66 10.94-2.75 1.5-18.25 3-35.75 4.5s-26.75-7.5-34.25-14.75-13-36-3-38 20 13.75 30 17 21.5-15.75 19.75-27c-1.2-7.734-14-12.625-24.47-12.656zm-53.34 13.281c1.27.001 5.86 1.594 6.56 2.375 2.25 2.5 2 7.25 0 8s-12.75 8-10.5 14.25 1.75 18-3.5 18-8 0-10-2.5-2-12 0-19.75 3.5-15 8-18.25c3.094-2.234 6.63-2.128 9.44-2.125zm13.59 73.095c.2.01 1.91 3.51 4.72 7.03 3 3.75 3.25 8.25 3.25 8.25s-4.25-4.75-6-8-2-7.25-2-7.25c0-.03.02-.03.03-.03z"/>
<path opacity=".387" stroke-linejoin="round" d="M122.75 62.25c6.25.5 12.25-2.25 10.75-6.5s-12.5-7.25-13.25-3 2 10.5 2.5 9.5zM108.75 46.75c-8.5-5.5-8.5-7.25-7-8s10.25 5 12.75 8-2.25 2-5.75 0z" stroke="#6d7f42" stroke-linecap="round" fill="none"/>
<g clip-path="url(#j)" transform="translate(0 -10)" stroke="#6d7f42" fill="none">
<path opacity=".387" stroke-linejoin="round" d="M15.75 151.25c33-2.5 38.25-3.5 36.25-10.25s-8.75-23.5-23-22.5-22.75-6.75-22.75-6.75" stroke-linecap="round"/>
<path opacity=".387" d="M17.5 177.25c18.25-10 28.75-8.5 35-11.5s13.25-5.75 15.75-9.5 5.75-11.5 5.75-18-3.5-36 3.25-43.5 17.25-26 26-23.25 12.25 9.75 22 9.5S147 77 149.5 68.75s-15.75-16-16-23 5.5-14.25 20.25-17 23.5-11.5 23.5-11.5"/>
<path opacity=".387" stroke-linejoin="round" d="M14.5 161c15.25-1.5 22.25 3.5 31.5 1.75s13.25-3 17.75-5.75 6-3.75 6.25-6.5 1-12.5-3-22S54.5 97 58.75 89.75 64 68.25 74.5 65.25 95.5 59 102.25 64s12.75 14.25 20 14.5S134 77 136.75 74s4.5-10.25 1.75-13.75-15.5-5.5-17.25-9.5-10.75-17-.25-26.75 36.25-8.25 36.25-8.25" stroke-linecap="round"/>
<path opacity=".387" stroke-linejoin="round" d="M21.25 159c13.75 1 21.5 4.25 33.5-.5s13.5-5.25 13.75-8.75-.25-15.75-3-20-13-13.75-15.75-24.25.25-41 8.5-48.25 32-7.25 37.5-10.5 5.25-16.75 13-24.5 27-12.25 27-12.25M190 18.5c11.75 5 39 9.75 47.25 40.25M9.75 214c19-12.5 36-1.25 49-4s38.25-37.25 46-37 5.5-11.75 8.5-13.25 5.5 6 11.75 8 24.5-2.75 23.5-5.75-7-7.75-5.5-8.5 8.5 5 12.75 3.5 43.75-1 48.5-13.5 21.5-13.5 21.5-13.5M77.5 233.5c13.5-11.5 23.25-25 28.5-27s12.75-3.75 15.75-8 2.5-10.75 5.25-12 3.75 5.75 8.75 6 36.75-5 42.75-11.75 32.25-12.5 32.5-14.5-1-4.75.25-5.5 24.75-6 24.75-6M168.75 236c9-13.75 20.5-41 29.5-42s10.25-2.5 12.5-4.25 17 2.25 17 2.25" stroke-linecap="round"/>
</g>
<g clip-path="url(#k)" transform="translate(0 -10)" stroke="#d38484" stroke-linecap="round" stroke-width="2" fill="none">
<path d="m57.75 20-8.5 28.25 18 6.25L75 90.75 54 113l9 10.5L51 135l.5 4.5L71.75 164l14-6.75 20.5 18.5L95.75 204l10.25 8.5-2.75 13"/>
<path d="m105.75 212.25 12.5-27.75 11-7 27.5 15.75 20.5-3.75-.25-15.75-10.25-6 12.75-26.25 5.75-3.75 38.75-10"/>
</g>
<g opacity=".504">
<path d="M219.25 330s-24.25 9-31.75 10.5-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75c0 0 7.5 13 11.75 27.75S27 385.5 27 385.5l51.469 5.125 48.971-4 52.25 12.031c10.38-4.418 52.11-4.9 55.31-13.156 0 0-4.75-15.25-7.5-29.75S219.25 330 219.25 330z" fill="#b1e479"/>
<path d="M27 385.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23l50.969 16.37 53.721-15.74 52.25 17.53c16.36-4.26 32.39-9.12 48.06-15.41 0 0 1.75-10.5 6.75-23.75S235 385.5 235 385.5c-3.2 8.256-44.93 8.738-55.31 13.156l-52.25-12.031-47.971 4L27 385.5z" fill="#87d531"/>
<path d="M231.75 485.5c-17.9 7.67-35.94 14.9-54.31 21.41l-52.25-18.53-50.221 9L26 487.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28z" fill="#ceeeab"/>
<path d="M13 433.25s5.75 16 9.25 30S26 487.5 26 487.5l48.969 9.88 50.221-9 52.25 18.53c18.37-6.51 36.41-13.74 54.31-21.41 0 0-.5-4.5-3.75-19.75S218 436 218 436c-15.67 6.29-31.7 11.15-48.06 15.41l-52.25-17.53-53.721 15.74L13 433.25z" fill="#b9e787"/>
</g>
<g opacity=".522">
<path d="M13.719 332.031c-2.729-.007-4.438.125-4.719.219 0 0 7.5 13 11.75 27.75S27 385.5 27 385.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 487.5 26 487.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6l1.5-53.25L63.5 450l16-59.75c-2.22-15.48-9.068-30.046-14.25-44.75 0 0-18.5-9-31.5-11.5-8.125-1.563-15.483-1.956-20.031-1.969z" fill="#83d32b"/>
<path d="M114.75 331.75s-3.25 3.5-22 8-27.5 5.75-27.5 5.75c5.182 14.704 12.03 29.27 14.25 44.75L63.5 450l12.25 46.5-1.5 53.25s12.75-2.75 24-6.25 16.25-6.5 16.25-6.5l11.25-48.5-8.25-54.25 11.75-45c-3.44-9.727-8.06-56.93-14.5-57.5z" fill="#b1e479"/>
<path d="M219.25 330s-24.25 9-31.75 10.5-21 5.25-21 5.25c4.92-1.358 11.44 45.392 14.25 52.25l-11.25 53.25 9.5 55-12.75 45.5s13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 436 218 436s1.75-10.5 6.75-23.75S235 385.5 235 385.5s-4.75-15.25-7.5-29.75-8.25-25.75-8.25-25.75z" fill="#a4df62"/>
<path d="M114.75 331.75c6.44.57 11.06 47.773 14.5 57.5l-11.75 45 8.25 53.25L114.5 537s5.5.5 22.5 6.25 29.25 8.5 29.25 8.5l12.75-45.5-9.5-55L180.75 398c-2.81-6.858-9.33-53.608-14.25-52.25 0 0-9.75-4.25-22-8.5s-29.75-5.5-29.75-5.5z" fill="#ceeeab"/>
</g>
<path opacity=".039" d="M9-189.75s7.5 13 11.75 27.75S27-136.5 27-136.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26-34.5 26-34.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6S87 25 98.25 21.5 114.5 15 114.5 15s5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8S218 11 223-8.5s8.75-28 8.75-28-.5-4.5-3.75-19.75S218-86 218-86s1.75-10.5 6.75-23.75S235-136.5 235-136.5s-4.75-15.25-7.5-29.75-8.25-25.75-8.25-25.75-24.25 9-31.75 10.5-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z" fill="url(#l)" transform="translate(0 192)"/>
<g opacity=".27">
<path fill="url(#m)" d="M219.25-182s-24.25 9-31.75 10.5-21 5.25-21 5.25c4.92-1.36 11.44 45.39 14.25 52.25l-.06.28c11.81-4.07 51.21-4.77 54.31-12.78 0 0-4.75-15.25-7.5-29.75S219.25-182 219.25-182z" transform="translate(0 182)"/>
<path fill="url(#n)" d="M27-126.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23l50.625 16.281 15.781-58.911L27-126.5z" transform="translate(0 182)"/>
<path fill="url(#o)" d="M26-24.5S22-14.25 18.5-.25s-5 30.75-5 30.75.147-.045.438-.094c2.033-.338 11.218-1.5 28.062 1.344 19.25 3.25 32.25 6 32.25 6l1.469-52.5-.75.125L26-24.5z" transform="translate(0 182)"/>
<path fill="url(#p)" d="M114.75-180.25s-3.25 3.5-22 8-27.5 5.75-27.5 5.75c5.182 14.7 12.03 29.27 14.25 44.75l-.094.31 48.034-3.94 1.12.26c-3.36-13.33-7.79-54.6-13.81-55.13z" transform="translate(0 182)"/>
<path fill="url(#q)" d="M117.59-78.094 63.969-62.375l-.344-.094L63.5-62l12.25 46.5-.031.75 49.471-8.875.53.187.03-.062-8.25-54.25.09-.344z" transform="translate(0 182)"/>
<path fill="url(#r)" d="M128.56-125.12c.24.92.47 1.74.69 2.37l-11.66 44.656.1-.031L169.5-60.75l11.19-52.97c-.36.12-.7.25-1 .38l-51.13-11.78z" transform="translate(0 182)"/>
<path fill="url(#s)" d="M125.53-23.5 114.5 25s5.5.5 22.5 6.25c2.12.719 4.16 1.406 6.12 2.031 1.97.626 3.86 1.186 5.63 1.719 10.62 3.199 17.5 4.75 17.5 4.75l12.72-45.406c-.51.182-1.02.38-1.53.562L125.53-23.5z" transform="translate(0 182)"/>
<path fill="url(#t)" d="M218-76c-15.67 6.288-31.7 11.15-48.06 15.406l-.44-.156 9.5 55-.03.094c17.84-6.36 35.38-13.387 52.78-20.844 0 0-.5-4.5-3.75-19.75S218-76 218-76z" transform="translate(0 182)"/>
</g>
<g opacity=".043">
<path fill="url(#u)" d="M13.719-179.97c-1.106 0-1.972.03-2.719.06-.445.02-.855.04-1.156.07-.03 0-.066-.01-.094 0-.398.03-.656.06-.75.09 0 0 7.5 13 11.75 27.75S27-126.5 27-126.5l51.469 5.12.937-.06.094-.31c-2.22-15.48-9.068-30.05-14.25-44.75 0 0-18.5-9-31.5-11.5-1.018-.2-2.01-.37-3-.53-.596-.1-1.167-.2-1.75-.28-.237-.04-.453-.06-.688-.1a98.772 98.772 0 0 0-3.812-.46c-.735-.08-1.395-.13-2.094-.19-.685-.06-1.353-.11-2-.16-.992-.07-1.89-.12-2.781-.15-1.456-.07-2.769-.1-3.906-.1z" transform="translate(0 182)"/>
<path fill="url(#v)" d="M13-78.75s5.75 16 9.25 30S26-24.5 26-24.5l48.969 9.875.75-.125.031-.75L63.5-62l.125-.469L13-78.75z" transform="translate(0 182)"/>
<path fill="url(#w)" d="m127.44-125.38-47.971 4h-.063L63.625-62.469l.344.094 53.621-15.719 11.66-44.656c-.22-.63-.45-1.45-.69-2.37l-1.12-.26z" transform="translate(0 182)"/>
<path fill="url(#x)" d="M125.19-23.625 75.719-14.75l-1.469 52.5S87 35 98.25 31.5 114.5 25 114.5 25l11.22-48.438-.53-.187z" transform="translate(0 182)"/>
<path fill="url(#y)" d="M114.75-180.25c6.02.53 10.45 41.8 13.81 55.13l51.13 11.78c.3-.13.64-.26 1-.38l.06-.28c-2.81-6.86-9.33-53.61-14.25-52.25 0 0-.61-.25-1.72-.72-3.32-1.39-11.09-4.59-20.28-7.78-12.25-4.25-29.75-5.5-29.75-5.5z" transform="translate(0 182)"/>
<path fill="url(#z)" d="m117.69-78.125-.1.031-.09.344 8.25 53.25-.22 1 51.91 18.406c.51-.181 1.02-.38 1.53-.562l.03-.094-9.5-55-51.81-17.375z" transform="translate(0 182)"/>
<path fill="url(#A)" d="M231.75-26.5c-17.4 7.457-34.94 14.484-52.78 20.844L166.25 39.75s13-2.75 26-5.75 26.5-8 26.5-8S218 21 223 1.5s8.75-28 8.75-28z" transform="translate(0 182)"/>
<path fill="url(#B)" d="M235-126.5c-3.1 8.01-42.5 8.71-54.31 12.78L169.5-60.75l.44.156C186.3-64.85 202.33-69.712 218-76c0 0 1.75-10.5 6.75-23.75S235-126.5 235-126.5z" transform="translate(0 182)"/>
</g>
<g clip-path="url(#C)" transform="matrix(1.2525 .0354 0 1.2525 -27.809 -48.344)">
<path fill="#ceeeab" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z"/>
<g clip-path="url(#D)">
<path fill="#a6dd8b" d="m110.75 5.5-4.25 7c-.75 9-2.25 13.75-8.5 16.5s-12.25 3.5-11 7.5 13 9.25 14.25 13 8.25 1.75 11 7 2 15.25-3.75 17.25-17.5.5-20.25 9.5-4.75 10.5-9 12.75-7.25 10.5-3.5 16.5 12.25-1.25 15-6.5S98 97.25 98 97.25h23.75l59-1.75 3.25-3.75s3.25 4 2.75 8.75-3.75 14.75.25 17.25 19.5-2 24-7-4.75-28.25-10.5-29.5-18.5-1.75-17-7 11.75 4.5 17.25 3.25 16.75-21 12-25.25-24.25-5.25-25.75-8 21-8 22.25-11-2.5-7.25-6.25-8.5S189 41 182 40.5s-20 5.75-17.25 11.5-6 11-14.5 6.5-24.75-13-21.75-24S150 15.75 150 15.75L110.75 5.5zM97.188 112.72c-1.595.15-4.469 4.69-4.938 7.03-.5 2.5-3.25 6.75-3.5 12.25s4.75 6.75 8.75 6.5 2.75-6.75 2-15c-.25 0-.5-10.25-2-10.75-.094-.03-.206-.04-.312-.03zm92.722 51.66c-6.34.29-6.63 7.46-7.41 8.87-1.25 2.25 2.25 13.75 2 18s-4.75 5.25-9.5 9.75.5 16 11.25 31l44.75 1.25-1-35s4.75-4.25-20.75-24.25c-9.56-7.5-15.54-9.8-19.34-9.62zM39.94 180.91c-4.03-.11-8.282 5.71-9.688 7.59-1.5 2-6.25 5-17 9.5l-14.5 34.25 53 4.25s1.75-11-4.25-15.75-17.25-5.75-18.25-13.25 9.25-10 13.75-14.25 2-11.25-2.25-12.25a3.728 3.728 0 0 0-.812-.09z"/>
</g>
<path fill="#aac3e7" d="M158.53 85.344c-4.76-.015-9.03.968-11.53 3.156-8 7-35 .75-48.5 7s-13.25 38-14.75 44.5-17.5 20.75-20 23.5-13.25 7.25-19.5 8.5-12.75 7.25-15.5 11c-2.021 2.76-7.406 6.45-10.125 8.22-.046.18-.079.35-.125.53-.134.53-.247 1.08-.375 1.63 4.98-1.61 11.18-8.18 16.625-13.63 6.25-6.25 20-7.75 27.75-11.5S76.75 148.5 89 144.5s21.25 11.75 24.25 18.5 1.75 12.75 3.75 17 11 11.75 11.5 13.5-5 6.5-6.25 8.5-10.5 7-11.75 8.75c-.97 1.35-1.92 6.77-2.31 9.22.41-.17.75-.31 1.12-.47.49-2.53 1.52-7.08 2.94-8.5 2-2 11.25-4.5 12.5-8.5s7-6.5 7-6.5 2.75 4 16 14c8.83 6.67 12.76 15.53 14.41 20.72.67.17 1.53.41 2.03.53-1.22-4.32-4.84-16.24-8.94-20.75-5-5.5-18.5-10.75-22.75-22S108 154.25 115 148.25s16.5-4 28.5 7.5 46.25 5.75 57.75 3.75c9.95-1.73 20.83 14.88 23.91 26.03.59-2.09 1.12-4 1.65-5.75-1.74-1.92-3.69-4.62-5.31-8.28-3.75-8.5-12-13.25-12-13.25s8.75-5 14.75-7.75c1.62-.74 3.01-1.68 4.19-2.66-.16-.77-.26-1.25-.44-2.09l-.09-.44c-4.77 5.56-19.24 9.62-21.66 10.94-2.75 1.5-18.25 3-35.75 4.5s-26.75-7.5-34.25-14.75-13-36-3-38 20 13.75 30 17 21.5-15.75 19.75-27c-1.2-7.734-14-12.625-24.47-12.656zm-53.34 13.281c1.27.001 5.86 1.595 6.56 2.375 2.25 2.5 2 7.25 0 8s-12.75 8-10.5 14.25 1.75 18-3.5 18-8 0-10-2.5-2-12 0-19.75 3.5-15 8-18.25c3.094-2.234 6.63-2.128 9.44-2.125zm13.59 73.095c.2.01 1.91 3.51 4.72 7.03 3 3.75 3.25 8.25 3.25 8.25s-4.25-4.75-6-8-2-7.25-2-7.25c0-.03.02-.03.03-.03z"/>
<path opacity=".387" stroke-linejoin="round" d="M122.75 72.25c6.25.5 12.25-2.25 10.75-6.5s-12.5-7.25-13.25-3 2 10.5 2.5 9.5zM108.75 56.75c-8.5-5.5-8.5-7.25-7-8s10.25 5 12.75 8-2.25 2-5.75 0z" stroke="#6d7f42" stroke-linecap="round" fill="none"/>
<g stroke="#6d7f42" fill="none" clip-path="url(#E)">
<path opacity=".387" stroke-linejoin="round" d="M15.75 151.25c33-2.5 38.25-3.5 36.25-10.25s-8.75-23.5-23-22.5-22.75-6.75-22.75-6.75" stroke-linecap="round"/>
<path opacity=".387" d="M17.5 177.25c18.25-10 28.75-8.5 35-11.5s13.25-5.75 15.75-9.5 5.75-11.5 5.75-18-3.5-36 3.25-43.5 17.25-26 26-23.25 12.25 9.75 22 9.5S147 77 149.5 68.75s-15.75-16-16-23 5.5-14.25 20.25-17 23.5-11.5 23.5-11.5"/>
<path opacity=".387" stroke-linejoin="round" d="M14.5 161c15.25-1.5 22.25 3.5 31.5 1.75s13.25-3 17.75-5.75 6-3.75 6.25-6.5 1-12.5-3-22S54.5 97 58.75 89.75 64 68.25 74.5 65.25 95.5 59 102.25 64s12.75 14.25 20 14.5S134 77 136.75 74s4.5-10.25 1.75-13.75-15.5-5.5-17.25-9.5-10.75-17-.25-26.75 36.25-8.25 36.25-8.25" stroke-linecap="round"/>
<path opacity=".387" stroke-linejoin="round" d="M21.25 159c13.75 1 21.5 4.25 33.5-.5s13.5-5.25 13.75-8.75-.25-15.75-3-20-13-13.75-15.75-24.25.25-41 8.5-48.25 32-7.25 37.5-10.5 5.25-16.75 13-24.5 27-12.25 27-12.25M190 18.5c11.75 5 39 9.75 47.25 40.25M9.75 214c19-12.5 36-1.25 49-4s38.25-37.25 46-37 5.5-11.75 8.5-13.25 5.5 6 11.75 8 24.5-2.75 23.5-5.75-7-7.75-5.5-8.5 8.5 5 12.75 3.5 43.75-1 48.5-13.5 21.5-13.5 21.5-13.5M77.5 233.5c13.5-11.5 23.25-25 28.5-27s12.75-3.75 15.75-8 2.5-10.75 5.25-12 3.75 5.75 8.75 6 36.75-5 42.75-11.75 32.25-12.5 32.5-14.5-1-4.75.25-5.5 24.75-6 24.75-6M168.75 236c9-13.75 20.5-41 29.5-42s10.25-2.5 12.5-4.25 17 2.25 17 2.25" stroke-linecap="round"/>
</g>
<g clip-path="url(#F)" stroke="#d38484" stroke-linecap="round" stroke-width="2" fill="none">
<path d="m57.75 20-8.5 28.25 18 6.25L75 90.75 54 113l9 10.5L51 135l.5 4.5L71.75 164l14-6.75 20.5 18.5L95.75 204l10.25 8.5-2.75 13"/>
<path d="m105.75 212.25 12.5-27.75 11-7 27.5 15.75 20.5-3.75-.25-15.75-10.25-6 12.75-26.25 5.75-3.75 38.75-10"/>
</g>
<path opacity=".039" d="M9 12.25s7.5 13 11.75 27.75S27 65.5 27 65.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23 5.75 16 9.25 30S26 167.5 26 167.5s-4 10.25-7.5 24.25-5 30.75-5 30.75 9.25-2 28.5 1.25 32.25 6 32.25 6 12.75-2.75 24-6.25 16.25-6.5 16.25-6.5 5.5.5 22.5 6.25 29.25 8.5 29.25 8.5 13-2.75 26-5.75 26.5-8 26.5-8-.75-5 4.25-24.5 8.75-28 8.75-28-.5-4.5-3.75-19.75S218 116 218 116s1.75-10.5 6.75-23.75S235 65.5 235 65.5s-4.75-15.25-7.5-29.75S219.25 10 219.25 10 195 19 187.5 20.5s-21 5.25-21 5.25-9.75-4.25-22-8.5-29.75-5.5-29.75-5.5-3.25 3.5-22 8-27.5 5.75-27.5 5.75-18.5-9-31.5-11.5-24-2-24.75-1.75z" fill="url(#G)"/>
<g opacity=".27">
<path fill="url(#m)" d="M219.25-182s-24.25 9-31.75 10.5-21 5.25-21 5.25c4.92-1.36 11.44 45.39 14.25 52.25l-.06.28c11.81-4.07 51.21-4.77 54.31-12.78 0 0-4.75-15.25-7.5-29.75S219.25-182 219.25-182z" transform="translate(0 192)"/>
<path fill="url(#n)" d="M27-126.5s-5.5 12.75-8.25 24.75-5.75 23-5.75 23l50.625 16.281 15.781-58.911L27-126.5z" transform="translate(0 192)"/>
<path fill="url(#o)" d="M26-24.5S22-14.25 18.5-.25s-5 30.75-5 30.75.147-.045.438-.094c2.033-.338 11.218-1.5 28.062 1.344 19.25 3.25 32.25 6 32.25 6l1.469-52.5-.75.125L26-24.5z" transform="translate(0 192)"/>
<path fill="url(#p)" d="M114.75-180.25s-3.25 3.5-22 8-27.5 5.75-27.5 5.75c5.182 14.7 12.03 29.27 14.25 44.75l-.094.31 48.034-3.94 1.12.26c-3.36-13.33-7.79-54.6-13.81-55.13z" transform="translate(0 192)"/>
<path fill="url(#q)" d="M117.59-78.094 63.969-62.375l-.344-.094L63.5-62l12.25 46.5-.031.75 49.471-8.875.53.187.03-.062-8.25-54.25.09-.344z" transform="translate(0 192)"/>
<path fill="url(#r)" d="M128.56-125.12c.24.92.47 1.74.69 2.37l-11.66 44.656.1-.031L169.5-60.75l11.19-52.97c-.36.12-.7.25-1 .38l-51.13-11.78z" transform="translate(0 192)"/>
<path fill="url(#s)" d="M125.53-23.5 114.5 25s5.5.5 22.5 6.25c2.12.719 4.16 1.406 6.12 2.031 1.97.626 3.86 1.186 5.63 1.719 10.62 3.199 17.5 4.75 17.5 4.75l12.72-45.406c-.51.182-1.02.38-1.53.562L125.53-23.5z" transform="translate(0 192)"/>
<path fill="url(#t)" d="M218-76c-15.67 6.288-31.7 11.15-48.06 15.406l-.44-.156 9.5 55-.03.094c17.84-6.36 35.38-13.387 52.78-20.844 0 0-.5-4.5-3.75-19.75S218-76 218-76z" transform="translate(0 192)"/>
</g>
<g opacity=".043">
<path fill="url(#u)" d="M13.719-179.97c-1.106 0-1.972.03-2.719.06-.445.02-.855.04-1.156.07-.03 0-.066-.01-.094 0-.398.03-.656.06-.75.09 0 0 7.5 13 11.75 27.75S27-126.5 27-126.5l51.469 5.12.937-.06.094-.31c-2.22-15.48-9.068-30.05-14.25-44.75 0 0-18.5-9-31.5-11.5-1.018-.2-2.01-.37-3-.53-.596-.1-1.167-.2-1.75-.28-.237-.04-.453-.06-.688-.1a98.772 98.772 0 0 0-3.812-.46c-.735-.08-1.395-.13-2.094-.19-.685-.06-1.353-.11-2-.16-.992-.07-1.89-.12-2.781-.15-1.456-.07-2.769-.1-3.906-.1z" transform="translate(0 192)"/>
<path fill="url(#v)" d="M13-78.75s5.75 16 9.25 30S26-24.5 26-24.5l48.969 9.875.75-.125.031-.75L63.5-62l.125-.469L13-78.75z" transform="translate(0 192)"/>
<path fill="url(#w)" d="m127.44-125.38-47.971 4h-.063L63.625-62.469l.344.094 53.621-15.719 11.66-44.656c-.22-.63-.45-1.45-.69-2.37l-1.12-.26z" transform="translate(0 192)"/>
<path fill="url(#x)" d="M125.19-23.625 75.719-14.75l-1.469 52.5S87 35 98.25 31.5 114.5 25 114.5 25l11.22-48.438-.53-.187z" transform="translate(0 192)"/>
<path fill="url(#y)" d="M114.75-180.25c6.02.53 10.45 41.8 13.81 55.13l51.13 11.78c.3-.13.64-.26 1-.38l.06-.28c-2.81-6.86-9.33-53.61-14.25-52.25 0 0-.61-.25-1.72-.72-3.32-1.39-11.09-4.59-20.28-7.78-12.25-4.25-29.75-5.5-29.75-5.5z" transform="translate(0 192)"/>
<path fill="url(#z)" d="m117.69-78.125-.1.031-.09.344 8.25 53.25-.22 1 51.91 18.406c.51-.181 1.02-.38 1.53-.562l.03-.094-9.5-55-51.81-17.375z" transform="translate(0 192)"/>
<path fill="url(#A)" d="M231.75-26.5c-17.4 7.457-34.94 14.484-52.78 20.844L166.25 39.75s13-2.75 26-5.75 26.5-8 26.5-8S218 21 223 1.5s8.75-28 8.75-28z" transform="translate(0 192)"/>
<path fill="url(#B)" d="M235-126.5c-3.1 8.01-42.5 8.71-54.31 12.78L169.5-60.75l.44.156C186.3-64.85 202.33-69.712 218-76c0 0 1.75-10.5 6.75-23.75S235-126.5 235-126.5z" transform="translate(0 192)"/>
</g>
</g>
<path d="M174.28 35.875c-17.6 0-35.19 6.639-48.72 19.906-.16.164-.33.303-.5.469-22.05 22.056-26.206 55.25-12.47 81.5l-2.28 1.63c2.25 4.23 4.94 8.3 8.1 12.12l-13.32 13.34c-1.34-.58-2.78-1.03-4.56-1.37l-2.842 2.84c-1.11-.48-2.139-.81-3.063-.87L30.5 229.56c-.037 1.1.226 2.19.656 3.28l-.531.54-1.094 1.09c.716 3.74 2.011 6.82 5.469 10.28s6.42 4.63 10.281 5.47l1.094-1.1.719-.71c1.088.46 2.13.78 3.094.84l64.122-64.13c-.24-.97-.62-1.93-1.03-2.9l3-3c-.25-1.73-.68-3.2-1.31-4.6l13.28-13.28c3.82 3.16 7.89 5.85 12.13 8.1l1.62-2.28c26.25 13.74 59.44 9.58 81.5-12.47.17-.17.31-.34.47-.5 26.69-27.22 26.55-70.923-.47-97.94-13.59-13.59-31.41-20.375-49.22-20.375zm-.5 3.281c.11.002.21-.002.31 0a67.89 67.89 0 0 1 5.72.375c.1.01.19.021.28.031l.29.032c.14.017.29.044.43.062.9.11 1.8.228 2.69.375.62.102 1.23.224 1.84.344.35.066.69.116 1.04.187.72.153 1.43.324 2.15.5.31.077.63.138.94.219.07.019.14.043.22.063.95.253 1.89.516 2.84.812.07.023.15.04.22.063.95.3 1.91.625 2.84.969.06.019.11.042.16.062.95.352 1.9.728 2.84 1.125.06.022.11.041.16.063.94.4 1.88.836 2.81 1.281l.13.062c.93.451 1.86.91 2.78 1.407.03.017.06.044.09.062.93.503 1.84 1.013 2.75 1.562.03.02.06.044.1.063.9.551 1.8 1.122 2.68 1.719.55.369 1.09.769 1.63 1.156.36.261.73.512 1.09.781.02.014.05.018.06.031.88.656 1.75 1.36 2.6 2.063a70.875 70.875 0 0 1 5.06 4.594c1.62 1.62 3.14 3.3 4.56 5.031.01.011.03.02.03.031.71.855 1.41 1.715 2.07 2.594.28.379.54.773.81 1.156.37.517.74 1.038 1.09 1.563.02.024.05.038.07.062.59.89 1.16 1.778 1.71 2.688.02.032.05.061.07.094a75.45 75.45 0 0 1 1.56 2.75c.02.032.04.06.06.093.5.921.96 1.844 1.41 2.781l.06.126c.45.929.88 1.868 1.28 2.812l.06.156c.4.936.78 1.895 1.13 2.844l.06.156c.35.949.67 1.885.97 2.844l.06.219c.3.941.56 1.894.82 2.843l.06.219c.08.31.14.627.22.938.17.72.35 1.432.5 2.156.07.342.12.688.18 1.031.12.617.25 1.225.35 1.844.15.893.26 1.791.37 2.688.02.145.05.291.07.437l.03.281.03.282c.1.952.19 1.912.25 2.872.06.94.11 1.9.12 2.85.01.1 0 .2 0 .31.02 1.05.01 2.1-.03 3.15-.03.94-.08 1.86-.15 2.79-.01.12-.02.25-.04.37-.07.92-.16 1.84-.28 2.75-.12.96-.27 1.93-.43 2.88-.02.08-.02.16-.04.25-.02.12-.03.25-.06.37-.16.91-.36 1.82-.56 2.72-.03.13-.06.27-.09.41a86.1 86.1 0 0 1-.69 2.65c-.17.59-.35 1.17-.53 1.75-.18.57-.34 1.13-.53 1.69-.3.86-.61 1.71-.94 2.56-.06.16-.13.32-.19.47-.04.11-.08.23-.12.34-.36.88-.73 1.73-1.13 2.6-.38.82-.77 1.63-1.19 2.44-.08.15-.16.31-.25.46-.41.8-.83 1.6-1.28 2.38-.09.17-.18.34-.28.5-.2.34-.42.66-.62 1-.37.59-.75 1.17-1.13 1.75 0 .01.01.02 0 .03-.49.75-1 1.49-1.53 2.22-.46.64-.95 1.25-1.44 1.87-.19.26-.36.53-.56.79l-.41.5c-.16.2-.33.39-.5.59-.55.67-1.13 1.32-1.72 1.97-.6.67-1.23 1.35-1.87 2-.16.16-.31.34-.47.5-21.13 21.13-52.26 24.49-77.19 11.15l.13-.18c-3.71-1.9-7.26-4.14-10.63-6.78-.54-.43-1.06-.87-1.59-1.32-.51-.43-1.03-.86-1.53-1.31-.09-.08-.19-.16-.28-.25a86.97 86.97 0 0 1-2.41-2.31c-.79-.8-1.56-1.59-2.31-2.41-.09-.09-.17-.19-.25-.28-.45-.5-.88-1.02-1.31-1.53-.45-.53-.89-1.05-1.32-1.59a65.831 65.831 0 0 1-6.78-10.63l-.19.13c-13.33-24.93-9.97-56.061 11.16-77.191.16-.163.34-.308.5-.469.65-.64 1.33-1.269 2-1.875.65-.588 1.3-1.162 1.97-1.719.27-.227.54-.465.81-.687.35-.278.72-.512 1.06-.782.63-.483 1.24-.98 1.88-1.437.74-.53 1.5-1.036 2.25-1.531.58-.383 1.16-.764 1.75-1.125.45-.274.89-.551 1.34-.813.69-.395 1.37-.757 2.07-1.125a61.343 61.343 0 0 1 3.37-1.687c.87-.397 1.72-.769 2.59-1.125.16-.062.32-.096.47-.157.97-.384 1.93-.758 2.91-1.093.56-.192 1.12-.356 1.69-.532.58-.181 1.16-.367 1.75-.531.88-.246 1.77-.48 2.65-.687.07-.017.15-.015.22-.032.42-.095.84-.193 1.25-.281.67-.141 1.36-.287 2.03-.406.09-.015.17-.017.25-.031.95-.164 1.92-.317 2.88-.438a65.183 65.183 0 0 1 9.06-.5z" clip-path="url(#H)" transform="matrix(1 .08088 0 1 0 -21.056)" filter="url(#I)" fill="#2d3335"/>
<path d="m48.01 227.46-4.807 4.55c-5.007-5.31-10.24-10.7-15.08-16.13l4.491-4.07c6.829 6.53 8.872 8.53 15.396 15.65z" fill="url(#J)"/>
<path d="M-95.5 225c28.719 0 52 23.28 52 52s-23.281 52-52 52c-28.72 0-52-23.28-52-52s23.28-52 52-52z" fill-opacity=".476" fill="#d0e9f2"/>
<path d="M-82 136c-27.61 0-50-22.39-50-50 0-27.614 22.39-50 50-50 27.614 0 50 22.386 50 50 0 27.61-22.386 50-50 50zm0-3c26.51 0 48-20.49 48-47s-21.49-47-48-47-48 20.49-48 47 21.49 47 48 47z" fill="#2d3335"/>
<path fill="#9eaaac" d="M-82 136c-27.61 0-50-22.39-50-50v-.5c.27 27.38 22.55 49.5 50 49.5 27.446 0 49.729-22.12 50-49.5.002.168 0 .332 0 .5 0 27.61-22.386 50-50 50zm-48-50.5V85c0-26.51 21.49-47 48-47s48 20.49 48 47c0 .168.002.333 0 .5C-34.27 59.234-55.658 39-82 39c-26.34 0-47.73 20.234-48 46.5z"/>
<path d="M-82 134c26.51 0 48-21.49 48-48s-21.49-48-48-48-48 21.49-48 48 21.49 48 48 48z" fill-opacity=".476" fill="#d0e9f2"/>
<path d="M-82 134c26.51 0 48-21.49 48-48s-21.49-48-48-48-48 21.49-48 48 21.49 48 48 48z" fill="url(#K)"/>
<path fill="url(#L)" d="m-96.438 130.72-.843 5C-92.451 137.2-87.316 138-82 138s10.451-.8 15.281-2.28l-.843-5C-72.115 132.19-76.959 133-82 133s-9.885-.81-14.438-2.28z"/>
<path fill="url(#M)" d="M-82 136.25c1.757 0 3.431.3 5 .81v26.63c-1.569.51-3.243.81-5 .81s-3.431-.3-5-.81v-26.63c1.569-.51 3.243-.81 5-.81z"/>
<path d="M-82 150.5c3.514 0 5.613.65 8 2.44v67.12c-3.137.91-4.486 1.44-8 1.44s-4.863-.53-8-1.44v-67.12c2.637-1.79 4.486-2.44 8-2.44z" fill="url(#N)"/>
<path d="M-82 154.5c3.514 0 7.238.78 10 2.44v65.12c-2.012 1.79-6.486 2.44-10 2.44s-7.863-.15-10-2.44v-65.12c1.887-1.66 6.486-2.44 10-2.44z" fill="url(#O)"/>
<path d="M-82 217.5c3.514 0 5.863.78 8 2.44v5.12c-2.387 1.54-4.486 2.44-8 2.44s-5.738-.9-8-2.44v-5.12c2.387-1.91 4.486-2.44 8-2.44z" fill="url(#P)"/>
<path fill="#e0bb41" d="M-82 221.5c-3.514 0-5.613.28-8 2.44v1.12c2.262 1.54 4.486 2.44 8 2.44s5.613-.9 8-2.44v-1.12c-2.512-2.04-4.486-2.44-8-2.44z"/>
<path d="M125.06 134.69c-27.176-27.18-27.176-71.251 0-98.431 27.18-27.18 71.25-27.18 98.43 0s27.18 71.251 0 98.431-71.25 27.18-98.43 0zm2.96-2.96c26.09 26.1 67.41 27.08 93.5.99 26.1-26.1 25.11-67.416-.98-93.509-26.09-26.092-67.41-27.077-93.51-.984-26.09 26.093-25.11 67.413.99 93.503z" fill="#2d3335"/>
<path fill="#9eaaac" d="M125.06 134.69c-27.176-27.18-27.176-71.251 0-98.431.17-.166.33-.329.5-.493-26.689 27.218-26.526 70.924.49 97.934 27.01 27.02 70.72 27.18 97.93.49-.16.17-.32.33-.49.5-27.18 27.18-71.25 27.18-98.43 0zm2.46-96.955c.17-.167.33-.327.5-.492 26.09-26.093 67.41-25.109 93.5.984 26.1 26.093 27.08 67.413.99 93.503l-.5.5c25.59-26.12 24.46-67.091-1.47-93.019-25.93-25.927-66.9-27.063-93.02-1.476z"/>
<path d="M127.03 132.72c26.1 26.09 68.4 26.09 94.49 0 26.1-26.1 26.1-68.4 0-94.493-26.09-26.093-68.39-26.093-94.49 0-26.09 26.093-26.09 68.393 0 94.493z" fill-opacity=".476" fill="#d0e9f2"/>
<path d="M127.03 132.72c26.1 26.09 68.4 26.09 94.49 0 26.1-26.1 26.1-68.4 0-94.493-26.09-26.093-68.39-26.093-94.49 0-26.09 26.093-26.09 68.393 0 94.493z" fill="url(#Q)"/>
<path fill="url(#R)" d="m116.05 115.28-5.75 4.09c3.3 6.21 7.56 12.05 12.8 17.28 5.23 5.24 11.07 9.5 17.28 12.8l4.09-5.75c-5.92-3.04-11.49-7.01-16.45-11.97s-8.93-10.53-11.97-16.45z"/>
<path fill="url(#S)" d="M124.82 134.93c1.73 1.73 3.08 3.67 4.12 5.72l-26.21 26.21a22.302 22.302 0 0 1-5.718-4.12 22.268 22.268 0 0 1-4.121-5.72l26.209-26.21c2.05 1.04 3.99 2.39 5.72 4.12z"/>
<path d="M116.27 159.23 50.199 225.3c-3.984-2.19-5.832-3-9.29-6.46s-4.267-5.3-6.459-9.29l66.07-66.07c5.06 3.75 12.12 10.4 15.75 15.75z" fill="url(#T)"/>
<path d="M105.78 154.12c3.46 3.46 7.43 6.66 8.52 11.02l-64.101 64.1c-6.517-5.16-14.555-12.79-19.686-19.69l64.1-64.1c3.491.23 7.657 5.16 11.117 8.62l.05.05z" fill="url(#U)"/>
<path opacity=".596" d="M105.53 154.36c3.46 3.46 7.68 6.42 8.77 10.78l-64.101 64.1c-7.877-6.91-11.798-10.63-19.686-19.69l64.1-64.1c3.491.23 7.417 5.45 10.877 8.91h.04z" fill="url(#V)"/>
<path opacity=".604" d="M131.17 37.05c40.66-32.173 78.13-12.728 77.78 5.303s-56.57 70.357-76.37 66.117-25.45-53.389-1.41-71.42z" fill="url(#W)"/>
<text opacity=".461" font-weight="bold" xml:space="preserve" transform="rotate(-8.9)" font-size="19.8" y="69.184" x="99.839" font-family="Monospace" fill="url(#X)"><tspan y="69.184" x="99.839">01011001</tspan><tspan y="93.934" x="99.839">00110101</tspan><tspan y="118.684" x="99.839">10010011</tspan></text>
<text font-weight="bold" xml:space="preserve" transform="rotate(-8.9)" font-size="19.8" y="67.898" x="99.41" font-family="Monospace" fill="url(#Y)"><tspan y="67.898" x="99.41">01011001</tspan><tspan y="92.648" x="99.41">00110101</tspan><tspan y="117.398" x="99.41">10010011</tspan></text>
<path opacity=".765" d="M219.46 129.09c-26.79 21.98-41.81 21.01-38.07 13.14 3.73-7.87 42.32-41.66 51.45-43.75 9.12-2.093 2.27 18.04-13.38 30.61z" fill="url(#Z)"/>
</svg>

After

Width:  |  Height:  |  Size: 49 KiB

@@ -0,0 +1,23 @@
from typing import Any
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.nominatim.tools.nominatim_search import NominatimSearchTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class NominatimProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
try:
result = NominatimSearchTool().fork_tool_runtime(
runtime={
"credentials": credentials,
}
).invoke(
user_id='',
tool_parameters={
'query': 'London',
'limit': 1,
},
)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

Some files were not shown because too many files have changed in this diff Show More