Add Dify integration and enhance workflow node specifications
- Introduce new fields `dify_api_url` and `dify_api_key` in `AssistantConfig` for Dify API integration. - Update `requirements.txt` to include `dify-client-python` for Dify SDK support. - Modify `config_resolver` to handle Dify connection information. - Add a new `globalNode` type in workflow specifications to provide unified settings across workflows. - Enhance node specifications with additional constraints and default values for better configuration management. - Update frontend components to support the new `globalNode` type and its properties, improving workflow editor functionality.
This commit is contained in:
127
backend/services/brains/dify_llm.py
Normal file
127
backend/services/brains/dify_llm.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Dify chat applications exposed as a Pipecat LLM processor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from dify_client import AsyncClient, models
|
||||
from loguru import logger
|
||||
from models import AssistantConfig
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
LLMContextFrame,
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMTextFrame,
|
||||
)
|
||||
from pipecat.processors.frame_processor import FrameDirection
|
||||
from pipecat.services.llm_service import LLMService
|
||||
from pipecat.services.settings import LLMSettings
|
||||
|
||||
|
||||
def normalize_api_base(url: str) -> str:
|
||||
"""Accept a Dify host, /v1 base URL, or full chat endpoint."""
|
||||
base = (url or "https://api.dify.ai").strip().rstrip("/")
|
||||
if base.endswith("/chat-messages"):
|
||||
base = base[: -len("/chat-messages")]
|
||||
if not base.endswith("/v1"):
|
||||
base = f"{base}/v1"
|
||||
return base
|
||||
|
||||
|
||||
def last_user_text(messages: list[dict]) -> str:
|
||||
for message in reversed(messages or []):
|
||||
if message.get("role") != "user":
|
||||
continue
|
||||
content = message.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
return "".join(
|
||||
str(part.get("text") or "")
|
||||
for part in content
|
||||
if isinstance(part, dict)
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
class DifyLLMService(LLMService):
|
||||
"""Stream Dify answer events into Pipecat's standard text frames."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cfg: AssistantConfig,
|
||||
*,
|
||||
client: AsyncClient | None = None,
|
||||
user_id: str | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
settings=LLMSettings(
|
||||
model=None,
|
||||
system_instruction=None,
|
||||
temperature=None,
|
||||
max_tokens=None,
|
||||
top_p=None,
|
||||
top_k=None,
|
||||
frequency_penalty=None,
|
||||
presence_penalty=None,
|
||||
seed=None,
|
||||
filter_incomplete_user_turns=None,
|
||||
user_turn_completion_config=None,
|
||||
)
|
||||
)
|
||||
self._client = client or AsyncClient(
|
||||
api_key=cfg.dify_api_key,
|
||||
api_base=normalize_api_base(cfg.dify_api_url),
|
||||
)
|
||||
self._user_id = user_id or f"ai-video-{uuid4().hex}"
|
||||
self._conversation_id = ""
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
await super().process_frame(frame, direction)
|
||||
|
||||
if not isinstance(frame, LLMContextFrame):
|
||||
await self.push_frame(frame, direction)
|
||||
return
|
||||
|
||||
user_text = last_user_text(frame.context.get_messages())
|
||||
if not user_text:
|
||||
return
|
||||
|
||||
await self.push_frame(LLMFullResponseStartFrame())
|
||||
try:
|
||||
request = models.ChatRequest(
|
||||
query=user_text,
|
||||
inputs={},
|
||||
user=self._user_id,
|
||||
response_mode=models.ResponseMode.STREAMING,
|
||||
conversation_id=self._conversation_id,
|
||||
auto_generate_name=False,
|
||||
)
|
||||
events = await self._client.achat_messages(request, timeout=120.0)
|
||||
async for event in events:
|
||||
conversation_id = getattr(event, "conversation_id", "")
|
||||
if conversation_id:
|
||||
self._conversation_id = conversation_id
|
||||
|
||||
event_name = str(getattr(event, "event", ""))
|
||||
if event_name == "error":
|
||||
logger.error(
|
||||
"Dify 流式错误: "
|
||||
f"code={getattr(event, 'code', '')} "
|
||||
f"message={getattr(event, 'message', '')}"
|
||||
)
|
||||
continue
|
||||
text = (
|
||||
getattr(event, "answer", "")
|
||||
if event_name in {"message", "agent_message"}
|
||||
else ""
|
||||
)
|
||||
if event_name == "text_chunk":
|
||||
text = getattr(getattr(event, "data", None), "text", "")
|
||||
if text:
|
||||
await self.push_frame(LLMTextFrame(text))
|
||||
except Exception as exc: # noqa: BLE001 - one failed turn must not kill the call
|
||||
logger.error(f"Dify 调用失败: {exc}")
|
||||
finally:
|
||||
await self.push_frame(LLMFullResponseEndFrame())
|
||||
Reference in New Issue
Block a user