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 ADDED
@@ -0,0 +1,91 @@
1
+ """wisruntime — 企业级 Agent Runtime SDK。
2
+
3
+ 让 AI 工程师用框架原生 API 构建 Agent,数行代码获得生产级服务端点。
4
+
5
+ Quick Start:
6
+ from wisruntime import create_server
7
+ from wisruntime.model import AgentRequest, Message
8
+
9
+ async def my_agent(request: AgentRequest):
10
+ '''用户自定义的 Agent 逻辑。yield str 即可,SDK 自动处理归一化和序列化。'''
11
+ async for chunk in llm.astream(messages):
12
+ yield chunk.content
13
+
14
+ create_server(my_agent).start(port=8000)
15
+ # → Dify SSE 服务就绪: POST /dify/v1/chat-messages
16
+ """
17
+
18
+ from wisruntime.config import Config, CredentialContext
19
+ from wisruntime.exceptions import ConfigError, InvokerError, ProtocolError, WisRuntimeError
20
+ from wisruntime.model import (
21
+ AgentRequest,
22
+ CustomEvent,
23
+ DoneEvent,
24
+ ErrorEvent,
25
+ FileOutputEvent,
26
+ Message,
27
+ ReasoningDeltaEvent,
28
+ SubAgentEndEvent,
29
+ SubAgentStartEvent,
30
+ TextDeltaEvent,
31
+ ToolCallEndEvent,
32
+ ToolCallStartEvent,
33
+ UsageEvent,
34
+ )
35
+
36
+ # Server 层延迟导入 — 避免在非 Web 场景拉动 fastapi/uvicorn
37
+ # 用户需要 Server 时显式导入: from wisruntime.server import create_server
38
+
39
+
40
+ def create_server(invoke_agent, *, protocols=None, config=None, checkpoint_enabled=False):
41
+ """创建 WisRuntimeServer 实例 (顶层入口 API)。
42
+
43
+ 接收用户编写的 invoke_agent 函数,返回可启动的 Server 实例。
44
+ Server 默认启用 DifyHandler,可通过 protocols 参数添加更多协议处理器。
45
+
46
+ Args:
47
+ invoke_agent: 用户函数,签名为 async def fn(request: AgentRequest) -> AsyncGenerator。
48
+ protocols: 协议处理器列表,默认 [DifyHandler()]。
49
+ config: ServerConfig 实例,配置 CORS、健康检查路径等。
50
+ checkpoint_enabled: 是否启用 Checkpoint 自动持久化 (Phase 3)。
51
+
52
+ Returns:
53
+ WisRuntimeServer 实例,调用 .start(port=8000) 或 .as_fastapi_app() 启动服务。
54
+
55
+ Demo:
56
+ >>> from wisruntime import create_server
57
+ >>> async def my_agent(request):
58
+ ... yield "你好,我是 AI 助手"
59
+ >>> server = create_server(my_agent)
60
+ >>> # server.start(port=8000)
61
+ """
62
+ from wisruntime.server.server import WisRuntimeServer, ServerConfig
63
+ from wisruntime.server.dify_handler import DifyHandler
64
+
65
+ _config = config or ServerConfig()
66
+ _protocols = protocols or [DifyHandler()]
67
+ return WisRuntimeServer(invoke_agent=invoke_agent, protocols=_protocols, config=_config)
68
+
69
+
70
+ __all__ = [
71
+ "create_server",
72
+ "Config",
73
+ "CredentialContext",
74
+ "AgentRequest",
75
+ "Message",
76
+ "TextDeltaEvent",
77
+ "ReasoningDeltaEvent",
78
+ "ToolCallStartEvent",
79
+ "ToolCallEndEvent",
80
+ "SubAgentStartEvent",
81
+ "SubAgentEndEvent",
82
+ "FileOutputEvent",
83
+ "UsageEvent",
84
+ "ErrorEvent",
85
+ "CustomEvent",
86
+ "DoneEvent",
87
+ "WisRuntimeError",
88
+ "ConfigError",
89
+ "ProtocolError",
90
+ "InvokerError",
91
+ ]
wisruntime/config.py ADDED
@@ -0,0 +1,119 @@
1
+ """全局配置与凭证上下文管理。
2
+
3
+ Config 提供三级 API-KEY 获取 (显式传入 > contextvars > 环境变量),
4
+ CredentialContext 基于 contextvars 实现请求级凭证注入 (Server 模式)。
5
+
6
+ 参考: AgentRun SDK 的 Config + CredentialContext 设计。
7
+ """
8
+
9
+ import os
10
+ from contextvars import ContextVar
11
+
12
+ # ============================================================================
13
+ # CredentialContext — 请求级 API-KEY 注入
14
+ # ============================================================================
15
+ # 基于 contextvars,每个异步请求有独立的上下文副本。
16
+ # 在 Server 模式下,ApiKeyMiddleware 从请求的 Bearer 头提取 API-KEY 并注入。
17
+ # 在本地开发模式下,contextvars 为空,回退到环境变量。
18
+
19
+ _request_api_key: ContextVar[str | None] = ContextVar("_request_api_key", default=None)
20
+
21
+
22
+ class CredentialContext:
23
+ """请求级凭证上下文管理器。
24
+
25
+ 提供 get/set 方法操作 contextvars 中的 API-KEY。
26
+ ApiKeyMiddleware 在每次 HTTP 请求时调用 set,请求结束时自动复位。
27
+
28
+ Demo:
29
+ >>> # Server 中间件注入
30
+ >>> token = CredentialContext.set_request_api_key("wisa-sk-xxx")
31
+ >>> CredentialContext.get_request_api_key()
32
+ 'wisa-sk-xxx'
33
+ >>> # 请求结束后 contextvars 自动复位到默认值 None
34
+ """
35
+
36
+ @staticmethod
37
+ def get_request_api_key() -> str | None:
38
+ """获取当前请求上下文中的 API-KEY。
39
+
40
+ 仅在 Server 模式的请求处理期间有效。
41
+ 本地开发时返回 None,由 Config 回退到环境变量。
42
+ """
43
+ return _request_api_key.get()
44
+
45
+ @staticmethod
46
+ def set_request_api_key(key: str | None) -> None:
47
+ """设置当前请求上下文中的 API-KEY。
48
+
49
+ 通常由 ApiKeyMiddleware 调用,注入从 HTTP 请求头解析的 API-KEY。
50
+ """
51
+ _request_api_key.set(key)
52
+
53
+
54
+ # ============================================================================
55
+ # Config — 全局配置
56
+ # ============================================================================
57
+
58
+
59
+ class Config:
60
+ """SDK 全局配置。
61
+
62
+ 管理 API-KEY、WIS Hub 端点、超时等全局设置。
63
+ 凭证获取遵循三级优先级: 显式传入 → contextvars → 环境变量。
64
+
65
+ Demo:
66
+ >>> # 方式 1: 显式传入 (本地开发)
67
+ >>> config = Config(api_key="wisa-sk-xxx", hub_endpoint="https://hub.example.com")
68
+ >>> # 方式 2: 环境变量 (容器部署)
69
+ >>> # export WIS_HUB_API_KEY="wisa-sk-xxx"
70
+ >>> config = Config() # 自动从环境变量读取
71
+ >>> config.api_key
72
+ 'wisa-sk-xxx'
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ api_key: str | None = None,
78
+ hub_endpoint: str | None = None,
79
+ timeout: int = 600,
80
+ ):
81
+ """初始化 Config。
82
+
83
+ Args:
84
+ api_key: 统一 API-KEY。None 表示从环境变量或 contextvars 获取。
85
+ hub_endpoint: WIS Hub 地址。None 时使用默认内置地址。
86
+ timeout: HTTP 请求超时秒数,默认 600 秒。
87
+ """
88
+ self._api_key = api_key
89
+ self._hub_endpoint = hub_endpoint
90
+ self.timeout = timeout
91
+
92
+ @property
93
+ def api_key(self) -> str | None:
94
+ """获取 API-KEY,按三级优先级: 显式传入 > contextvars > 环境变量。"""
95
+ if self._api_key:
96
+ return self._api_key
97
+ ctx_key = CredentialContext.get_request_api_key()
98
+ if ctx_key:
99
+ return ctx_key
100
+ return os.environ.get("WIS_HUB_API_KEY")
101
+
102
+ @property
103
+ def hub_endpoint(self) -> str:
104
+ """获取 WIS Hub 地址。"""
105
+ if self._hub_endpoint:
106
+ return self._hub_endpoint
107
+ return os.environ.get("WIS_HUB_ENDPOINT", "https://hub.wis.example.com")
108
+
109
+ def with_api_key(self, api_key: str) -> "Config":
110
+ """创建一个携带指定 API-KEY 的新 Config 实例 (不影响原实例)。
111
+
112
+ Demo:
113
+ >>> config = Config()
114
+ >>> config2 = config.with_api_key("custom-key")
115
+ >>> config2.api_key
116
+ 'custom-key'
117
+ """
118
+ new = Config(api_key=api_key, hub_endpoint=self._hub_endpoint, timeout=self.timeout)
119
+ return new
@@ -0,0 +1,56 @@
1
+ """自定义异常定义。
2
+
3
+ wisruntime 内部使用的异常类型,帮助区分 SDK 内部错误和用户代码错误。
4
+ """
5
+
6
+
7
+ class WisRuntimeError(Exception):
8
+ """wisruntime 基础异常。
9
+
10
+ 所有 SDK 内部抛出的异常都继承自此类型,方便用户统一捕获。
11
+
12
+ Demo:
13
+ >>> try:
14
+ ... raise WisRuntimeError("配置错误")
15
+ ... except WisRuntimeError as e:
16
+ ... print(e)
17
+ 配置错误
18
+ """
19
+
20
+ pass
21
+
22
+
23
+ class ConfigError(WisRuntimeError):
24
+ """配置相关错误。
25
+
26
+ 当必需的配置项缺失或无效时抛出 (如未设置 API-KEY 却尝试创建 MCPClient)。
27
+
28
+ Demo:
29
+ >>> raise ConfigError("缺少 WIS_HUB_API_KEY,请设置环境变量或显式传入 api_key")
30
+ """
31
+
32
+ pass
33
+
34
+
35
+ class ProtocolError(WisRuntimeError):
36
+ """协议解析/序列化错误。
37
+
38
+ 当协议请求格式不正确或序列化失败时抛出。
39
+
40
+ Demo:
41
+ >>> raise ProtocolError("Dify 请求缺少必需的 query 字段")
42
+ """
43
+
44
+ pass
45
+
46
+
47
+ class InvokerError(WisRuntimeError):
48
+ """AgentInvoker 调用错误。
49
+
50
+ 当用户函数不符合预期格式时抛出。
51
+
52
+ Demo:
53
+ >>> raise InvokerError("invoke_agent 函数不能是同步生成器,请使用异步生成器 (async def)")
54
+ """
55
+
56
+ pass
wisruntime/model.py ADDED
@@ -0,0 +1,326 @@
1
+ """wisruntime 核心数据模型。
2
+
3
+ 定义整个 SDK 的稳定数据契约:
4
+ - CanonicalEvent: 框架事件与协议事件之间的中间表示 (11 种类型)
5
+ - AgentRequest: 协议无关的归一化请求模型
6
+ - Message: 归一化消息模型
7
+
8
+ 这些类型是所有 Adapter (产出端) 和 ProtocolHandler (消费端) 的唯一契约。
9
+ """
10
+
11
+ from typing import Annotated, Any, Literal
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ # ============================================================================
17
+ # CanonicalEvent — 11 种事件类型
18
+ # ============================================================================
19
+ # 设计参考: LangChain v2/v3, Dify/Wisknow, AgentRun 四套事件体系。
20
+ # 使用 Pydantic discriminated union 实现类型安全的事件分发。
21
+
22
+
23
+ class TextDeltaEvent(BaseModel):
24
+ """文本增量事件。
25
+
26
+ 对应模型流式输出的内容增量。用户 yield 的 str 会被 AgentInvoker 自动转换为此类型。
27
+
28
+ Demo:
29
+ >>> event = TextDeltaEvent(type="text_delta", delta="你好")
30
+ >>> event.model_dump_json()
31
+ '{"type":"text_delta","delta":"你好"}'
32
+ """
33
+
34
+ type: Literal["text_delta"] = "text_delta"
35
+ delta: str
36
+
37
+
38
+ class ReasoningDeltaEvent(BaseModel):
39
+ """推理增量事件 (Chain-of-Thought)。
40
+
41
+ 对应模型的思维链输出,与正文内容分开传输。在 Dify 协议中映射为 reasoning_chunk。
42
+
43
+ Demo:
44
+ >>> event = ReasoningDeltaEvent(type="reasoning_delta", delta="让我想一想...")
45
+ >>> event.delta
46
+ '让我想一想...'
47
+ """
48
+
49
+ type: Literal["reasoning_delta"] = "reasoning_delta"
50
+ delta: str
51
+
52
+
53
+ class ToolCallStartEvent(BaseModel):
54
+ """工具调用开始事件。
55
+
56
+ 工具调用的参数可能分块流式到达,args_delta 是增量而非完整参数。
57
+ tool_call_id 由框架或 Adapter 提供,用于和后续 ToolCallEnd 配对。
58
+
59
+ Demo:
60
+ >>> event = ToolCallStartEvent(
61
+ ... type="tool_call_start",
62
+ ... tool_call_id="call_001",
63
+ ... name="search",
64
+ ... args_delta='{"query": "天气"}'
65
+ ... )
66
+ >>> event.tool_call_id
67
+ 'call_001'
68
+ """
69
+
70
+ type: Literal["tool_call_start"] = "tool_call_start"
71
+ tool_call_id: str
72
+ name: str
73
+ args_delta: str = ""
74
+
75
+
76
+ class ToolCallEndEvent(BaseModel):
77
+ """工具调用结束事件。
78
+
79
+ 携带工具执行结果或错误信息。tool_call_id 与对应的 ToolCallStart 配对。
80
+ result 和 error 互斥: 成功时 result 有值,失败时 error 有值。
81
+
82
+ Demo:
83
+ >>> # 成功场景
84
+ >>> event = ToolCallEndEvent(
85
+ ... type="tool_call_end",
86
+ ... tool_call_id="call_001",
87
+ ... result={"temperature": 25},
88
+ ... )
89
+ >>> # 失败场景
90
+ >>> event = ToolCallEndEvent(
91
+ ... type="tool_call_end",
92
+ ... tool_call_id="call_002",
93
+ ... error="Tool execution timeout",
94
+ ... )
95
+ """
96
+
97
+ type: Literal["tool_call_end"] = "tool_call_end"
98
+ tool_call_id: str
99
+ result: Any | None = None
100
+ error: str | None = None
101
+
102
+
103
+ class SubAgentStartEvent(BaseModel):
104
+ """子 Agent 开始运行事件。
105
+
106
+ 对应 Wisknow/灵知 平台的子代理场景。主 Agent 委派任务给子 Agent 时触发。
107
+
108
+ Demo:
109
+ >>> event = SubAgentStartEvent(
110
+ ... type="subagent_start",
111
+ ... agent_name="健康顾问",
112
+ ... run_id="run-abc-123",
113
+ ... task_description="分析用户的健康数据并给出建议",
114
+ ... )
115
+ """
116
+
117
+ type: Literal["subagent_start"] = "subagent_start"
118
+ agent_name: str
119
+ run_id: str
120
+ task_description: str = ""
121
+
122
+
123
+ class SubAgentEndEvent(BaseModel):
124
+ """子 Agent 运行结束事件。
125
+
126
+ run_id 与对应的 SubAgentStart 配对。status 为 "completed" 或 "failed"。
127
+
128
+ Demo:
129
+ >>> event = SubAgentEndEvent(
130
+ ... type="subagent_end",
131
+ ... agent_name="健康顾问",
132
+ ... run_id="run-abc-123",
133
+ ... status="completed",
134
+ ... )
135
+ """
136
+
137
+ type: Literal["subagent_end"] = "subagent_end"
138
+ agent_name: str
139
+ run_id: str
140
+ status: Literal["completed", "failed"]
141
+
142
+
143
+ class FileOutputEvent(BaseModel):
144
+ """文件输出事件。
145
+
146
+ 对应多模态输出: 图片、音频、视频、文件等。
147
+
148
+ Demo:
149
+ >>> event = FileOutputEvent(
150
+ ... type="file_output",
151
+ ... file_id="file-001",
152
+ ... file_type="image",
153
+ ... url="https://cdn.example.com/generated.png",
154
+ ... belongs_to="assistant",
155
+ ... )
156
+ """
157
+
158
+ type: Literal["file_output"] = "file_output"
159
+ file_id: str
160
+ file_type: str
161
+ url: str
162
+ belongs_to: Literal["user", "assistant"]
163
+
164
+
165
+ class UsageEvent(BaseModel):
166
+ """Token 用量统计事件。
167
+
168
+ 通常在流结束前发送,提供本次调用的 token 消耗统计。
169
+
170
+ Demo:
171
+ >>> event = UsageEvent(type="usage", prompt_tokens=100, completion_tokens=50, total_tokens=150)
172
+ """
173
+
174
+ type: Literal["usage"] = "usage"
175
+ prompt_tokens: int = 0
176
+ completion_tokens: int = 0
177
+ total_tokens: int = 0
178
+
179
+
180
+ class ErrorEvent(BaseModel):
181
+ """错误事件。
182
+
183
+ 出现此事件后流应立即终止。recoverable 标记错误是否可通过重试解决。
184
+
185
+ Demo:
186
+ >>> # 可恢复错误 (如暂时网络问题)
187
+ >>> event = ErrorEvent(type="error", message="网络超时", code="TIMEOUT", recoverable=True)
188
+ >>> # 不可恢复错误 (如参数校验失败)
189
+ >>> event = ErrorEvent(type="error", message="无效的输入", code="INVALID_INPUT", recoverable=False)
190
+ """
191
+
192
+ type: Literal["error"] = "error"
193
+ message: str
194
+ code: str | None = None
195
+ recoverable: bool = False
196
+
197
+
198
+ class CustomEvent(BaseModel):
199
+ """自定义事件 (扩展通道)。
200
+
201
+ 用于框架特有或应用特有的事件类型,不需要修改 CanonicalEvent 体系。
202
+ 对应 LangChain v2 的 on_custom_event 和灵知平台的 todo_update / recommendation_card 等。
203
+
204
+ Demo:
205
+ >>> event = CustomEvent(
206
+ ... type="custom",
207
+ ... name="todo_update",
208
+ ... payload={"todos": [{"id": "1", "content": "搜索数据", "status": "in_progress"}]},
209
+ ... )
210
+ """
211
+
212
+ type: Literal["custom"] = "custom"
213
+ name: str
214
+ payload: dict = Field(default_factory=dict)
215
+
216
+
217
+ class DoneEvent(BaseModel):
218
+ """流结束标记事件。
219
+
220
+ 由 AgentInvoker 在用户函数正常退出时自动插入。
221
+ 协议层 (如 DifyHandler) 根据此事件生成对应的流结束信号 (如 message_end)。
222
+
223
+ Demo:
224
+ >>> event = DoneEvent(type="done", metadata={"total_tokens": 150})
225
+ """
226
+
227
+ type: Literal["done"] = "done"
228
+ metadata: dict = Field(default_factory=dict)
229
+
230
+
231
+ # ============================================================================
232
+ # Discriminated Union — 类型安全的事件分发
233
+ # ============================================================================
234
+ # 通过 Literal discriminator,Pydantic 可以根据 type 字段自动解析为正确的子类。
235
+ # 消费端 (ProtocolHandler) 可以用 match/case 或 isinstance 来分发处理。
236
+
237
+ CanonicalEvent = Annotated[
238
+ TextDeltaEvent
239
+ | ReasoningDeltaEvent
240
+ | ToolCallStartEvent
241
+ | ToolCallEndEvent
242
+ | SubAgentStartEvent
243
+ | SubAgentEndEvent
244
+ | FileOutputEvent
245
+ | UsageEvent
246
+ | ErrorEvent
247
+ | CustomEvent
248
+ | DoneEvent,
249
+ Field(discriminator="type"),
250
+ ]
251
+ """CanonicalEvent — wisruntime 的核心数据契约。
252
+
253
+ 所有 Adapter 产出此类型,所有 ProtocolHandler 消费此类型。
254
+ 11 种事件覆盖: 内容流 (Text/Reasoning/File), 工具调用生命周期,
255
+ 子 Agent 生命周期, 元事件 (Usage/Error/Custom/Done)。
256
+
257
+ 消费方式:
258
+ async for event in generator:
259
+ match event.type:
260
+ case "text_delta": process_text(event.delta)
261
+ case "reasoning_delta": process_reasoning(event.delta)
262
+ case "tool_call_start": process_tool_start(event)
263
+ case "done": break
264
+ """
265
+
266
+
267
+ # ============================================================================
268
+ # AgentRequest + Message — 归一化请求模型
269
+ # ============================================================================
270
+
271
+
272
+ class Message(BaseModel):
273
+ """归一化消息模型。
274
+
275
+ 协议无关的消息表示,对应 OpenAI chat message 格式。
276
+
277
+ Demo:
278
+ >>> msg = Message(role="user", content="你好")
279
+ >>> msg = Message(role="assistant", content=[{"type": "text", "text": "你好!"}])
280
+ """
281
+
282
+ role: Literal["system", "user", "assistant", "tool"]
283
+ content: str | list[dict]
284
+ """文本字符串或多模态内容块列表。"""
285
+
286
+ name: str | None = None
287
+
288
+
289
+ class AgentRequest(BaseModel):
290
+ """协议无关的归一化请求模型。
291
+
292
+ 各 ProtocolHandler (Dify/AGUI/...) 将各自的请求格式解析为此模型,
293
+ 然后传给 AgentInvoker 调用用户函数。
294
+
295
+ Demo:
296
+ >>> request = AgentRequest(
297
+ ... messages=[Message(role="user", content="今天天气怎么样?")],
298
+ ... stream=True,
299
+ ... conversation_id="conv-001",
300
+ ... user_id="user-001",
301
+ ... headers={"authorization": "Bearer xxx"},
302
+ ... )
303
+ """
304
+
305
+ model_config = {"arbitrary_types_allowed": True}
306
+
307
+ messages: list[Message]
308
+ """归一化后的消息列表。由 ProtocolHandler 从原始请求中提取。"""
309
+
310
+ stream: bool = True
311
+ """是否流式输出。True = SSE Streaming, False = 阻塞模式返回完整结果。"""
312
+
313
+ conversation_id: str | None = None
314
+ """会话 ID,用于多轮对话上下文关联和 Checkpoint 恢复。"""
315
+
316
+ user_id: str | None = None
317
+ """用户标识,用于审计和个性化。"""
318
+
319
+ headers: dict[str, str] = Field(default_factory=dict)
320
+ """关键 HTTP 请求头的副本,可安全跨线程传递。"""
321
+
322
+ raw_request: Any | None = None
323
+ """原始 Starlette Request 对象。仅在本地开发时可用,不能跨线程传递。"""
324
+
325
+ protocol_extensions: dict = Field(default_factory=dict)
326
+ """协议特有字段。如 Dify 的 inputs, files 等。由各 ProtocolHandler 解析后放入此字典。"""
File without changes