wisruntime 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wisruntime/__init__.py +91 -0
- wisruntime/config.py +119 -0
- wisruntime/exceptions.py +56 -0
- wisruntime/model.py +326 -0
- wisruntime/server/__init__.py +0 -0
- wisruntime/server/dify_handler.py +416 -0
- wisruntime/server/invoker.py +161 -0
- wisruntime/server/protocol.py +104 -0
- wisruntime/server/server.py +152 -0
- wisruntime-0.1.0.dist-info/METADATA +816 -0
- wisruntime-0.1.0.dist-info/RECORD +13 -0
- wisruntime-0.1.0.dist-info/WHEEL +5 -0
- wisruntime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""DifyHandler — Dify SSE 协议处理器。
|
|
2
|
+
|
|
3
|
+
实现 Dify 兼容的对话 API:
|
|
4
|
+
- POST /dify/v1/chat-messages (SSE Streaming + Blocking)
|
|
5
|
+
- 心跳 ping 每 10 秒
|
|
6
|
+
- agent_thought 的 id/position 状态维护
|
|
7
|
+
|
|
8
|
+
CanonicalEvent → Dify SSE 映射:
|
|
9
|
+
TextDelta → agent_message
|
|
10
|
+
ReasoningDelta → reasoning_chunk
|
|
11
|
+
ToolCallStart → agent_thought (tool + tool_input)
|
|
12
|
+
ToolCallEnd → agent_thought (observation)
|
|
13
|
+
SubAgentStart → subagent_start
|
|
14
|
+
SubAgentEnd → subagent_end
|
|
15
|
+
FileOutput → message_file
|
|
16
|
+
Usage → 累积到 message_end.metadata.usage
|
|
17
|
+
Error → error
|
|
18
|
+
Custom → {event: name, ...payload}
|
|
19
|
+
Done → message_end
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import uuid
|
|
26
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
27
|
+
|
|
28
|
+
from fastapi import APIRouter, Request
|
|
29
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
30
|
+
from sse_starlette.sse import EventSourceResponse
|
|
31
|
+
from starlette.responses import Response
|
|
32
|
+
|
|
33
|
+
from wisruntime.model import (
|
|
34
|
+
AgentRequest,
|
|
35
|
+
Message,
|
|
36
|
+
UsageEvent,
|
|
37
|
+
)
|
|
38
|
+
from wisruntime.server.invoker import AgentInvoker
|
|
39
|
+
from wisruntime.server.protocol import ProtocolHandler
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# SSE 心跳间隔 (秒)
|
|
44
|
+
PING_INTERVAL = 10
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DifyHandler(ProtocolHandler):
|
|
48
|
+
"""Dify SSE 协议处理器。
|
|
49
|
+
|
|
50
|
+
将 CanonicalEvent 流序列化为 Dify 兼容的 SSE 格式。
|
|
51
|
+
内部维护 agent_thought 的 id/position 状态 (协议特有)。
|
|
52
|
+
|
|
53
|
+
Demo:
|
|
54
|
+
>>> handler = DifyHandler()
|
|
55
|
+
>>> handler.install(app, invoker)
|
|
56
|
+
>>> # → 注册 POST /dify/v1/chat-messages
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def get_prefix(self) -> str:
|
|
60
|
+
return "/dify/v1"
|
|
61
|
+
|
|
62
|
+
async def parse_request(self, request: Request) -> AgentRequest:
|
|
63
|
+
"""解析 Dify 请求 JSON 为归一化 AgentRequest。
|
|
64
|
+
|
|
65
|
+
字段映射:
|
|
66
|
+
- query → messages[0] (user message)
|
|
67
|
+
- response_mode → stream (streaming=True, blocking=False)
|
|
68
|
+
- conversation_id → conversation_id
|
|
69
|
+
- user → user_id
|
|
70
|
+
- inputs, files → protocol_extensions
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
request: Starlette Request。
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
AgentRequest 归一化请求。
|
|
77
|
+
"""
|
|
78
|
+
body = await request.json()
|
|
79
|
+
|
|
80
|
+
query = body.get("query", "")
|
|
81
|
+
response_mode = body.get("response_mode", "streaming")
|
|
82
|
+
|
|
83
|
+
# 构建归一化消息列表 — Dify 请求中是单轮 query
|
|
84
|
+
messages = [Message(role="user", content=query)]
|
|
85
|
+
|
|
86
|
+
# 收集 Dify 特有字段到 protocol_extensions
|
|
87
|
+
protocol_extensions = {}
|
|
88
|
+
for key in ("inputs", "files"):
|
|
89
|
+
if key in body:
|
|
90
|
+
protocol_extensions[key] = body[key]
|
|
91
|
+
|
|
92
|
+
return AgentRequest(
|
|
93
|
+
messages=messages,
|
|
94
|
+
stream=response_mode == "streaming",
|
|
95
|
+
conversation_id=body.get("conversation_id") or None,
|
|
96
|
+
user_id=body.get("user") or None,
|
|
97
|
+
headers=dict(request.headers),
|
|
98
|
+
raw_request=request,
|
|
99
|
+
protocol_extensions=protocol_extensions,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# ========================================================================
|
|
103
|
+
# SSE 流式序列化
|
|
104
|
+
# ========================================================================
|
|
105
|
+
|
|
106
|
+
async def _format_stream(
|
|
107
|
+
self,
|
|
108
|
+
events: AsyncGenerator,
|
|
109
|
+
task_id: str,
|
|
110
|
+
) -> AsyncGenerator[str, None]:
|
|
111
|
+
"""将 CanonicalEvent 流格式化为 Dify SSE 字符串。
|
|
112
|
+
|
|
113
|
+
维护内部状态:
|
|
114
|
+
- _position_counter: agent_thought 的位置序号 (从 1 开始递增)
|
|
115
|
+
- _accumulated_usage: 累积的 token 用量 (用于 message_end)
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
events: CanonicalEvent 异步流。
|
|
119
|
+
task_id: 本次请求的唯一标识。
|
|
120
|
+
|
|
121
|
+
Yields:
|
|
122
|
+
SSE 数据行字符串。
|
|
123
|
+
"""
|
|
124
|
+
position = 0
|
|
125
|
+
accumulated_usage: dict = {}
|
|
126
|
+
|
|
127
|
+
async for event in events:
|
|
128
|
+
match event.type:
|
|
129
|
+
case "text_delta":
|
|
130
|
+
yield self._sse_line(
|
|
131
|
+
event="agent_message",
|
|
132
|
+
task_id=task_id,
|
|
133
|
+
answer=event.delta,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
case "reasoning_delta":
|
|
137
|
+
yield self._sse_line(
|
|
138
|
+
event="reasoning_chunk",
|
|
139
|
+
task_id=task_id,
|
|
140
|
+
reasoning=event.delta,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
case "tool_call_start":
|
|
144
|
+
position += 1
|
|
145
|
+
thought_id = str(uuid.uuid4())
|
|
146
|
+
yield self._sse_line(
|
|
147
|
+
event="agent_thought",
|
|
148
|
+
task_id=task_id,
|
|
149
|
+
id=thought_id,
|
|
150
|
+
position=position,
|
|
151
|
+
tool=event.name,
|
|
152
|
+
tool_input=event.args_delta,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
case "tool_call_end":
|
|
156
|
+
yield self._sse_line(
|
|
157
|
+
event="agent_thought",
|
|
158
|
+
task_id=task_id,
|
|
159
|
+
id=str(uuid.uuid4()),
|
|
160
|
+
position=position,
|
|
161
|
+
observation=str(event.result) if event.result else event.error or "",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
case "subagent_start":
|
|
165
|
+
yield self._sse_line(
|
|
166
|
+
event="subagent_start",
|
|
167
|
+
task_id=task_id,
|
|
168
|
+
agent_name=event.agent_name,
|
|
169
|
+
run_id=event.run_id,
|
|
170
|
+
task_description=event.task_description,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
case "subagent_end":
|
|
174
|
+
yield self._sse_line(
|
|
175
|
+
event="subagent_end",
|
|
176
|
+
task_id=task_id,
|
|
177
|
+
agent_name=event.agent_name,
|
|
178
|
+
run_id=event.run_id,
|
|
179
|
+
status=event.status,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
case "file_output":
|
|
183
|
+
yield self._sse_line(
|
|
184
|
+
event="message_file",
|
|
185
|
+
task_id=task_id,
|
|
186
|
+
id=event.file_id,
|
|
187
|
+
type=event.file_type,
|
|
188
|
+
url=event.url,
|
|
189
|
+
belongs_to=event.belongs_to,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
case "usage":
|
|
193
|
+
accumulated_usage = {
|
|
194
|
+
"prompt_tokens": event.prompt_tokens,
|
|
195
|
+
"completion_tokens": event.completion_tokens,
|
|
196
|
+
"total_tokens": event.total_tokens,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "error":
|
|
200
|
+
yield self._sse_line(
|
|
201
|
+
event="error",
|
|
202
|
+
task_id=task_id,
|
|
203
|
+
message=event.message,
|
|
204
|
+
code=event.code or "UNKNOWN",
|
|
205
|
+
status=500,
|
|
206
|
+
)
|
|
207
|
+
return # 错误后停止流,不发送 message_end
|
|
208
|
+
|
|
209
|
+
case "custom":
|
|
210
|
+
yield self._sse_line(
|
|
211
|
+
event=event.name,
|
|
212
|
+
task_id=task_id,
|
|
213
|
+
**event.payload,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
case "done":
|
|
217
|
+
yield self._sse_line(
|
|
218
|
+
event="message_end",
|
|
219
|
+
task_id=task_id,
|
|
220
|
+
metadata={
|
|
221
|
+
"usage": accumulated_usage,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
return # 流正常结束
|
|
225
|
+
|
|
226
|
+
case _:
|
|
227
|
+
# 未知事件类型 → 跳过 (向前兼容)
|
|
228
|
+
logger.debug("跳过未知 CanonicalEvent 类型: %s", event.type)
|
|
229
|
+
|
|
230
|
+
# ========================================================================
|
|
231
|
+
# 心跳 ping
|
|
232
|
+
# ========================================================================
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
async def _ping_generator(event_generator: AsyncIterator[str]) -> AsyncGenerator[str, None]:
|
|
236
|
+
"""包装事件流,每 PING_INTERVAL 秒插入心跳 ping。
|
|
237
|
+
|
|
238
|
+
当内部 event_generator 结束时,ping task 自动取消。
|
|
239
|
+
使用 asyncio.create_task + Event 来实现"有数据时重置计时器"模式。
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
event_generator: 格式化后的事件 SSE 字符串流。
|
|
243
|
+
|
|
244
|
+
Yields:
|
|
245
|
+
包含心跳 ping 的完整 SSE 流。
|
|
246
|
+
"""
|
|
247
|
+
ping_event = DifyHandler._sse_line_static(event="ping")
|
|
248
|
+
|
|
249
|
+
async def event_producer(queue: asyncio.Queue):
|
|
250
|
+
"""从 event_generator 读取事件放入队列。"""
|
|
251
|
+
try:
|
|
252
|
+
async for event_str in event_generator:
|
|
253
|
+
await queue.put(("data", event_str))
|
|
254
|
+
await queue.put(("done", None))
|
|
255
|
+
except Exception as exc:
|
|
256
|
+
await queue.put(("error", exc))
|
|
257
|
+
|
|
258
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
259
|
+
producer_task = asyncio.create_task(event_producer(queue))
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
while True:
|
|
263
|
+
try:
|
|
264
|
+
# 等待事件或超时
|
|
265
|
+
msg_type, payload = await asyncio.wait_for(queue.get(), timeout=PING_INTERVAL)
|
|
266
|
+
|
|
267
|
+
if msg_type == "data":
|
|
268
|
+
yield payload
|
|
269
|
+
elif msg_type == "done":
|
|
270
|
+
break
|
|
271
|
+
elif msg_type == "error":
|
|
272
|
+
raise payload # type: ignore[misc]
|
|
273
|
+
except asyncio.TimeoutError:
|
|
274
|
+
# 超时 → 发送心跳
|
|
275
|
+
yield ping_event
|
|
276
|
+
|
|
277
|
+
finally:
|
|
278
|
+
# 确保 producer task 被取消
|
|
279
|
+
if not producer_task.done():
|
|
280
|
+
producer_task.cancel()
|
|
281
|
+
try:
|
|
282
|
+
await producer_task
|
|
283
|
+
except asyncio.CancelledError:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# ========================================================================
|
|
287
|
+
# Blocking 模式
|
|
288
|
+
# ========================================================================
|
|
289
|
+
|
|
290
|
+
async def _format_blocking(
|
|
291
|
+
self,
|
|
292
|
+
events: AsyncGenerator,
|
|
293
|
+
task_id: str,
|
|
294
|
+
) -> dict:
|
|
295
|
+
"""收集全部 CanonicalEvent,组装为 Blocking 模式响应。
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
events: CanonicalEvent 异步流。
|
|
299
|
+
task_id: 请求标识。
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Blocking 模式的 JSON 响应字典。
|
|
303
|
+
"""
|
|
304
|
+
answer_parts: list[str] = []
|
|
305
|
+
metadata: dict = {}
|
|
306
|
+
tool_calls: list[dict] = []
|
|
307
|
+
|
|
308
|
+
async for event in events:
|
|
309
|
+
match event.type:
|
|
310
|
+
case "text_delta":
|
|
311
|
+
answer_parts.append(event.delta)
|
|
312
|
+
case "usage":
|
|
313
|
+
metadata["usage"] = {
|
|
314
|
+
"prompt_tokens": event.prompt_tokens,
|
|
315
|
+
"completion_tokens": event.completion_tokens,
|
|
316
|
+
"total_tokens": event.total_tokens,
|
|
317
|
+
}
|
|
318
|
+
case "tool_call_start":
|
|
319
|
+
tool_calls.append({"name": event.name, "args": event.args_delta})
|
|
320
|
+
case "tool_call_end":
|
|
321
|
+
if tool_calls:
|
|
322
|
+
tool_calls[-1]["result"] = event.result
|
|
323
|
+
case "error":
|
|
324
|
+
return {
|
|
325
|
+
"task_id": task_id,
|
|
326
|
+
"error": {"message": event.message, "code": event.code},
|
|
327
|
+
}
|
|
328
|
+
case "done":
|
|
329
|
+
break
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
"task_id": task_id,
|
|
333
|
+
"answer": "".join(answer_parts),
|
|
334
|
+
"metadata": metadata,
|
|
335
|
+
"tool_calls": tool_calls,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# ========================================================================
|
|
339
|
+
# 路由注册
|
|
340
|
+
# ========================================================================
|
|
341
|
+
|
|
342
|
+
def _build_router(self) -> APIRouter:
|
|
343
|
+
"""构建 Dify 路由。
|
|
344
|
+
|
|
345
|
+
注册 POST /chat-messages 端点,支持 streaming 和 blocking 两种模式。
|
|
346
|
+
"""
|
|
347
|
+
router = APIRouter()
|
|
348
|
+
handler = self # 闭包引用
|
|
349
|
+
|
|
350
|
+
@router.post("/chat-messages")
|
|
351
|
+
async def chat_messages(request: Request):
|
|
352
|
+
"""Dify 对话接口 — SSE Streaming / Blocking。
|
|
353
|
+
|
|
354
|
+
根据请求中的 response_mode 自动选择:
|
|
355
|
+
- streaming: SSE 流式响应
|
|
356
|
+
- blocking: 阻塞等待完整结果后返回 JSON
|
|
357
|
+
"""
|
|
358
|
+
# 解析请求
|
|
359
|
+
agent_request = await handler.parse_request(request)
|
|
360
|
+
task_id = str(uuid.uuid4())
|
|
361
|
+
|
|
362
|
+
if agent_request.stream:
|
|
363
|
+
# SSE Streaming 模式
|
|
364
|
+
async def generate_sse():
|
|
365
|
+
"""生成 SSE 事件流 (含心跳)。"""
|
|
366
|
+
events = handler._invoker.invoke(handler._fn, agent_request)
|
|
367
|
+
formatted = handler._format_stream(events, task_id)
|
|
368
|
+
async for sse_line in handler._ping_generator(formatted):
|
|
369
|
+
yield sse_line
|
|
370
|
+
|
|
371
|
+
return StreamingResponse(
|
|
372
|
+
generate_sse(),
|
|
373
|
+
media_type="text/event-stream",
|
|
374
|
+
headers={
|
|
375
|
+
"Cache-Control": "no-cache",
|
|
376
|
+
"Connection": "keep-alive",
|
|
377
|
+
"X-Accel-Buffering": "no",
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
# Blocking 模式
|
|
382
|
+
events = handler._invoker.invoke(handler._fn, agent_request)
|
|
383
|
+
result = await handler._format_blocking(events, task_id)
|
|
384
|
+
return JSONResponse(content=result)
|
|
385
|
+
|
|
386
|
+
return router
|
|
387
|
+
|
|
388
|
+
# ========================================================================
|
|
389
|
+
# SSE 格式化工具
|
|
390
|
+
# ========================================================================
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def _sse_line(**kwargs) -> str:
|
|
394
|
+
"""将 dict 序列化为 Dify SSE 行: `data: {json}\\n\\n`。
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
**kwargs: 要序列化的字段。
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
SSE 格式化字符串。
|
|
401
|
+
|
|
402
|
+
Demo:
|
|
403
|
+
>>> DifyHandler._sse_line(event="ping")
|
|
404
|
+
'data: {"event": "ping"}\\n\\n'
|
|
405
|
+
"""
|
|
406
|
+
return f"data: {json.dumps(kwargs, ensure_ascii=False)}\n\n"
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def _sse_line_static(**kwargs) -> str:
|
|
410
|
+
"""与 _sse_line 相同,但作为静态方法独立调用 (用于心跳)。
|
|
411
|
+
|
|
412
|
+
Demo:
|
|
413
|
+
>>> DifyHandler._sse_line_static(event="ping")
|
|
414
|
+
'data: {"event": "ping"}\\n\\n'
|
|
415
|
+
"""
|
|
416
|
+
return f"data: {json.dumps(kwargs, ensure_ascii=False)}\n\n"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""AgentInvoker — 用户函数归一化引擎。
|
|
2
|
+
|
|
3
|
+
负责调用用户编写的 invoke_agent 函数,将其产出的各种类型
|
|
4
|
+
(str, CanonicalEvent, 异常) 统一转换为 AsyncGenerator[CanonicalEvent]。
|
|
5
|
+
|
|
6
|
+
核心职责:
|
|
7
|
+
1. 自动检测函数类型 (async generator / async func / sync func)
|
|
8
|
+
2. 归一化输出: str → TextDeltaEvent, CanonicalEvent → 透传, None → 跳过
|
|
9
|
+
3. 异常处理: Exception → ErrorEvent, 不 emit Done
|
|
10
|
+
4. 自动插入 Done 事件 (正常结束时)
|
|
11
|
+
|
|
12
|
+
参考: AgentRun SDK 的 AgentInvoker 设计。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import contextvars
|
|
17
|
+
import inspect
|
|
18
|
+
import logging
|
|
19
|
+
from collections.abc import AsyncGenerator, Callable
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from wisruntime.exceptions import InvokerError
|
|
23
|
+
from wisruntime.model import (
|
|
24
|
+
AgentRequest,
|
|
25
|
+
CanonicalEvent,
|
|
26
|
+
DoneEvent,
|
|
27
|
+
ErrorEvent,
|
|
28
|
+
TextDeltaEvent,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentInvoker:
|
|
35
|
+
"""用户函数归一化引擎。
|
|
36
|
+
|
|
37
|
+
接收用户编写的 invoke_agent 函数,调用它并将输出归一化为 CanonicalEvent 流。
|
|
38
|
+
用户函数可以是:
|
|
39
|
+
- 异步生成器 (async def + yield) — 最常见
|
|
40
|
+
- 异步函数 (async def + return) — 单次返回值
|
|
41
|
+
- 同步函数 (def) — 在线程池中执行
|
|
42
|
+
|
|
43
|
+
Demo:
|
|
44
|
+
>>> invoker = AgentInvoker()
|
|
45
|
+
>>> async def my_agent(request: AgentRequest):
|
|
46
|
+
... yield "你好"
|
|
47
|
+
... yield "世界"
|
|
48
|
+
>>> async for event in invoker.invoke(my_agent, request):
|
|
49
|
+
... print(event.type)
|
|
50
|
+
text_delta
|
|
51
|
+
text_delta
|
|
52
|
+
done
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self._fn: Callable | None = None
|
|
57
|
+
self._is_async_gen: bool = False
|
|
58
|
+
self._is_async_func: bool = False
|
|
59
|
+
|
|
60
|
+
async def invoke(
|
|
61
|
+
self,
|
|
62
|
+
fn: Callable,
|
|
63
|
+
request: AgentRequest,
|
|
64
|
+
) -> AsyncGenerator[CanonicalEvent, None]:
|
|
65
|
+
"""调用用户函数并归一化输出。
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
fn: 用户编写的 invoke_agent 函数。
|
|
69
|
+
request: 归一化的 AgentRequest。
|
|
70
|
+
|
|
71
|
+
Yields:
|
|
72
|
+
CanonicalEvent 流,以 DoneEvent 结束 (正常) 或 ErrorEvent (异常)。
|
|
73
|
+
"""
|
|
74
|
+
self._detect_function_type(fn)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
if self._is_async_gen:
|
|
78
|
+
# 异步生成器 — 最常见的场景,直接迭代
|
|
79
|
+
async for item in fn(request):
|
|
80
|
+
event = self._normalize_item(item)
|
|
81
|
+
if event is not None:
|
|
82
|
+
yield event
|
|
83
|
+
elif self._is_async_func:
|
|
84
|
+
# 异步函数 (非生成器) — await 后处理返回值
|
|
85
|
+
result = await fn(request)
|
|
86
|
+
event = self._normalize_item(result)
|
|
87
|
+
if event is not None:
|
|
88
|
+
yield event
|
|
89
|
+
else:
|
|
90
|
+
# 同步函数 — 在线程池中执行,保持 contextvars
|
|
91
|
+
ctx = contextvars.copy_context()
|
|
92
|
+
items = await asyncio.get_event_loop().run_in_executor(
|
|
93
|
+
None,
|
|
94
|
+
lambda: ctx.run(lambda: list(fn(request))),
|
|
95
|
+
)
|
|
96
|
+
for item in items:
|
|
97
|
+
event = self._normalize_item(item)
|
|
98
|
+
if event is not None:
|
|
99
|
+
yield event
|
|
100
|
+
|
|
101
|
+
# 正常结束 — 自动插入 Done 事件
|
|
102
|
+
yield DoneEvent()
|
|
103
|
+
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.exception("invoke_agent 执行异常")
|
|
106
|
+
yield ErrorEvent(
|
|
107
|
+
message=str(exc),
|
|
108
|
+
code=type(exc).__name__,
|
|
109
|
+
recoverable=False,
|
|
110
|
+
)
|
|
111
|
+
# 异常后不 emit Done — 流以 Error 终止
|
|
112
|
+
|
|
113
|
+
def _detect_function_type(self, fn: Callable) -> None:
|
|
114
|
+
"""检测函数类型: async generator / async function / sync function。
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
fn: 用户的 invoke_agent 函数。
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
InvokerError: 如果是同步生成器 (不支持),抛出此异常。
|
|
121
|
+
"""
|
|
122
|
+
if inspect.isasyncgenfunction(fn):
|
|
123
|
+
self._is_async_gen = True
|
|
124
|
+
self._is_async_func = False
|
|
125
|
+
elif inspect.iscoroutinefunction(fn):
|
|
126
|
+
self._is_async_gen = False
|
|
127
|
+
self._is_async_func = True
|
|
128
|
+
elif inspect.isgeneratorfunction(fn):
|
|
129
|
+
# 同步生成器不支持 — 会阻塞事件循环
|
|
130
|
+
raise InvokerError(
|
|
131
|
+
"invoke_agent 不支持同步生成器 (def + yield)。"
|
|
132
|
+
"请使用异步生成器 (async def + yield) 或同步函数返回列表。"
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
# 普通同步函数
|
|
136
|
+
self._is_async_gen = False
|
|
137
|
+
self._is_async_func = False
|
|
138
|
+
|
|
139
|
+
def _normalize_item(self, item: Any) -> CanonicalEvent | None:
|
|
140
|
+
"""归一化单个输出项。
|
|
141
|
+
|
|
142
|
+
转换规则:
|
|
143
|
+
- str → TextDeltaEvent
|
|
144
|
+
- CanonicalEvent 子类 → 透传
|
|
145
|
+
- None → 跳过 (返回 None)
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
item: 用户函数 yield/return 的单个值。
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
CanonicalEvent 或 None (表示跳过此项)。
|
|
152
|
+
"""
|
|
153
|
+
if item is None:
|
|
154
|
+
return None
|
|
155
|
+
if isinstance(item, str):
|
|
156
|
+
return TextDeltaEvent(delta=item)
|
|
157
|
+
# 已经是 CanonicalEvent 的实例 (如 ToolCallStartEvent 等),直接透传
|
|
158
|
+
if isinstance(item, type(TextDeltaEvent)) or hasattr(item, "type"):
|
|
159
|
+
return item
|
|
160
|
+
# 未知类型 — 转为 str 后包装为 TextDeltaEvent
|
|
161
|
+
return TextDeltaEvent(delta=str(item))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""协议处理器抽象基类和注册中心。
|
|
2
|
+
|
|
3
|
+
ProtocolHandler 定义协议处理器的统一接口:
|
|
4
|
+
- parse_request: HTTP 请求 → AgentRequest
|
|
5
|
+
- _format_stream: CanonicalEvent 流 → 协议特定的 SSE/JSON 格式
|
|
6
|
+
- install: 在 FastAPI app 上注册路由
|
|
7
|
+
|
|
8
|
+
ProtocolRegistry 管理多个 handler 的注册和查找。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import AsyncGenerator, Callable
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter
|
|
15
|
+
|
|
16
|
+
from wisruntime.model import AgentRequest, CanonicalEvent
|
|
17
|
+
from wisruntime.server.invoker import AgentInvoker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProtocolHandler(ABC):
|
|
21
|
+
"""协议处理器抽象基类。
|
|
22
|
+
|
|
23
|
+
每个协议 (Dify SSE, AG-UI, ...) 实现自己的 Handler,
|
|
24
|
+
负责请求解析和响应序列化。
|
|
25
|
+
|
|
26
|
+
install 时由 Server 注入 AgentInvoker 和用户函数 fn,
|
|
27
|
+
路由处理时可以调用 invoker.invoke(fn, request)。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_prefix(self) -> str:
|
|
32
|
+
"""返回此协议的路由前缀,如 '/dify/v1'。"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def parse_request(self, request) -> AgentRequest:
|
|
37
|
+
"""解析原始 HTTP 请求为归一化 AgentRequest。"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def _format_stream(
|
|
42
|
+
self,
|
|
43
|
+
events: AsyncGenerator[CanonicalEvent, None],
|
|
44
|
+
task_id: str,
|
|
45
|
+
) -> AsyncGenerator[str, None]:
|
|
46
|
+
"""将 CanonicalEvent 流格式化为协议特定的 SSE 字符串。"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def install(self, app, invoker: AgentInvoker, fn: Callable) -> None:
|
|
50
|
+
"""在 FastAPI app 上注册此协议的路由。
|
|
51
|
+
|
|
52
|
+
子类可以覆写此方法以自定义路由注册逻辑。
|
|
53
|
+
默认实现调用 _build_router()。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
app: FastAPI 应用实例。
|
|
57
|
+
invoker: AgentInvoker 实例,由 Server 注入。
|
|
58
|
+
fn: 用户的 invoke_agent 函数。
|
|
59
|
+
"""
|
|
60
|
+
self._invoker = invoker
|
|
61
|
+
self._fn = fn
|
|
62
|
+
router = self._build_router()
|
|
63
|
+
prefix = self.get_prefix()
|
|
64
|
+
app.include_router(router, prefix=prefix)
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def _build_router(self) -> APIRouter:
|
|
68
|
+
"""构建包含此协议路由的 APIRouter。"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ProtocolRegistry:
|
|
73
|
+
"""协议处理器注册中心。
|
|
74
|
+
|
|
75
|
+
管理多个 ProtocolHandler 的注册和查找。
|
|
76
|
+
|
|
77
|
+
Demo:
|
|
78
|
+
>>> registry = ProtocolRegistry()
|
|
79
|
+
>>> registry.register(DifyHandler())
|
|
80
|
+
>>> len(registry.handlers)
|
|
81
|
+
1
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self):
|
|
85
|
+
self._handlers: list[ProtocolHandler] = []
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def handlers(self) -> list[ProtocolHandler]:
|
|
89
|
+
return self._handlers
|
|
90
|
+
|
|
91
|
+
def register(self, handler: ProtocolHandler) -> None:
|
|
92
|
+
"""注册一个协议处理器。"""
|
|
93
|
+
self._handlers.append(handler)
|
|
94
|
+
|
|
95
|
+
def install_all(self, app, invoker: AgentInvoker, fn: Callable) -> None:
|
|
96
|
+
"""将所有已注册的 handler 安装到 FastAPI app 上。
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
app: FastAPI 应用实例。
|
|
100
|
+
invoker: AgentInvoker 实例。
|
|
101
|
+
fn: 用户的 invoke_agent 函数。
|
|
102
|
+
"""
|
|
103
|
+
for handler in self._handlers:
|
|
104
|
+
handler.install(app, invoker, fn)
|