Merge branch 'main' into feat/support-extractor-tools
commit
67b1190535
@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
poetry install -C api
|
cd api && poetry install
|
||||||
@ -0,0 +1,241 @@
|
|||||||
|

|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introduzindo o Dify Workflow com Upload de Arquivo: Recrie o Podcast Google NotebookLM</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
|
||||||
|
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Auto-hospedagem</a> ·
|
||||||
|
<a href="https://docs.dify.ai">Documentação</a> ·
|
||||||
|
<a href="https://udify.app/chat/22L1zSxg6yW1cWQg">Consultas empresariais</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://dify.ai" target="_blank">
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/Product-F04438"></a>
|
||||||
|
<a href="https://dify.ai/pricing" target="_blank">
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/free-pricing?logo=free&color=%20%23155EEF&label=pricing&labelColor=%20%23528bff"></a>
|
||||||
|
<a href="https://discord.gg/FngNHpbcY7" target="_blank">
|
||||||
|
<img src="https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||||
|
alt="chat on Discord"></a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=dify_ai" target="_blank">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/dify_ai?logo=X&color=%20%23f5f5f5"
|
||||||
|
alt="follow on X(Twitter)"></a>
|
||||||
|
<a href="https://hub.docker.com/u/langgenius" target="_blank">
|
||||||
|
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langgenius/dify-web?labelColor=%20%23FDB062&color=%20%23f79009"></a>
|
||||||
|
<a href="https://github.com/langgenius/dify/graphs/commit-activity" target="_blank">
|
||||||
|
<img alt="Commits last month" src="https://img.shields.io/github/commit-activity/m/langgenius/dify?labelColor=%20%2332b583&color=%20%2312b76a"></a>
|
||||||
|
<a href="https://github.com/langgenius/dify/" target="_blank">
|
||||||
|
<img alt="Issues closed" src="https://img.shields.io/github/issues-search?query=repo%3Alanggenius%2Fdify%20is%3Aclosed&label=issues%20closed&labelColor=%20%237d89b0&color=%20%235d6b98"></a>
|
||||||
|
<a href="https://github.com/langgenius/dify/discussions/" target="_blank">
|
||||||
|
<img alt="Discussion posts" src="https://img.shields.io/github/discussions/langgenius/dify?labelColor=%20%239b8afb&color=%20%237a5af8"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="./README.md"><img alt="README em Inglês" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||||
|
<a href="./README_CN.md"><img alt="简体中文版自述文件" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
|
||||||
|
<a href="./README_JA.md"><img alt="日本語のREADME" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
|
||||||
|
<a href="./README_ES.md"><img alt="README em Espanhol" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
||||||
|
<a href="./README_FR.md"><img alt="README em Francês" src="https://img.shields.io/badge/Français-d9d9d9"></a>
|
||||||
|
<a href="./README_KL.md"><img alt="README tlhIngan Hol" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
|
||||||
|
<a href="./README_KR.md"><img alt="README em Coreano" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||||
|
<a href="./README_AR.md"><img alt="README em Árabe" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||||
|
<a href="./README_TR.md"><img alt="README em Turco" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||||
|
<a href="./README_VI.md"><img alt="README em Vietnamita" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||||
|
<a href="./README_PT.md"><img alt="README em Português - BR" src="https://img.shields.io/badge/Portugu%C3%AAs-BR?style=flat&label=BR&color=d9d9d9"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Dify é uma plataforma de desenvolvimento de aplicativos LLM de código aberto. Sua interface intuitiva combina workflow de IA, pipeline RAG, capacidades de agente, gerenciamento de modelos, recursos de observabilidade e muito mais, permitindo que você vá rapidamente do protótipo à produção. Aqui está uma lista das principais funcionalidades:
|
||||||
|
</br> </br>
|
||||||
|
|
||||||
|
**1. Workflow**:
|
||||||
|
Construa e teste workflows poderosos de IA em uma interface visual, aproveitando todos os recursos a seguir e muito mais.
|
||||||
|
|
||||||
|
|
||||||
|
https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**2. Suporte abrangente a modelos**:
|
||||||
|
Integração perfeita com centenas de LLMs proprietários e de código aberto de diversas provedoras e soluções auto-hospedadas, abrangendo GPT, Mistral, Llama3 e qualquer modelo compatível com a API da OpenAI. A lista completa de provedores suportados pode ser encontrada [aqui](https://docs.dify.ai/getting-started/readme/model-providers).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
**3. IDE de Prompt**:
|
||||||
|
Interface intuitiva para criação de prompts, comparação de desempenho de modelos e adição de recursos como conversão de texto para fala em um aplicativo baseado em chat.
|
||||||
|
|
||||||
|
**4. Pipeline RAG**:
|
||||||
|
Extensas capacidades de RAG que cobrem desde a ingestão de documentos até a recuperação, com suporte nativo para extração de texto de PDFs, PPTs e outros formatos de documentos comuns.
|
||||||
|
|
||||||
|
**5. Capacidades de agente**:
|
||||||
|
Você pode definir agentes com base em LLM Function Calling ou ReAct e adicionar ferramentas pré-construídas ou personalizadas para o agente. O Dify oferece mais de 50 ferramentas integradas para agentes de IA, como Google Search, DALL·E, Stable Diffusion e WolframAlpha.
|
||||||
|
|
||||||
|
**6. LLMOps**:
|
||||||
|
Monitore e analise os registros e o desempenho do aplicativo ao longo do tempo. É possível melhorar continuamente prompts, conjuntos de dados e modelos com base nos dados de produção e anotações.
|
||||||
|
|
||||||
|
**7. Backend como Serviço**:
|
||||||
|
Todas os recursos do Dify vêm com APIs correspondentes, permitindo que você integre o Dify sem esforço na lógica de negócios da sua empresa.
|
||||||
|
|
||||||
|
|
||||||
|
## Comparação de recursos
|
||||||
|
<table style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th align="center">Recurso</th>
|
||||||
|
<th align="center">Dify.AI</th>
|
||||||
|
<th align="center">LangChain</th>
|
||||||
|
<th align="center">Flowise</th>
|
||||||
|
<th align="center">OpenAI Assistants API</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Abordagem de Programação</td>
|
||||||
|
<td align="center">Orientada a API + Aplicativo</td>
|
||||||
|
<td align="center">Código Python</td>
|
||||||
|
<td align="center">Orientada a Aplicativo</td>
|
||||||
|
<td align="center">Orientada a API</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">LLMs Suportados</td>
|
||||||
|
<td align="center">Variedade Rica</td>
|
||||||
|
<td align="center">Variedade Rica</td>
|
||||||
|
<td align="center">Variedade Rica</td>
|
||||||
|
<td align="center">Apenas OpenAI</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">RAG Engine</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Agente</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Workflow</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Observabilidade</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Recursos Empresariais (SSO/Controle de Acesso)</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Implantação Local</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">✅</td>
|
||||||
|
<td align="center">❌</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Usando o Dify
|
||||||
|
|
||||||
|
- **Nuvem </br>**
|
||||||
|
Oferecemos o serviço [Dify Cloud](https://dify.ai) para qualquer pessoa experimentar sem nenhuma configuração. Ele fornece todas as funcionalidades da versão auto-hospedada, incluindo 200 chamadas GPT-4 gratuitas no plano sandbox.
|
||||||
|
|
||||||
|
- **Auto-hospedagem do Dify Community Edition</br>**
|
||||||
|
Configure rapidamente o Dify no seu ambiente com este [guia inicial](#quick-start).
|
||||||
|
Use nossa [documentação](https://docs.dify.ai) para referências adicionais e instruções mais detalhadas.
|
||||||
|
|
||||||
|
- **Dify para empresas/organizações</br>**
|
||||||
|
Oferecemos recursos adicionais voltados para empresas. [Envie suas perguntas através deste chatbot](https://udify.app/chat/22L1zSxg6yW1cWQg) ou [envie-nos um e-mail](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry) para discutir necessidades empresariais. </br>
|
||||||
|
> Para startups e pequenas empresas que utilizam AWS, confira o [Dify Premium no AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) e implemente no seu próprio AWS VPC com um clique. É uma oferta AMI acessível com a opção de criar aplicativos com logotipo e marca personalizados.
|
||||||
|
|
||||||
|
|
||||||
|
## Mantendo-se atualizado
|
||||||
|
|
||||||
|
Dê uma estrela no Dify no GitHub e seja notificado imediatamente sobre novos lançamentos.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Início rápido
|
||||||
|
> Antes de instalar o Dify, certifique-se de que sua máquina atenda aos seguintes requisitos mínimos de sistema:
|
||||||
|
>
|
||||||
|
>- CPU >= 2 Núcleos
|
||||||
|
>- RAM >= 4 GiB
|
||||||
|
|
||||||
|
</br>
|
||||||
|
|
||||||
|
A maneira mais fácil de iniciar o servidor Dify é executar nosso arquivo [docker-compose.yml](docker/docker-compose.yaml). Antes de rodar o comando de instalação, certifique-se de que o [Docker](https://docs.docker.com/get-docker/) e o [Docker Compose](https://docs.docker.com/compose/install/) estão instalados na sua máquina:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docker
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Após a execução, você pode acessar o painel do Dify no navegador em [http://localhost/install](http://localhost/install) e iniciar o processo de inicialização.
|
||||||
|
|
||||||
|
> Se você deseja contribuir com o Dify ou fazer desenvolvimento adicional, consulte nosso [guia para implantar a partir do código fonte](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code).
|
||||||
|
|
||||||
|
## Próximos passos
|
||||||
|
|
||||||
|
Se precisar personalizar a configuração, consulte os comentários no nosso arquivo [.env.example](docker/.env.example) e atualize os valores correspondentes no seu arquivo `.env`. Além disso, talvez seja necessário fazer ajustes no próprio arquivo `docker-compose.yaml`, como alterar versões de imagem, mapeamentos de portas ou montagens de volumes, com base no seu ambiente de implantação específico e nas suas necessidades. Após fazer quaisquer alterações, execute novamente `docker-compose up -d`. Você pode encontrar a lista completa de variáveis de ambiente disponíveis [aqui](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
||||||
|
|
||||||
|
Se deseja configurar uma instalação de alta disponibilidade, há [Helm Charts](https://helm.sh/) e arquivos YAML contribuídos pela comunidade que permitem a implantação do Dify no Kubernetes.
|
||||||
|
|
||||||
|
- [Helm Chart de @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
|
- [Helm Chart de @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Arquivo YAML de @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
|
||||||
|
#### Usando o Terraform para Implantação
|
||||||
|
|
||||||
|
Implante o Dify na Plataforma Cloud com um único clique usando [terraform](https://www.terraform.io/)
|
||||||
|
|
||||||
|
##### Azure Global
|
||||||
|
- [Azure Terraform por @nikawang](https://github.com/nikawang/dify-azure-terraform)
|
||||||
|
|
||||||
|
##### Google Cloud
|
||||||
|
- [Google Cloud Terraform por @sotazum](https://github.com/DeNA/dify-google-cloud-terraform)
|
||||||
|
|
||||||
|
## Contribuindo
|
||||||
|
|
||||||
|
Para aqueles que desejam contribuir com código, veja nosso [Guia de Contribuição](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
|
Ao mesmo tempo, considere apoiar o Dify compartilhando-o nas redes sociais e em eventos e conferências.
|
||||||
|
|
||||||
|
> Estamos buscando contribuidores para ajudar na tradução do Dify para idiomas além de Mandarim e Inglês. Se você tiver interesse em ajudar, consulte o [README i18n](https://github.com/langgenius/dify/blob/main/web/i18n/README.md) para mais informações e deixe-nos um comentário no canal `global-users` em nosso [Servidor da Comunidade no Discord](https://discord.gg/8Tpq4AcN9c).
|
||||||
|
|
||||||
|
**Contribuidores**
|
||||||
|
|
||||||
|
<a href="https://github.com/langgenius/dify/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langgenius/dify" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Comunidade e contato
|
||||||
|
|
||||||
|
* [Discussões no GitHub](https://github.com/langgenius/dify/discussions). Melhor para: compartilhar feedback e fazer perguntas.
|
||||||
|
* [Problemas no GitHub](https://github.com/langgenius/dify/issues). Melhor para: relatar bugs encontrados no Dify.AI e propor novos recursos. Veja nosso [Guia de Contribuição](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
|
||||||
|
* [Discord](https://discord.gg/FngNHpbcY7). Melhor para: compartilhar suas aplicações e interagir com a comunidade.
|
||||||
|
* [X(Twitter)](https://twitter.com/dify_ai). Melhor para: compartilhar suas aplicações e interagir com a comunidade.
|
||||||
|
|
||||||
|
## Histórico de estrelas
|
||||||
|
|
||||||
|
[](https://star-history.com/#langgenius/dify&Date)
|
||||||
|
|
||||||
|
## Divulgação de segurança
|
||||||
|
|
||||||
|
Para proteger sua privacidade, evite postar problemas de segurança no GitHub. Em vez disso, envie suas perguntas para security@dify.ai e forneceremos uma resposta mais detalhada.
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
Este repositório está disponível sob a [Licença de Código Aberto Dify](LICENSE), que é essencialmente Apache 2.0 com algumas restrições adicionais.
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import Field, PositiveInt
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class OceanBaseVectorConfig(BaseSettings):
|
||||||
|
"""
|
||||||
|
Configuration settings for OceanBase Vector database
|
||||||
|
"""
|
||||||
|
|
||||||
|
OCEANBASE_VECTOR_HOST: Optional[str] = Field(
|
||||||
|
description="Hostname or IP address of the OceanBase Vector server (e.g. 'localhost')",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
OCEANBASE_VECTOR_PORT: Optional[PositiveInt] = Field(
|
||||||
|
description="Port number on which the OceanBase Vector server is listening (default is 2881)",
|
||||||
|
default=2881,
|
||||||
|
)
|
||||||
|
|
||||||
|
OCEANBASE_VECTOR_USER: Optional[str] = Field(
|
||||||
|
description="Username for authenticating with the OceanBase Vector database",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
OCEANBASE_VECTOR_PASSWORD: Optional[str] = Field(
|
||||||
|
description="Password for authenticating with the OceanBase Vector database",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
OCEANBASE_VECTOR_DATABASE: Optional[str] = Field(
|
||||||
|
description="Name of the OceanBase Vector database to connect to",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="1200" height="925" viewBox="0 0 1200 925" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M780.152 250.999L907.882 462.174C907.882 462.174 880.925 510.854 867.43 535.21C834.845 594.039 764.171 612.49 710.442 508.333L420.376 0H0L459.926 803.307C552.303 964.663 787.366 964.663 879.743 803.307C989.874 610.952 1089.87 441.97 1200 249.646L1052.28 0H639.519L780.152 250.999Z" fill="#3366FF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 417 B |
@ -0,0 +1,83 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from core.model_runtime.entities.common_entities import I18nObject
|
||||||
|
from core.model_runtime.entities.llm_entities import LLMMode
|
||||||
|
from core.model_runtime.entities.model_entities import (
|
||||||
|
AIModelEntity,
|
||||||
|
DefaultParameterName,
|
||||||
|
FetchFrom,
|
||||||
|
ModelPropertyKey,
|
||||||
|
ModelType,
|
||||||
|
ParameterRule,
|
||||||
|
ParameterType,
|
||||||
|
PriceConfig,
|
||||||
|
)
|
||||||
|
from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel
|
||||||
|
|
||||||
|
|
||||||
|
class VesslAILargeLanguageModel(OAIAPICompatLargeLanguageModel):
|
||||||
|
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
|
||||||
|
features = []
|
||||||
|
|
||||||
|
entity = AIModelEntity(
|
||||||
|
model=model,
|
||||||
|
label=I18nObject(en_US=model),
|
||||||
|
model_type=ModelType.LLM,
|
||||||
|
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
|
||||||
|
features=features,
|
||||||
|
model_properties={
|
||||||
|
ModelPropertyKey.MODE: credentials.get("mode"),
|
||||||
|
},
|
||||||
|
parameter_rules=[
|
||||||
|
ParameterRule(
|
||||||
|
name=DefaultParameterName.TEMPERATURE.value,
|
||||||
|
label=I18nObject(en_US="Temperature"),
|
||||||
|
type=ParameterType.FLOAT,
|
||||||
|
default=float(credentials.get("temperature", 0.7)),
|
||||||
|
min=0,
|
||||||
|
max=2,
|
||||||
|
precision=2,
|
||||||
|
),
|
||||||
|
ParameterRule(
|
||||||
|
name=DefaultParameterName.TOP_P.value,
|
||||||
|
label=I18nObject(en_US="Top P"),
|
||||||
|
type=ParameterType.FLOAT,
|
||||||
|
default=float(credentials.get("top_p", 1)),
|
||||||
|
min=0,
|
||||||
|
max=1,
|
||||||
|
precision=2,
|
||||||
|
),
|
||||||
|
ParameterRule(
|
||||||
|
name=DefaultParameterName.TOP_K.value,
|
||||||
|
label=I18nObject(en_US="Top K"),
|
||||||
|
type=ParameterType.INT,
|
||||||
|
default=int(credentials.get("top_k", 50)),
|
||||||
|
min=-2147483647,
|
||||||
|
max=2147483647,
|
||||||
|
precision=0,
|
||||||
|
),
|
||||||
|
ParameterRule(
|
||||||
|
name=DefaultParameterName.MAX_TOKENS.value,
|
||||||
|
label=I18nObject(en_US="Max Tokens"),
|
||||||
|
type=ParameterType.INT,
|
||||||
|
default=512,
|
||||||
|
min=1,
|
||||||
|
max=int(credentials.get("max_tokens_to_sample", 4096)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
pricing=PriceConfig(
|
||||||
|
input=Decimal(credentials.get("input_price", 0)),
|
||||||
|
output=Decimal(credentials.get("output_price", 0)),
|
||||||
|
unit=Decimal(credentials.get("unit", 0)),
|
||||||
|
currency=credentials.get("currency", "USD"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if credentials["mode"] == "chat":
|
||||||
|
entity.model_properties[ModelPropertyKey.MODE] = LLMMode.CHAT.value
|
||||||
|
elif credentials["mode"] == "completion":
|
||||||
|
entity.model_properties[ModelPropertyKey.MODE] = LLMMode.COMPLETION.value
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown completion type {credentials['completion_type']}")
|
||||||
|
|
||||||
|
return entity
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from core.model_runtime.model_providers.__base.model_provider import ModelProvider
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VesslAIProvider(ModelProvider):
|
||||||
|
def validate_provider_credentials(self, credentials: dict) -> None:
|
||||||
|
pass
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
provider: vessl_ai
|
||||||
|
label:
|
||||||
|
en_US: vessl_ai
|
||||||
|
icon_small:
|
||||||
|
en_US: icon_s_en.svg
|
||||||
|
icon_large:
|
||||||
|
en_US: icon_l_en.png
|
||||||
|
background: "#F1EFED"
|
||||||
|
help:
|
||||||
|
title:
|
||||||
|
en_US: How to deploy VESSL AI LLM Model Endpoint
|
||||||
|
url:
|
||||||
|
en_US: https://docs.vessl.ai/guides/get-started/llama3-deployment
|
||||||
|
supported_model_types:
|
||||||
|
- llm
|
||||||
|
configurate_methods:
|
||||||
|
- customizable-model
|
||||||
|
model_credential_schema:
|
||||||
|
model:
|
||||||
|
label:
|
||||||
|
en_US: Model Name
|
||||||
|
placeholder:
|
||||||
|
en_US: Enter your model name
|
||||||
|
credential_form_schemas:
|
||||||
|
- variable: endpoint_url
|
||||||
|
label:
|
||||||
|
en_US: endpoint url
|
||||||
|
type: text-input
|
||||||
|
required: true
|
||||||
|
placeholder:
|
||||||
|
en_US: Enter the url of your endpoint url
|
||||||
|
- variable: api_key
|
||||||
|
required: true
|
||||||
|
label:
|
||||||
|
en_US: API Key
|
||||||
|
type: secret-input
|
||||||
|
placeholder:
|
||||||
|
en_US: Enter your VESSL AI secret key
|
||||||
|
- variable: mode
|
||||||
|
show_on:
|
||||||
|
- variable: __model_type
|
||||||
|
value: llm
|
||||||
|
label:
|
||||||
|
en_US: Completion mode
|
||||||
|
type: select
|
||||||
|
required: false
|
||||||
|
default: chat
|
||||||
|
placeholder:
|
||||||
|
en_US: Select completion mode
|
||||||
|
options:
|
||||||
|
- value: completion
|
||||||
|
label:
|
||||||
|
en_US: Completion
|
||||||
|
- value: chat
|
||||||
|
label:
|
||||||
|
en_US: Chat
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
model: ernie-4.0-turbo-128k
|
||||||
|
label:
|
||||||
|
en_US: Ernie-4.0-turbo-128K
|
||||||
|
model_type: llm
|
||||||
|
features:
|
||||||
|
- agent-thought
|
||||||
|
model_properties:
|
||||||
|
mode: chat
|
||||||
|
context_size: 131072
|
||||||
|
parameter_rules:
|
||||||
|
- name: temperature
|
||||||
|
use_template: temperature
|
||||||
|
min: 0.1
|
||||||
|
max: 1.0
|
||||||
|
default: 0.8
|
||||||
|
- name: top_p
|
||||||
|
use_template: top_p
|
||||||
|
- name: max_tokens
|
||||||
|
use_template: max_tokens
|
||||||
|
default: 1024
|
||||||
|
min: 2
|
||||||
|
max: 4096
|
||||||
|
- name: presence_penalty
|
||||||
|
use_template: presence_penalty
|
||||||
|
default: 1.0
|
||||||
|
min: 1.0
|
||||||
|
max: 2.0
|
||||||
|
- name: frequency_penalty
|
||||||
|
use_template: frequency_penalty
|
||||||
|
- name: response_format
|
||||||
|
use_template: response_format
|
||||||
|
- name: disable_search
|
||||||
|
label:
|
||||||
|
zh_Hans: 禁用搜索
|
||||||
|
en_US: Disable Search
|
||||||
|
type: boolean
|
||||||
|
help:
|
||||||
|
zh_Hans: 禁用模型自行进行外部搜索。
|
||||||
|
en_US: Disable the model to perform external search.
|
||||||
|
required: false
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, model_validator
|
||||||
|
from pyobvector import VECTOR, ObVecClient
|
||||||
|
from sqlalchemy import JSON, Column, String, func
|
||||||
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from core.rag.datasource.vdb.vector_base import BaseVector
|
||||||
|
from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
|
||||||
|
from core.rag.datasource.vdb.vector_type import VectorType
|
||||||
|
from core.rag.embedding.embedding_base import Embeddings
|
||||||
|
from core.rag.models.document import Document
|
||||||
|
from extensions.ext_redis import redis_client
|
||||||
|
from models.dataset import Dataset
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_OCEANBASE_HNSW_BUILD_PARAM = {"M": 16, "efConstruction": 256}
|
||||||
|
DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM = {"efSearch": 64}
|
||||||
|
OCEANBASE_SUPPORTED_VECTOR_INDEX_TYPE = "HNSW"
|
||||||
|
DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE = "l2"
|
||||||
|
|
||||||
|
|
||||||
|
class OceanBaseVectorConfig(BaseModel):
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
user: str
|
||||||
|
password: str
|
||||||
|
database: str
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def validate_config(cls, values: dict) -> dict:
|
||||||
|
if not values["host"]:
|
||||||
|
raise ValueError("config OCEANBASE_VECTOR_HOST is required")
|
||||||
|
if not values["port"]:
|
||||||
|
raise ValueError("config OCEANBASE_VECTOR_PORT is required")
|
||||||
|
if not values["user"]:
|
||||||
|
raise ValueError("config OCEANBASE_VECTOR_USER is required")
|
||||||
|
if not values["database"]:
|
||||||
|
raise ValueError("config OCEANBASE_VECTOR_DATABASE is required")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class OceanBaseVector(BaseVector):
|
||||||
|
def __init__(self, collection_name: str, config: OceanBaseVectorConfig):
|
||||||
|
super().__init__(collection_name)
|
||||||
|
self._config = config
|
||||||
|
self._hnsw_ef_search = -1
|
||||||
|
self._client = ObVecClient(
|
||||||
|
uri=f"{self._config.host}:{self._config.port}",
|
||||||
|
user=self._config.user,
|
||||||
|
password=self._config.password,
|
||||||
|
db_name=self._config.database,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_type(self) -> str:
|
||||||
|
return VectorType.OCEANBASE
|
||||||
|
|
||||||
|
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
|
||||||
|
self._vec_dim = len(embeddings[0])
|
||||||
|
self._create_collection()
|
||||||
|
self.add_texts(texts, embeddings)
|
||||||
|
|
||||||
|
def _create_collection(self) -> None:
|
||||||
|
lock_name = "vector_indexing_lock_" + self._collection_name
|
||||||
|
with redis_client.lock(lock_name, timeout=20):
|
||||||
|
collection_exist_cache_key = "vector_indexing_" + self._collection_name
|
||||||
|
if redis_client.get(collection_exist_cache_key):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._client.check_table_exists(self._collection_name):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
cols = [
|
||||||
|
Column("id", String(36), primary_key=True, autoincrement=False),
|
||||||
|
Column("vector", VECTOR(self._vec_dim)),
|
||||||
|
Column("text", LONGTEXT),
|
||||||
|
Column("metadata", JSON),
|
||||||
|
]
|
||||||
|
vidx_params = self._client.prepare_index_params()
|
||||||
|
vidx_params.add_index(
|
||||||
|
field_name="vector",
|
||||||
|
index_type=OCEANBASE_SUPPORTED_VECTOR_INDEX_TYPE,
|
||||||
|
index_name="vector_index",
|
||||||
|
metric_type=DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE,
|
||||||
|
params=DEFAULT_OCEANBASE_HNSW_BUILD_PARAM,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._client.create_table_with_index_params(
|
||||||
|
table_name=self._collection_name,
|
||||||
|
columns=cols,
|
||||||
|
vidxs=vidx_params,
|
||||||
|
)
|
||||||
|
vals = []
|
||||||
|
params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'")
|
||||||
|
for row in params:
|
||||||
|
val = int(row[6])
|
||||||
|
vals.append(val)
|
||||||
|
if len(vals) == 0:
|
||||||
|
print("ob_vector_memory_limit_percentage not found in parameters.")
|
||||||
|
exit(1)
|
||||||
|
if any(val == 0 for val in vals):
|
||||||
|
try:
|
||||||
|
self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30")
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(
|
||||||
|
"Failed to set ob_vector_memory_limit_percentage. "
|
||||||
|
+ "Maybe the database user has insufficient privilege.",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
redis_client.set(collection_exist_cache_key, 1, ex=3600)
|
||||||
|
|
||||||
|
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
|
||||||
|
ids = self._get_uuids(documents)
|
||||||
|
for id, doc, emb in zip(ids, documents, embeddings):
|
||||||
|
self._client.insert(
|
||||||
|
table_name=self._collection_name,
|
||||||
|
data={
|
||||||
|
"id": id,
|
||||||
|
"vector": emb,
|
||||||
|
"text": doc.page_content,
|
||||||
|
"metadata": doc.metadata,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def text_exists(self, id: str) -> bool:
|
||||||
|
cur = self._client.get(table_name=self._collection_name, id=id)
|
||||||
|
return cur.rowcount != 0
|
||||||
|
|
||||||
|
def delete_by_ids(self, ids: list[str]) -> None:
|
||||||
|
self._client.delete(table_name=self._collection_name, ids=ids)
|
||||||
|
|
||||||
|
def get_ids_by_metadata_field(self, key: str, value: str) -> list[str]:
|
||||||
|
cur = self._client.get(
|
||||||
|
table_name=self._collection_name,
|
||||||
|
where_clause=f"metadata->>'$.{key}' = '{value}'",
|
||||||
|
output_column_name=["id"],
|
||||||
|
)
|
||||||
|
return [row[0] for row in cur]
|
||||||
|
|
||||||
|
def delete_by_metadata_field(self, key: str, value: str) -> None:
|
||||||
|
ids = self.get_ids_by_metadata_field(key, value)
|
||||||
|
self.delete_by_ids(ids)
|
||||||
|
|
||||||
|
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||||
|
ef_search = kwargs.get("ef_search", self._hnsw_ef_search)
|
||||||
|
if ef_search != self._hnsw_ef_search:
|
||||||
|
self._client.set_ob_hnsw_ef_search(ef_search)
|
||||||
|
self._hnsw_ef_search = ef_search
|
||||||
|
topk = kwargs.get("top_k", 10)
|
||||||
|
cur = self._client.ann_search(
|
||||||
|
table_name=self._collection_name,
|
||||||
|
vec_column_name="vector",
|
||||||
|
vec_data=query_vector,
|
||||||
|
topk=topk,
|
||||||
|
distance_func=func.l2_distance,
|
||||||
|
output_column_names=["text", "metadata"],
|
||||||
|
with_dist=True,
|
||||||
|
)
|
||||||
|
docs = []
|
||||||
|
for text, metadata, distance in cur:
|
||||||
|
metadata = json.loads(metadata)
|
||||||
|
metadata["score"] = 1 - distance / math.sqrt(2)
|
||||||
|
docs.append(
|
||||||
|
Document(
|
||||||
|
page_content=text,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return docs
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self._client.drop_table_if_exist(self._collection_name)
|
||||||
|
|
||||||
|
|
||||||
|
class OceanBaseVectorFactory(AbstractVectorFactory):
|
||||||
|
def init_vector(
|
||||||
|
self,
|
||||||
|
dataset: Dataset,
|
||||||
|
attributes: list,
|
||||||
|
embeddings: Embeddings,
|
||||||
|
) -> BaseVector:
|
||||||
|
if dataset.index_struct_dict:
|
||||||
|
class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"]
|
||||||
|
collection_name = class_prefix.lower()
|
||||||
|
else:
|
||||||
|
dataset_id = dataset.id
|
||||||
|
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
|
||||||
|
dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.OCEANBASE, collection_name))
|
||||||
|
return OceanBaseVector(
|
||||||
|
collection_name,
|
||||||
|
OceanBaseVectorConfig(
|
||||||
|
host=dify_config.OCEANBASE_VECTOR_HOST,
|
||||||
|
port=dify_config.OCEANBASE_VECTOR_PORT,
|
||||||
|
user=dify_config.OCEANBASE_VECTOR_USER,
|
||||||
|
password=(dify_config.OCEANBASE_VECTOR_PASSWORD or ""),
|
||||||
|
database=dify_config.OCEANBASE_VECTOR_DATABASE,
|
||||||
|
),
|
||||||
|
)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class RerankMode(Enum):
|
class RerankMode(str, Enum):
|
||||||
RERANKING_MODEL = "reranking_model"
|
RERANKING_MODEL = "reranking_model"
|
||||||
WEIGHTED_SCORE = "weighted_score"
|
WEIGHTED_SCORE = "weighted_score"
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
from .service import AppDslService
|
||||||
|
|
||||||
|
__all__ = ["AppDslService"]
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
class DSLVersionNotSupportedError(ValueError):
|
||||||
|
"""Raised when the imported DSL version is not supported by the current Dify version."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidYAMLFormatError(ValueError):
|
||||||
|
"""Raised when the provided YAML format is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class MissingAppDataError(ValueError):
|
||||||
|
"""Raised when the app data is missing in the provided DSL."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAppModeError(ValueError):
|
||||||
|
"""Raised when the app mode is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class MissingWorkflowDataError(ValueError):
|
||||||
|
"""Raised when the workflow data is missing in the provided DSL."""
|
||||||
|
|
||||||
|
|
||||||
|
class MissingModelConfigError(ValueError):
|
||||||
|
"""Raised when the model config data is missing in the provided DSL."""
|
||||||
|
|
||||||
|
|
||||||
|
class FileSizeLimitExceededError(ValueError):
|
||||||
|
"""Raised when the file size exceeds the allowed limit."""
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyContentError(ValueError):
|
||||||
|
"""Raised when the content fetched from the URL is empty."""
|
||||||
|
|
||||||
|
|
||||||
|
class ContentDecodingError(ValueError):
|
||||||
|
"""Raised when there is an error decoding the content."""
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
import os
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta
|
||||||
|
from core.model_runtime.entities.message_entities import (
|
||||||
|
AssistantPromptMessage,
|
||||||
|
SystemPromptMessage,
|
||||||
|
UserPromptMessage,
|
||||||
|
)
|
||||||
|
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||||
|
from core.model_runtime.model_providers.vessl_ai.llm.llm import VesslAILargeLanguageModel
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_credentials():
|
||||||
|
model = VesslAILargeLanguageModel()
|
||||||
|
|
||||||
|
with pytest.raises(CredentialsValidateFailedError):
|
||||||
|
model.validate_credentials(
|
||||||
|
model=os.environ.get("VESSL_AI_MODEL_NAME"),
|
||||||
|
credentials={
|
||||||
|
"api_key": "invalid_key",
|
||||||
|
"endpoint_url": os.environ.get("VESSL_AI_ENDPOINT_URL"),
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(CredentialsValidateFailedError):
|
||||||
|
model.validate_credentials(
|
||||||
|
model=os.environ.get("VESSL_AI_MODEL_NAME"),
|
||||||
|
credentials={
|
||||||
|
"api_key": os.environ.get("VESSL_AI_API_KEY"),
|
||||||
|
"endpoint_url": "http://invalid_url",
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
model.validate_credentials(
|
||||||
|
model=os.environ.get("VESSL_AI_MODEL_NAME"),
|
||||||
|
credentials={
|
||||||
|
"api_key": os.environ.get("VESSL_AI_API_KEY"),
|
||||||
|
"endpoint_url": os.environ.get("VESSL_AI_ENDPOINT_URL"),
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invoke_model():
|
||||||
|
model = VesslAILargeLanguageModel()
|
||||||
|
|
||||||
|
response = model.invoke(
|
||||||
|
model=os.environ.get("VESSL_AI_MODEL_NAME"),
|
||||||
|
credentials={
|
||||||
|
"api_key": os.environ.get("VESSL_AI_API_KEY"),
|
||||||
|
"endpoint_url": os.environ.get("VESSL_AI_ENDPOINT_URL"),
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
prompt_messages=[
|
||||||
|
SystemPromptMessage(
|
||||||
|
content="You are a helpful AI assistant.",
|
||||||
|
),
|
||||||
|
UserPromptMessage(content="Who are you?"),
|
||||||
|
],
|
||||||
|
model_parameters={
|
||||||
|
"temperature": 1.0,
|
||||||
|
"top_k": 2,
|
||||||
|
"top_p": 0.5,
|
||||||
|
},
|
||||||
|
stop=["How"],
|
||||||
|
stream=False,
|
||||||
|
user="abc-123",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(response, LLMResult)
|
||||||
|
assert len(response.message.content) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_invoke_stream_model():
|
||||||
|
model = VesslAILargeLanguageModel()
|
||||||
|
|
||||||
|
response = model.invoke(
|
||||||
|
model=os.environ.get("VESSL_AI_MODEL_NAME"),
|
||||||
|
credentials={
|
||||||
|
"api_key": os.environ.get("VESSL_AI_API_KEY"),
|
||||||
|
"endpoint_url": os.environ.get("VESSL_AI_ENDPOINT_URL"),
|
||||||
|
"mode": "chat",
|
||||||
|
},
|
||||||
|
prompt_messages=[
|
||||||
|
SystemPromptMessage(
|
||||||
|
content="You are a helpful AI assistant.",
|
||||||
|
),
|
||||||
|
UserPromptMessage(content="Who are you?"),
|
||||||
|
],
|
||||||
|
model_parameters={
|
||||||
|
"temperature": 1.0,
|
||||||
|
"top_k": 2,
|
||||||
|
"top_p": 0.5,
|
||||||
|
},
|
||||||
|
stop=["How"],
|
||||||
|
stream=True,
|
||||||
|
user="abc-123",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(response, Generator)
|
||||||
|
|
||||||
|
for chunk in response:
|
||||||
|
assert isinstance(chunk, LLMResultChunk)
|
||||||
|
assert isinstance(chunk.delta, LLMResultChunkDelta)
|
||||||
|
assert isinstance(chunk.delta.message, AssistantPromptMessage)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_num_tokens():
|
||||||
|
model = VesslAILargeLanguageModel()
|
||||||
|
|
||||||
|
num_tokens = model.get_num_tokens(
|
||||||
|
model=os.environ.get("VESSL_AI_MODEL_NAME"),
|
||||||
|
credentials={
|
||||||
|
"api_key": os.environ.get("VESSL_AI_API_KEY"),
|
||||||
|
"endpoint_url": os.environ.get("VESSL_AI_ENDPOINT_URL"),
|
||||||
|
},
|
||||||
|
prompt_messages=[
|
||||||
|
SystemPromptMessage(
|
||||||
|
content="You are a helpful AI assistant.",
|
||||||
|
),
|
||||||
|
UserPromptMessage(content="Hello World!"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(num_tokens, int)
|
||||||
|
assert num_tokens == 21
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.rag.datasource.vdb.oceanbase.oceanbase_vector import (
|
||||||
|
OceanBaseVector,
|
||||||
|
OceanBaseVectorConfig,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.vdb.__mock.tcvectordb import setup_tcvectordb_mock
|
||||||
|
from tests.integration_tests.vdb.test_vector_store import (
|
||||||
|
AbstractVectorTest,
|
||||||
|
get_example_text,
|
||||||
|
setup_mock_redis,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def oceanbase_vector():
|
||||||
|
return OceanBaseVector(
|
||||||
|
"dify_test_collection",
|
||||||
|
config=OceanBaseVectorConfig(
|
||||||
|
host="127.0.0.1",
|
||||||
|
port="2881",
|
||||||
|
user="root@test",
|
||||||
|
database="test",
|
||||||
|
password="test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OceanBaseVectorTest(AbstractVectorTest):
|
||||||
|
def __init__(self, vector: OceanBaseVector):
|
||||||
|
super().__init__()
|
||||||
|
self.vector = vector
|
||||||
|
|
||||||
|
def search_by_vector(self):
|
||||||
|
hits_by_vector = self.vector.search_by_vector(query_vector=self.example_embedding)
|
||||||
|
assert len(hits_by_vector) == 0
|
||||||
|
|
||||||
|
def search_by_full_text(self):
|
||||||
|
hits_by_full_text = self.vector.search_by_full_text(query=get_example_text())
|
||||||
|
assert len(hits_by_full_text) == 0
|
||||||
|
|
||||||
|
def text_exists(self):
|
||||||
|
exist = self.vector.text_exists(self.example_doc_id)
|
||||||
|
assert exist == True
|
||||||
|
|
||||||
|
def get_ids_by_metadata_field(self):
|
||||||
|
ids = self.vector.get_ids_by_metadata_field(key="document_id", value=self.example_doc_id)
|
||||||
|
assert len(ids) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def setup_mock_oceanbase_client():
|
||||||
|
with patch("core.rag.datasource.vdb.oceanbase.oceanbase_vector.ObVecClient", new_callable=MagicMock) as mock_client:
|
||||||
|
yield mock_client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def setup_mock_oceanbase_vector(oceanbase_vector):
|
||||||
|
with patch.object(oceanbase_vector, "_client"):
|
||||||
|
yield oceanbase_vector
|
||||||
|
|
||||||
|
|
||||||
|
def test_oceanbase_vector(
|
||||||
|
setup_mock_redis,
|
||||||
|
setup_mock_oceanbase_client,
|
||||||
|
setup_mock_oceanbase_vector,
|
||||||
|
oceanbase_vector,
|
||||||
|
):
|
||||||
|
OceanBaseVectorTest(oceanbase_vector).run_all_tests()
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
|
from core.file import File, FileTransferMethod, FileType
|
||||||
|
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
||||||
|
from core.variables import ArrayAnySegment, ArrayFileSegment, NoneSegment
|
||||||
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
|
||||||
|
from core.workflow.nodes.answer import AnswerStreamGenerateRoute
|
||||||
|
from core.workflow.nodes.end import EndStreamParam
|
||||||
|
from core.workflow.nodes.llm.entities import ContextConfig, LLMNodeData, ModelConfig, VisionConfig, VisionConfigOptions
|
||||||
|
from core.workflow.nodes.llm.node import LLMNode
|
||||||
|
from models.enums import UserFrom
|
||||||
|
from models.workflow import WorkflowType
|
||||||
|
|
||||||
|
|
||||||
|
class TestLLMNode:
|
||||||
|
@pytest.fixture
|
||||||
|
def llm_node(self):
|
||||||
|
data = LLMNodeData(
|
||||||
|
title="Test LLM",
|
||||||
|
model=ModelConfig(provider="openai", name="gpt-3.5-turbo", mode="chat", completion_params={}),
|
||||||
|
prompt_template=[],
|
||||||
|
memory=None,
|
||||||
|
context=ContextConfig(enabled=False),
|
||||||
|
vision=VisionConfig(
|
||||||
|
enabled=True,
|
||||||
|
configs=VisionConfigOptions(
|
||||||
|
variable_selector=["sys", "files"],
|
||||||
|
detail=ImagePromptMessageContent.DETAIL.HIGH,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables={},
|
||||||
|
user_inputs={},
|
||||||
|
)
|
||||||
|
node = LLMNode(
|
||||||
|
id="1",
|
||||||
|
config={
|
||||||
|
"id": "1",
|
||||||
|
"data": data.model_dump(),
|
||||||
|
},
|
||||||
|
graph_init_params=GraphInitParams(
|
||||||
|
tenant_id="1",
|
||||||
|
app_id="1",
|
||||||
|
workflow_type=WorkflowType.WORKFLOW,
|
||||||
|
workflow_id="1",
|
||||||
|
graph_config={},
|
||||||
|
user_id="1",
|
||||||
|
user_from=UserFrom.ACCOUNT,
|
||||||
|
invoke_from=InvokeFrom.SERVICE_API,
|
||||||
|
call_depth=0,
|
||||||
|
),
|
||||||
|
graph=Graph(
|
||||||
|
root_node_id="1",
|
||||||
|
answer_stream_generate_routes=AnswerStreamGenerateRoute(
|
||||||
|
answer_dependencies={},
|
||||||
|
answer_generate_route={},
|
||||||
|
),
|
||||||
|
end_stream_param=EndStreamParam(
|
||||||
|
end_dependencies={},
|
||||||
|
end_stream_variable_selector_mapping={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
graph_runtime_state=GraphRuntimeState(
|
||||||
|
variable_pool=variable_pool,
|
||||||
|
start_at=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return node
|
||||||
|
|
||||||
|
def test_fetch_files_with_file_segment(self, llm_node):
|
||||||
|
file = File(
|
||||||
|
id="1",
|
||||||
|
tenant_id="test",
|
||||||
|
type=FileType.IMAGE,
|
||||||
|
filename="test.jpg",
|
||||||
|
transfer_method=FileTransferMethod.LOCAL_FILE,
|
||||||
|
related_id="1",
|
||||||
|
)
|
||||||
|
llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], file)
|
||||||
|
|
||||||
|
result = llm_node._fetch_files(selector=["sys", "files"])
|
||||||
|
assert result == [file]
|
||||||
|
|
||||||
|
def test_fetch_files_with_array_file_segment(self, llm_node):
|
||||||
|
files = [
|
||||||
|
File(
|
||||||
|
id="1",
|
||||||
|
tenant_id="test",
|
||||||
|
type=FileType.IMAGE,
|
||||||
|
filename="test1.jpg",
|
||||||
|
transfer_method=FileTransferMethod.LOCAL_FILE,
|
||||||
|
related_id="1",
|
||||||
|
),
|
||||||
|
File(
|
||||||
|
id="2",
|
||||||
|
tenant_id="test",
|
||||||
|
type=FileType.IMAGE,
|
||||||
|
filename="test2.jpg",
|
||||||
|
transfer_method=FileTransferMethod.LOCAL_FILE,
|
||||||
|
related_id="2",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], ArrayFileSegment(value=files))
|
||||||
|
|
||||||
|
result = llm_node._fetch_files(selector=["sys", "files"])
|
||||||
|
assert result == files
|
||||||
|
|
||||||
|
def test_fetch_files_with_none_segment(self, llm_node):
|
||||||
|
llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], NoneSegment())
|
||||||
|
|
||||||
|
result = llm_node._fetch_files(selector=["sys", "files"])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_fetch_files_with_array_any_segment(self, llm_node):
|
||||||
|
llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], ArrayAnySegment(value=[]))
|
||||||
|
|
||||||
|
result = llm_node._fetch_files(selector=["sys", "files"])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_fetch_files_with_non_existent_variable(self, llm_node):
|
||||||
|
result = llm_node._fetch_files(selector=["sys", "files"])
|
||||||
|
assert result == []
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
from oss2 import Bucket
|
||||||
|
from oss2.models import GetObjectResult, PutObjectResult
|
||||||
|
|
||||||
|
from tests.unit_tests.oss.__mock.base import (
|
||||||
|
get_example_bucket,
|
||||||
|
get_example_data,
|
||||||
|
get_example_filename,
|
||||||
|
get_example_filepath,
|
||||||
|
get_example_folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, status, headers, request_id):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
self.request_id = request_id
|
||||||
|
|
||||||
|
|
||||||
|
class MockAliyunOssClass:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
auth,
|
||||||
|
endpoint,
|
||||||
|
bucket_name,
|
||||||
|
is_cname=False,
|
||||||
|
session=None,
|
||||||
|
connect_timeout=None,
|
||||||
|
app_name="",
|
||||||
|
enable_crc=True,
|
||||||
|
proxies=None,
|
||||||
|
region=None,
|
||||||
|
cloudbox_id=None,
|
||||||
|
is_path_style=False,
|
||||||
|
is_verify_object_strict=True,
|
||||||
|
):
|
||||||
|
self.bucket_name = get_example_bucket()
|
||||||
|
self.key = posixpath.join(get_example_folder(), get_example_filename())
|
||||||
|
self.content = get_example_data()
|
||||||
|
self.filepath = get_example_filepath()
|
||||||
|
self.resp = MockResponse(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"etag": "ee8de918d05640145b18f70f4c3aa602",
|
||||||
|
"x-oss-version-id": "CAEQNhiBgMDJgZCA0BYiIDc4MGZjZGI2OTBjOTRmNTE5NmU5NmFhZjhjYmY0****",
|
||||||
|
},
|
||||||
|
"request_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
def put_object(self, key, data, headers=None, progress_callback=None):
|
||||||
|
assert key == self.key
|
||||||
|
assert data == self.content
|
||||||
|
return PutObjectResult(self.resp)
|
||||||
|
|
||||||
|
def get_object(self, key, byte_range=None, headers=None, progress_callback=None, process=None, params=None):
|
||||||
|
assert key == self.key
|
||||||
|
|
||||||
|
get_object_output = MagicMock(GetObjectResult)
|
||||||
|
get_object_output.read.return_value = self.content
|
||||||
|
return get_object_output
|
||||||
|
|
||||||
|
def get_object_to_file(
|
||||||
|
self, key, filename, byte_range=None, headers=None, progress_callback=None, process=None, params=None
|
||||||
|
):
|
||||||
|
assert key == self.key
|
||||||
|
assert filename == self.filepath
|
||||||
|
|
||||||
|
def object_exists(self, key, headers=None):
|
||||||
|
assert key == self.key
|
||||||
|
return True
|
||||||
|
|
||||||
|
def delete_object(self, key, params=None, headers=None):
|
||||||
|
assert key == self.key
|
||||||
|
self.resp.headers["x-oss-delete-marker"] = True
|
||||||
|
return self.resp
|
||||||
|
|
||||||
|
|
||||||
|
MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def setup_aliyun_oss_mock(monkeypatch: MonkeyPatch):
|
||||||
|
if MOCK:
|
||||||
|
monkeypatch.setattr(Bucket, "__init__", MockAliyunOssClass.__init__)
|
||||||
|
monkeypatch.setattr(Bucket, "put_object", MockAliyunOssClass.put_object)
|
||||||
|
monkeypatch.setattr(Bucket, "get_object", MockAliyunOssClass.get_object)
|
||||||
|
monkeypatch.setattr(Bucket, "get_object_to_file", MockAliyunOssClass.get_object_to_file)
|
||||||
|
monkeypatch.setattr(Bucket, "object_exists", MockAliyunOssClass.object_exists)
|
||||||
|
monkeypatch.setattr(Bucket, "delete_object", MockAliyunOssClass.delete_object)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
if MOCK:
|
||||||
|
monkeypatch.undo()
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from oss2 import Auth
|
||||||
|
|
||||||
|
from extensions.storage.aliyun_oss_storage import AliyunOssStorage
|
||||||
|
from tests.unit_tests.oss.__mock.aliyun_oss import setup_aliyun_oss_mock
|
||||||
|
from tests.unit_tests.oss.__mock.base import (
|
||||||
|
BaseStorageTest,
|
||||||
|
get_example_bucket,
|
||||||
|
get_example_folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAliyunOss(BaseStorageTest):
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup_method(self, setup_aliyun_oss_mock):
|
||||||
|
"""Executed before each test method."""
|
||||||
|
with patch.object(Auth, "__init__", return_value=None):
|
||||||
|
self.storage = AliyunOssStorage()
|
||||||
|
self.storage.bucket_name = get_example_bucket()
|
||||||
|
self.storage.folder = get_example_folder()
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import pytest
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
from services.app_dsl_service import AppDslService
|
||||||
|
from services.app_dsl_service.exc import DSLVersionNotSupportedError
|
||||||
|
from services.app_dsl_service.service import _check_or_fix_dsl, current_dsl_version
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppDSLService:
|
||||||
|
def test_check_or_fix_dsl_missing_version(self):
|
||||||
|
import_data = {}
|
||||||
|
result = _check_or_fix_dsl(import_data)
|
||||||
|
assert result["version"] == "0.1.0"
|
||||||
|
assert result["kind"] == "app"
|
||||||
|
|
||||||
|
def test_check_or_fix_dsl_missing_kind(self):
|
||||||
|
import_data = {"version": "0.1.0"}
|
||||||
|
result = _check_or_fix_dsl(import_data)
|
||||||
|
assert result["kind"] == "app"
|
||||||
|
|
||||||
|
def test_check_or_fix_dsl_older_version(self):
|
||||||
|
import_data = {"version": "0.0.9", "kind": "app"}
|
||||||
|
result = _check_or_fix_dsl(import_data)
|
||||||
|
assert result["version"] == "0.0.9"
|
||||||
|
|
||||||
|
def test_check_or_fix_dsl_current_version(self):
|
||||||
|
import_data = {"version": current_dsl_version, "kind": "app"}
|
||||||
|
result = _check_or_fix_dsl(import_data)
|
||||||
|
assert result["version"] == current_dsl_version
|
||||||
|
|
||||||
|
def test_check_or_fix_dsl_newer_version(self):
|
||||||
|
current_version = version.parse(current_dsl_version)
|
||||||
|
newer_version = f"{current_version.major}.{current_version.minor + 1}.0"
|
||||||
|
import_data = {"version": newer_version, "kind": "app"}
|
||||||
|
with pytest.raises(DSLVersionNotSupportedError):
|
||||||
|
_check_or_fix_dsl(import_data)
|
||||||
|
|
||||||
|
def test_check_or_fix_dsl_invalid_kind(self):
|
||||||
|
import_data = {"version": current_dsl_version, "kind": "invalid"}
|
||||||
|
result = _check_or_fix_dsl(import_data)
|
||||||
|
assert result["kind"] == "app"
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
app:
|
||||||
|
port: 8194
|
||||||
|
debug: True
|
||||||
|
key: dify-sandbox
|
||||||
|
max_workers: 4
|
||||||
|
max_requests: 50
|
||||||
|
worker_timeout: 5
|
||||||
|
python_path: /usr/local/bin/python3
|
||||||
|
enable_network: True # please make sure there is no network risk in your environment
|
||||||
|
allowed_syscalls: # please leave it empty if you have no idea how seccomp works
|
||||||
|
proxy:
|
||||||
|
socks5: ''
|
||||||
|
http: ''
|
||||||
|
https: ''
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
app:
|
||||||
|
port: 8194
|
||||||
|
debug: True
|
||||||
|
key: dify-sandbox
|
||||||
|
max_workers: 4
|
||||||
|
max_requests: 50
|
||||||
|
worker_timeout: 5
|
||||||
|
python_path: /usr/local/bin/python3
|
||||||
|
python_lib_path:
|
||||||
|
- /usr/local/lib/python3.10
|
||||||
|
- /usr/lib/python3.10
|
||||||
|
- /usr/lib/python3
|
||||||
|
- /usr/lib/x86_64-linux-gnu
|
||||||
|
- /etc/ssl/certs/ca-certificates.crt
|
||||||
|
- /etc/nsswitch.conf
|
||||||
|
- /etc/hosts
|
||||||
|
- /etc/resolv.conf
|
||||||
|
- /run/systemd/resolve/stub-resolv.conf
|
||||||
|
- /run/resolvconf/resolv.conf
|
||||||
|
- /etc/localtime
|
||||||
|
- /usr/share/zoneinfo
|
||||||
|
- /etc/timezone
|
||||||
|
# add more paths if needed
|
||||||
|
python_pip_mirror_url: https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
nodejs_path: /usr/local/bin/node
|
||||||
|
enable_network: True
|
||||||
|
allowed_syscalls:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
# add all the syscalls which you require
|
||||||
|
proxy:
|
||||||
|
socks5: ''
|
||||||
|
http: ''
|
||||||
|
https: ''
|
||||||
@ -0,0 +1,326 @@
|
|||||||
|
import { VarType } from '../../types'
|
||||||
|
import { extractFunctionParams, extractReturnType } from './code-parser'
|
||||||
|
import { CodeLanguage } from './types'
|
||||||
|
|
||||||
|
const SAMPLE_CODES = {
|
||||||
|
python3: {
|
||||||
|
noParams: 'def main():',
|
||||||
|
singleParam: 'def main(param1):',
|
||||||
|
multipleParams: `def main(param1, param2, param3):
|
||||||
|
return {"result": param1}`,
|
||||||
|
withTypes: `def main(param1: str, param2: int, param3: List[str]):
|
||||||
|
result = process_data(param1, param2)
|
||||||
|
return {"output": result}`,
|
||||||
|
withDefaults: `def main(param1: str = "default", param2: int = 0):
|
||||||
|
return {"data": param1}`,
|
||||||
|
},
|
||||||
|
javascript: {
|
||||||
|
noParams: 'function main() {',
|
||||||
|
singleParam: 'function main(param1) {',
|
||||||
|
multipleParams: `function main(param1, param2, param3) {
|
||||||
|
return { result: param1 }
|
||||||
|
}`,
|
||||||
|
withComments: `// Main function
|
||||||
|
function main(param1, param2) {
|
||||||
|
// Process data
|
||||||
|
return { output: process(param1, param2) }
|
||||||
|
}`,
|
||||||
|
withSpaces: 'function main( param1 , param2 ) {',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('extractFunctionParams', () => {
|
||||||
|
describe('Python3', () => {
|
||||||
|
test('handles no parameters', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.python3.noParams, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extracts single parameter', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.python3.singleParam, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual(['param1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extracts multiple parameters', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.python3.multipleParams, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual(['param1', 'param2', 'param3'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles type hints', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.python3.withTypes, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual(['param1', 'param2', 'param3'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles default values', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.python3.withDefaults, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual(['param1', 'param2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// JavaScriptのテストケース
|
||||||
|
describe('JavaScript', () => {
|
||||||
|
test('handles no parameters', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.javascript.noParams, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extracts single parameter', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.javascript.singleParam, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual(['param1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extracts multiple parameters', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.javascript.multipleParams, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual(['param1', 'param2', 'param3'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles comments in code', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.javascript.withComments, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual(['param1', 'param2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles whitespace', () => {
|
||||||
|
const result = extractFunctionParams(SAMPLE_CODES.javascript.withSpaces, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual(['param1', 'param2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const RETURN_TYPE_SAMPLES = {
|
||||||
|
python3: {
|
||||||
|
singleReturn: `
|
||||||
|
def main(param1):
|
||||||
|
return {"result": "value"}`,
|
||||||
|
|
||||||
|
multipleReturns: `
|
||||||
|
def main(param1, param2):
|
||||||
|
return {"result": "value", "status": "success"}`,
|
||||||
|
|
||||||
|
noReturn: `
|
||||||
|
def main():
|
||||||
|
print("Hello")`,
|
||||||
|
|
||||||
|
complexReturn: `
|
||||||
|
def main():
|
||||||
|
data = process()
|
||||||
|
return {"result": data, "count": 42, "messages": ["hello"]}`,
|
||||||
|
nestedObject: `
|
||||||
|
def main(name, age, city):
|
||||||
|
return {
|
||||||
|
'personal_info': {
|
||||||
|
'name': name,
|
||||||
|
'age': age,
|
||||||
|
'city': city
|
||||||
|
},
|
||||||
|
'timestamp': int(time.time()),
|
||||||
|
'status': 'active'
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
javascript: {
|
||||||
|
singleReturn: `
|
||||||
|
function main(param1) {
|
||||||
|
return { result: "value" }
|
||||||
|
}`,
|
||||||
|
|
||||||
|
multipleReturns: `
|
||||||
|
function main(param1) {
|
||||||
|
return { result: "value", status: "success" }
|
||||||
|
}`,
|
||||||
|
|
||||||
|
withParentheses: `
|
||||||
|
function main() {
|
||||||
|
return ({ result: "value", status: "success" })
|
||||||
|
}`,
|
||||||
|
|
||||||
|
noReturn: `
|
||||||
|
function main() {
|
||||||
|
console.log("Hello")
|
||||||
|
}`,
|
||||||
|
|
||||||
|
withQuotes: `
|
||||||
|
function main() {
|
||||||
|
return { "result": 'value', 'status': "success" }
|
||||||
|
}`,
|
||||||
|
nestedObject: `
|
||||||
|
function main(name, age, city) {
|
||||||
|
return {
|
||||||
|
personal_info: {
|
||||||
|
name: name,
|
||||||
|
age: age,
|
||||||
|
city: city
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
withJSDoc: `
|
||||||
|
/**
|
||||||
|
* Creates a user profile with personal information and metadata
|
||||||
|
* @param {string} name - The user's name
|
||||||
|
* @param {number} age - The user's age
|
||||||
|
* @param {string} city - The user's city of residence
|
||||||
|
* @returns {Object} An object containing the user profile
|
||||||
|
*/
|
||||||
|
function main(name, age, city) {
|
||||||
|
return {
|
||||||
|
result: {
|
||||||
|
personal_info: {
|
||||||
|
name: name,
|
||||||
|
age: age,
|
||||||
|
city: city
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}`,
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('extractReturnType', () => {
|
||||||
|
// Python3のテスト
|
||||||
|
describe('Python3', () => {
|
||||||
|
test('extracts single return value', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.singleReturn, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extracts multiple return values', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.multipleReturns, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty object when no return statement', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.noReturn, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles complex return statement', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.complexReturn, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('handles nested object structure', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.nestedObject, CodeLanguage.python3)
|
||||||
|
expect(result).toEqual({
|
||||||
|
personal_info: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// JavaScriptのテスト
|
||||||
|
describe('JavaScript', () => {
|
||||||
|
test('extracts single return value', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.singleReturn, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extracts multiple return values', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.multipleReturns, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles return with parentheses', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.withParentheses, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty object when no return statement', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.noReturn, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles quoted keys', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.withQuotes, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual({
|
||||||
|
result: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('handles nested object structure', () => {
|
||||||
|
const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.nestedObject, CodeLanguage.javascript)
|
||||||
|
expect(result).toEqual({
|
||||||
|
personal_info: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { VarType } from '../../types'
|
||||||
|
import type { OutputVar } from './types'
|
||||||
|
import { CodeLanguage } from './types'
|
||||||
|
|
||||||
|
export const extractFunctionParams = (code: string, language: CodeLanguage) => {
|
||||||
|
if (language === CodeLanguage.json)
|
||||||
|
return []
|
||||||
|
|
||||||
|
const patterns: Record<Exclude<CodeLanguage, CodeLanguage.json>, RegExp> = {
|
||||||
|
[CodeLanguage.python3]: /def\s+main\s*\((.*?)\)/,
|
||||||
|
[CodeLanguage.javascript]: /function\s+main\s*\((.*?)\)/,
|
||||||
|
}
|
||||||
|
const match = code.match(patterns[language])
|
||||||
|
const params: string[] = []
|
||||||
|
|
||||||
|
if (match?.[1]) {
|
||||||
|
params.push(...match[1].split(',')
|
||||||
|
.map(p => p.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(p => p.split(':')[0].trim()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
export const extractReturnType = (code: string, language: CodeLanguage): OutputVar => {
|
||||||
|
const codeWithoutComments = code.replace(/\/\*\*[\s\S]*?\*\//, '')
|
||||||
|
console.log(codeWithoutComments)
|
||||||
|
|
||||||
|
const returnIndex = codeWithoutComments.indexOf('return')
|
||||||
|
if (returnIndex === -1)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
// returnから始まる部分文字列を取得
|
||||||
|
const codeAfterReturn = codeWithoutComments.slice(returnIndex)
|
||||||
|
|
||||||
|
let bracketCount = 0
|
||||||
|
let startIndex = codeAfterReturn.indexOf('{')
|
||||||
|
|
||||||
|
if (language === CodeLanguage.javascript && startIndex === -1) {
|
||||||
|
const parenStart = codeAfterReturn.indexOf('(')
|
||||||
|
if (parenStart !== -1)
|
||||||
|
startIndex = codeAfterReturn.indexOf('{', parenStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex === -1)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
let endIndex = -1
|
||||||
|
|
||||||
|
for (let i = startIndex; i < codeAfterReturn.length; i++) {
|
||||||
|
if (codeAfterReturn[i] === '{')
|
||||||
|
bracketCount++
|
||||||
|
if (codeAfterReturn[i] === '}') {
|
||||||
|
bracketCount--
|
||||||
|
if (bracketCount === 0) {
|
||||||
|
endIndex = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex === -1)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
const returnContent = codeAfterReturn.slice(startIndex + 1, endIndex - 1)
|
||||||
|
console.log(returnContent)
|
||||||
|
|
||||||
|
const result: OutputVar = {}
|
||||||
|
|
||||||
|
const keyRegex = /['"]?(\w+)['"]?\s*:(?![^{]*})/g
|
||||||
|
const matches = returnContent.matchAll(keyRegex)
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
console.log(`Found key: "${match[1]}" from match: "${match[0]}"`)
|
||||||
|
const key = match[1]
|
||||||
|
result[key] = {
|
||||||
|
type: VarType.string,
|
||||||
|
children: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue