Add workflow support and enhance runtime configuration in models and services
- Introduce RuntimeModelResource and RuntimeKnowledgeBase classes to manage workflow resources. - Update AssistantConfig to include workflow_model_resources and workflow_knowledge_bases for better integration. - Refactor validation and processing logic in routes and services to accommodate workflow types. - Implement dynamic variable support for workflow assistants and enhance graph normalization. - Add ToolExecutor for reusable tool execution across different assistant types. - Update various services to ensure compatibility with new workflow features and improve error handling.
This commit is contained in:
@@ -2,11 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from urllib.parse import quote
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from models import AssistantConfig
|
||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSSpeakFrame
|
||||
@@ -20,10 +17,9 @@ from pipecat.utils.time import time_now_iso8601
|
||||
|
||||
from services.brains.base import BaseBrain, BrainRuntime, BrainSpec
|
||||
from services.runtime_variables import (
|
||||
DynamicVariableError,
|
||||
DynamicVariableStore,
|
||||
value_at_path,
|
||||
)
|
||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||
|
||||
|
||||
class PromptBrain(BaseBrain):
|
||||
@@ -37,6 +33,7 @@ class PromptBrain(BaseBrain):
|
||||
self._cfg = cfg
|
||||
self._dynamic_enabled = True
|
||||
self._store = DynamicVariableStore.from_config(cfg)
|
||||
self._tools = ToolExecutor(self._store)
|
||||
self._runtime: BrainRuntime | None = None
|
||||
|
||||
async def greeting(self, cfg: AssistantConfig) -> str:
|
||||
@@ -85,120 +82,16 @@ class PromptBrain(BaseBrain):
|
||||
self._runtime.set_system_prompt(self._store.render(self._cfg.prompt))
|
||||
|
||||
def _make_http_tool(self, tool, runtime: BrainRuntime):
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
parameters = list(config.get("parameters") or [])
|
||||
properties = {
|
||||
str(parameter.get("name")): {
|
||||
"type": str(parameter.get("type") or "string"),
|
||||
"description": str(parameter.get("description") or ""),
|
||||
}
|
||||
for parameter in parameters
|
||||
if parameter.get("name")
|
||||
}
|
||||
required = [
|
||||
str(parameter["name"])
|
||||
for parameter in parameters
|
||||
if parameter.get("name") and parameter.get("required", True)
|
||||
]
|
||||
|
||||
tool_secrets = tool.secrets or {}
|
||||
dynamic_secrets = tool_secrets.get("dynamic_variables") or {}
|
||||
for name, value in dynamic_secrets.items():
|
||||
if not str(name).startswith("secret__"):
|
||||
raise DynamicVariableError(f"工具密钥变量必须以 secret__ 开头: {name}")
|
||||
self._store.secrets[str(name)] = str(value)
|
||||
properties, required = self._tools.schema_parts(tool)
|
||||
self._tools.register_secrets(tool)
|
||||
|
||||
async def call_http(params: FunctionCallParams) -> None:
|
||||
arguments = params.arguments or {}
|
||||
url = self._store.render(str(config.get("url") or ""))
|
||||
configured_headers = self._store.render_data(
|
||||
deepcopy(config.get("headers") or {}), allow_secrets=True
|
||||
)
|
||||
secret_headers = self._store.render_data(
|
||||
deepcopy(tool_secrets.get("headers") or {}), allow_secrets=True
|
||||
)
|
||||
headers: dict[str, str] = {}
|
||||
query: dict[str, object] = {}
|
||||
body = self._store.render_data(deepcopy(config.get("body") or {}))
|
||||
|
||||
for parameter in parameters:
|
||||
name = str(parameter.get("name") or "")
|
||||
if not name or name not in arguments:
|
||||
continue
|
||||
value = arguments[name]
|
||||
location = str(parameter.get("location") or "body")
|
||||
if location == "path":
|
||||
encoded = quote(str(value), safe="")
|
||||
url = url.replace(f"{{{name}}}", encoded)
|
||||
elif location == "query":
|
||||
query[name] = value
|
||||
elif location == "header":
|
||||
headers[name] = str(value)
|
||||
else:
|
||||
body[name] = value
|
||||
|
||||
# Admin-configured headers win over model-provided arguments; secret
|
||||
# headers are applied last so an LLM can never replace credentials.
|
||||
headers.update(
|
||||
{str(key): str(value) for key, value in configured_headers.items()}
|
||||
)
|
||||
headers.update({str(key): str(value) for key, value in secret_headers.items()})
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=float(config.get("timeout_seconds") or 15),
|
||||
follow_redirects=False,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
str(config.get("method") or "GET"),
|
||||
url,
|
||||
headers=headers,
|
||||
params=query,
|
||||
json=body if body else None,
|
||||
)
|
||||
response.raise_for_status()
|
||||
if len(response.content) > 1_000_000:
|
||||
raise DynamicVariableError("HTTP 工具响应超过 1 MB 限制")
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
payload = {"text": response.text[:8000]}
|
||||
|
||||
updated: list[str] = []
|
||||
assignments = config.get("dynamic_variable_assignments") or {}
|
||||
for variable_name, path in assignments.items():
|
||||
try:
|
||||
value = value_at_path(payload, str(path))
|
||||
except KeyError:
|
||||
try:
|
||||
value = value_at_path({"response": payload}, str(path))
|
||||
except KeyError:
|
||||
continue
|
||||
self._store.assign(str(variable_name), value)
|
||||
updated.append(str(variable_name))
|
||||
if updated:
|
||||
result = await self._tools.execute(tool, dict(params.arguments or {}))
|
||||
if result["updated_variables"]:
|
||||
self._refresh_prompt()
|
||||
await params.result_callback(
|
||||
{
|
||||
"status": "ok",
|
||||
"status_code": response.status_code,
|
||||
"data": payload,
|
||||
"updated_variables": updated,
|
||||
}
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
await params.result_callback(
|
||||
{"status": "error", "message": "HTTP 工具调用超时"}
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
await params.result_callback(
|
||||
{
|
||||
"status": "error",
|
||||
"status_code": exc.response.status_code,
|
||||
"message": "HTTP 工具返回错误状态",
|
||||
}
|
||||
)
|
||||
except (httpx.RequestError, DynamicVariableError) as exc:
|
||||
await params.result_callback(result)
|
||||
except (ToolExecutionError, ValueError) as exc:
|
||||
await params.result_callback(
|
||||
{"status": "error", "message": f"HTTP 工具调用失败: {exc}"}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user