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.
@@ -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)