Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

新增difyapi 的Chat 请求运行器 #938

Merged
merged 4 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
674 changes: 674 additions & 0 deletions libs/LICENSE

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions libs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# LangBot/libs

LangBot 项目下的 libs 目录下的所有代码均遵循本目录下的许可证约束。
您在使用、修改、分发本目录下的代码时,需要遵守其中包含的条款。
3 changes: 3 additions & 0 deletions libs/dify_service_api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Dify Service API Python SDK

这个 SDK 尚不完全支持 Dify Service API 的所有功能。
2 changes: 2 additions & 0 deletions libs/dify_service_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .v1 import client
from .v1 import errors
44 changes: 44 additions & 0 deletions libs/dify_service_api/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from v1 import client

import asyncio

import os
import json


class TestDifyClient:
async def test_chat_messages(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL"))

resp = await cln.chat_messages(inputs={}, query="Who are you?", user="test")
print(json.dumps(resp, ensure_ascii=False, indent=4))

async def test_upload_file(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL"))

file_bytes = open("img.png", "rb").read()

print(type(file_bytes))

file = ("img2.png", file_bytes, "image/png")

resp = await cln.upload_file(file=file, user="test")
print(json.dumps(resp, ensure_ascii=False, indent=4))

async def test_workflow_run(self):
cln = client.AsyncDifyServiceClient(api_key=os.getenv("DIFY_API_KEY"), base_url=os.getenv("DIFY_BASE_URL"))

# resp = await cln.workflow_run(inputs={}, user="test")
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
# print(resp)
chunks = []

ignored_events = ['text_chunk']
async for chunk in cln.workflow_run(inputs={}, user="test"):
if chunk['event'] in ignored_events:
continue
chunks.append(chunk)
print(json.dumps(chunks, ensure_ascii=False, indent=4))

if __name__ == "__main__":
asyncio.run(TestDifyClient().test_workflow_run())
Empty file.
125 changes: 125 additions & 0 deletions libs/dify_service_api/v1/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from __future__ import annotations

import httpx
import typing
import json

from .errors import DifyAPIError


class AsyncDifyServiceClient:
"""Dify Service API 客户端"""

api_key: str
base_url: str

def __init__(
self,
api_key: str,
base_url: str = "https://api.dify.ai/v1",
) -> None:
self.api_key = api_key
self.base_url = base_url

async def chat_messages(
self,
inputs: dict[str, typing.Any],
query: str,
user: str,
response_mode: str = "blocking", # 当前不支持 streaming
conversation_id: str = "",
files: list[dict[str, typing.Any]] = [],
timeout: float = 30.0,
) -> dict[str, typing.Any]:
"""发送消息"""
if response_mode != "blocking":
raise DifyAPIError("当前仅支持 blocking 模式")

async with httpx.AsyncClient(
base_url=self.base_url,
trust_env=True,
timeout=timeout,
) as client:
response = await client.post(
"/chat-messages",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json={
"inputs": inputs,
"query": query,
"user": user,
"response_mode": response_mode,
"conversation_id": conversation_id,
"files": files,
},
)

if response.status_code != 200:
raise DifyAPIError(f"{response.status_code} {response.text}")

return response.json()

async def workflow_run(
self,
inputs: dict[str, typing.Any],
user: str,
response_mode: str = "streaming", # 当前不支持 blocking
files: list[dict[str, typing.Any]] = [],
timeout: float = 30.0,
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
"""运行工作流"""
if response_mode != "streaming":
raise DifyAPIError("当前仅支持 streaming 模式")

async with httpx.AsyncClient(
base_url=self.base_url,
trust_env=True,
timeout=timeout,
) as client:

async with client.stream(
"POST",
"/workflows/run",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json={
"inputs": inputs,
"user": user,
"response_mode": response_mode,
"files": files,
},
) as r:
async for chunk in r.aiter_lines():
if chunk.strip() == "":
continue
if chunk.startswith("data:"):
yield json.loads(chunk[5:])

async def upload_file(
self,
file: httpx._types.FileTypes,
user: str,
timeout: float = 30.0,
) -> str:
"""上传文件"""
# curl -X POST 'http://dify.rockchin.top/v1/files/upload' \
# --header 'Authorization: Bearer {api_key}' \
# --form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \
# --form 'user=abc-123'
async with httpx.AsyncClient(
base_url=self.base_url,
trust_env=True,
timeout=timeout,
) as client:
# multipart/form-data
response = await client.post(
"/files/upload",
headers={"Authorization": f"Bearer {self.api_key}"},
files={
"file": file,
"user": (None, user),
},
)

if response.status_code != 201:
raise DifyAPIError(f"{response.status_code} {response.text}")

return response.json()
17 changes: 17 additions & 0 deletions libs/dify_service_api/v1/client_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from . import client

import asyncio

import os


class TestDifyClient:
async def test_chat_messages(self):
cln = client.DifyClient(api_key=os.getenv("DIFY_API_KEY"))

resp = await cln.chat_messages(inputs={}, query="Who are you?", user_id="test")
print(resp)


if __name__ == "__main__":
asyncio.run(TestDifyClient().test_chat_messages())
6 changes: 6 additions & 0 deletions libs/dify_service_api/v1/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class DifyAPIError(Exception):
"""Dify API 请求失败"""

def __init__(self, message: str):
self.message = message
super().__init__(self.message)
5 changes: 4 additions & 1 deletion pkg/core/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class Config:


class Conversation(pydantic.BaseModel):
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation,但只有一个当前使用的 Conversation"""
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation,但只有一个当前使用的 Conversation"""

prompt: sysprompt_entities.Prompt

Expand All @@ -105,6 +105,9 @@ class Conversation(pydantic.BaseModel):

use_funcs: typing.Optional[list[tools_entities.LLMFunction]]

uuid: typing.Optional[str] = None
"""该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。"""


class Session(pydantic.BaseModel):
"""会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}"""
Expand Down
28 changes: 28 additions & 0 deletions pkg/core/migrations/m016_dify_service_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

from .. import migration


@migration.migration_class("dify-service-api-config", 16)
class DifyServiceAPICfgMigration(migration.Migration):
"""迁移"""

async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'dify-service-api' not in self.ap.provider_cfg.data

async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['dify-service-api'] = {
"base-url": "https://api.dify.ai/v1",
"app-type": "chat",
"chat": {
"api-key": "sk-1234567890"
},
"workflow": {
"api-key": "sk-1234567890",
"output-key": "summary"
}
}

await self.ap.provider_cfg.dump_config()
2 changes: 1 addition & 1 deletion pkg/core/stages/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ..migrations import m001_sensitive_word_migration, m002_openai_config_migration, m003_anthropic_requester_cfg_completion, m004_moonshot_cfg_completion
from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_qcg_center_url, m008_ad_fixwin_config_migrate, m009_msg_truncator_cfg
from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config
from ..migrations import m015_gitee_ai_config
from ..migrations import m015_gitee_ai_config, m016_dify_service_api


@stage.stage_class("MigrationStage")
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/runnermgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..core import app

from .runners import localagent

from .runners import difysvapi

class RunnerManager:

Expand Down
Loading