planify 1.0.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.
- planify/__init__.py +7 -0
- planify/agent/__init__.py +5 -0
- planify/agent/runner.py +357 -0
- planify/bootstrap.py +313 -0
- planify/cli.py +527 -0
- planify/cli_history.py +498 -0
- planify/context/__init__.py +5 -0
- planify/context/compact.py +201 -0
- planify/core/__init__.py +22 -0
- planify/core/client.py +38 -0
- planify/core/config.py +274 -0
- planify/core/encoding.py +140 -0
- planify/core/llm/__init__.py +24 -0
- planify/core/llm/anthropic_provider.py +329 -0
- planify/core/llm/errors.py +35 -0
- planify/core/llm/factory.py +23 -0
- planify/core/llm/openai_compat_provider.py +266 -0
- planify/core/llm/presets.py +26 -0
- planify/core/llm/provider.py +61 -0
- planify/core/llm/tool_translator.py +149 -0
- planify/core/llm/types.py +88 -0
- planify/core/logging_config.py +130 -0
- planify/core/runtime.py +262 -0
- planify/core/runtime_manager.py +472 -0
- planify/main.py +324 -0
- planify/managers/__init__.py +8 -0
- planify/managers/background_manager.py +187 -0
- planify/managers/task_manager.py +178 -0
- planify/managers/teammate_manager.py +441 -0
- planify/managers/todo_manager.py +106 -0
- planify/messaging/__init__.py +5 -0
- planify/messaging/message_bus.py +186 -0
- planify/prompts.py +255 -0
- planify/skills/__init__.py +5 -0
- planify/skills/access_state.py +85 -0
- planify/skills/skill_loader.py +87 -0
- planify/streaming/__init__.py +38 -0
- planify/streaming/emitter.py +403 -0
- planify/streaming/runner.py +770 -0
- planify/streaming/types.py +335 -0
- planify/streaming/waiter.py +374 -0
- planify/subagent/__init__.py +5 -0
- planify/subagent/runner.py +234 -0
- planify/tools/__init__.py +16 -0
- planify/tools/baidu_weather.py +447 -0
- planify/tools/basic.py +469 -0
- planify/tools/file_tasks.py +93 -0
- planify/tools/lunar.py +180 -0
- planify/tools/protocols.py +135 -0
- planify/tools/registry.py +446 -0
- planify/tools/team_tools.py +91 -0
- planify/tools/user_interaction.py +511 -0
- planify/tools/weather_tool.py +88 -0
- planify/tools/web.py +262 -0
- planify/tools/webfetch.py +379 -0
- planify-1.0.0.dist-info/METADATA +268 -0
- planify-1.0.0.dist-info/RECORD +60 -0
- planify-1.0.0.dist-info/WHEEL +5 -0
- planify-1.0.0.dist-info/entry_points.txt +2 -0
- planify-1.0.0.dist-info/top_level.txt +1 -0
planify/__init__.py
ADDED
planify/agent/runner.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
代理运行器 - 核心代理循环 (s01)
|
|
5
|
+
|
|
6
|
+
提供代理主循环和系统提示词生成。
|
|
7
|
+
持续调用 LLM 并执行工具,直到模型停止调用工具。
|
|
8
|
+
支持多用户多会话架构。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import dataclasses
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from ..core.llm.types import TextBlock, Tool, ToolUseBlock
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Agent:
|
|
21
|
+
"""
|
|
22
|
+
代理类 - 管理代理状态和执行循环。
|
|
23
|
+
|
|
24
|
+
代理循环包含:
|
|
25
|
+
1. 微压缩 (s06) - 清理旧的 tool_result
|
|
26
|
+
2. 自动压缩检查 (s06) - 超过阈值时压缩上下文
|
|
27
|
+
3. 后台通知处理 (s08) - 获取已完成的后台任务
|
|
28
|
+
4. 收件箱检查 (s09) - 读取队友消息
|
|
29
|
+
5. LLM 调用
|
|
30
|
+
6. 工具执行
|
|
31
|
+
7. Todo 提醒检查 (s03) - 3 轮未更新后提醒
|
|
32
|
+
|
|
33
|
+
关键洞察:"整个秘密就是一个模式:while stop_reason == 'tool_use'"
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
client: Any,
|
|
39
|
+
model: str,
|
|
40
|
+
tools: List[Dict],
|
|
41
|
+
tool_handlers: Dict[str, Any],
|
|
42
|
+
todo_manager: Any,
|
|
43
|
+
bg_manager: Any,
|
|
44
|
+
bus: Any,
|
|
45
|
+
skills_loader: Any,
|
|
46
|
+
config: Dict[str, Any],
|
|
47
|
+
logger: Any,
|
|
48
|
+
runtime: Optional[Any] = None,
|
|
49
|
+
tool_callback: Optional[callable] = None,
|
|
50
|
+
tool_result_callback: Optional[callable] = None,
|
|
51
|
+
system_prompt_extra: Optional[str] = None,
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
初始化代理。
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
client: Anthropic API 客户端
|
|
58
|
+
model: 模型名称
|
|
59
|
+
tools: 工具定义列表
|
|
60
|
+
tool_handlers: 工具处理器字典
|
|
61
|
+
todo_manager: Todo 管理器
|
|
62
|
+
bg_manager: 后台管理器
|
|
63
|
+
bus: 消息总线
|
|
64
|
+
skills_loader: 技能加载器
|
|
65
|
+
config: 配置字典
|
|
66
|
+
logger: 日志记录器
|
|
67
|
+
runtime: AgentRuntime 实例(可选)
|
|
68
|
+
tool_callback: 工具调用回调函数 (name, args) -> None
|
|
69
|
+
tool_result_callback: 工具结果回调函数 (name, result) -> None
|
|
70
|
+
system_prompt_extra: 宿主应用注入的额外 system prompt 段(可选)
|
|
71
|
+
"""
|
|
72
|
+
# provider 是 client 的别名(LLMProvider 抽象接口),
|
|
73
|
+
# 所有调用点(包括 compact)已迁移完成。
|
|
74
|
+
self.provider = client
|
|
75
|
+
self.model = model
|
|
76
|
+
self.tools = tools
|
|
77
|
+
self.tool_handlers = tool_handlers
|
|
78
|
+
self.todo_mgr = todo_manager
|
|
79
|
+
self.bg_manager = bg_manager
|
|
80
|
+
self.bus = bus
|
|
81
|
+
self.skills = skills_loader
|
|
82
|
+
self.config = config
|
|
83
|
+
self.logger = logger
|
|
84
|
+
self.runtime = runtime
|
|
85
|
+
self.tool_callback = tool_callback
|
|
86
|
+
self.tool_result_callback = tool_result_callback
|
|
87
|
+
self._system_prompt_extra = system_prompt_extra
|
|
88
|
+
|
|
89
|
+
# 延迟导入以避免循环依赖(使用相对导入)
|
|
90
|
+
from ..context import estimate_tokens, microcompact, auto_compact
|
|
91
|
+
from ..context.compact import MICROCOMPACT_GATE_RATIO
|
|
92
|
+
from ..prompts import SystemPromptBuilder
|
|
93
|
+
|
|
94
|
+
self._estimate_tokens = estimate_tokens
|
|
95
|
+
self._microcompact = microcompact
|
|
96
|
+
self._auto_compact = auto_compact
|
|
97
|
+
self._microcompact_gate_ratio = MICROCOMPACT_GATE_RATIO
|
|
98
|
+
self._prompt_builder = SystemPromptBuilder()
|
|
99
|
+
|
|
100
|
+
def get_system_prompt(self) -> str:
|
|
101
|
+
"""
|
|
102
|
+
获取系统提示词。
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
系统提示词字符串
|
|
106
|
+
"""
|
|
107
|
+
workdir = self.config.get("workdir", ".")
|
|
108
|
+
return self._prompt_builder.get(
|
|
109
|
+
workdir, agent_type="agent", extra_prompt=self._system_prompt_extra
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def run(self, messages: List[Dict]) -> None:
|
|
113
|
+
"""
|
|
114
|
+
运行代理循环。
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
messages: 消息历史列表(将被就地修改)
|
|
118
|
+
"""
|
|
119
|
+
system = self.get_system_prompt()
|
|
120
|
+
rounds_without_todo = 0
|
|
121
|
+
loop_count = 0
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
loop_count += 1
|
|
125
|
+
|
|
126
|
+
# === 日志记录 ===
|
|
127
|
+
try:
|
|
128
|
+
msg_json = json.dumps(messages[-3:], ensure_ascii=False, default=str)
|
|
129
|
+
self.logger.info(f"[LLM Call #{loop_count}] Input messages: {msg_json}")
|
|
130
|
+
except Exception:
|
|
131
|
+
self.logger.info(f"[LLM Call #{loop_count}] Input messages: (encoding error)")
|
|
132
|
+
|
|
133
|
+
# === s06: 压缩管道 ===
|
|
134
|
+
# 缓存友好:清理推迟到逼近 auto_compact 阈值(默认 80%)才触发,
|
|
135
|
+
# 避免历史中段单点突变打废整体前缀缓存(国产端点双倍代价)
|
|
136
|
+
self._microcompact(
|
|
137
|
+
messages,
|
|
138
|
+
min_estimated_tokens=int(
|
|
139
|
+
self.config["token_threshold"] * self._microcompact_gate_ratio
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
if self._estimate_tokens(messages) > self.config["token_threshold"]:
|
|
143
|
+
# 压缩 transcript 目录:注意 config["transcript_dir"] 是
|
|
144
|
+
# .planify/transcript.json 文件路径(语义不符),统一用
|
|
145
|
+
# <workdir>/.transcripts/
|
|
146
|
+
transcript_dir = Path(self.config.get("workdir", ".")) / ".transcripts"
|
|
147
|
+
compacted = self._auto_compact(messages, self.provider, transcript_dir)
|
|
148
|
+
|
|
149
|
+
# 本地列表必须就地替换(runtime 路径下两者可能是不同列表);
|
|
150
|
+
# 若本就是同一列表,二次替换幂等无害
|
|
151
|
+
messages[:] = compacted
|
|
152
|
+
if self.runtime:
|
|
153
|
+
self.runtime.replace_messages_in_place(compacted)
|
|
154
|
+
|
|
155
|
+
# === s08: 后台通知 ===
|
|
156
|
+
notifs = self.bg_manager.drain()
|
|
157
|
+
if notifs:
|
|
158
|
+
txt = "\n".join(
|
|
159
|
+
f"[bg:{n['task_id']}] {n['status']}: {n['result']}"
|
|
160
|
+
for n in notifs
|
|
161
|
+
)
|
|
162
|
+
messages.append({
|
|
163
|
+
"role": "user",
|
|
164
|
+
"content": f"<background-results>\n{txt}\n</background-results>"
|
|
165
|
+
})
|
|
166
|
+
messages.append({
|
|
167
|
+
"role": "assistant",
|
|
168
|
+
"content": "Noted background results."
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
# === s09: 检查 lead 收件箱 ===
|
|
172
|
+
inbox = self.bus.read_inbox("lead")
|
|
173
|
+
if inbox:
|
|
174
|
+
messages.append({
|
|
175
|
+
"role": "user",
|
|
176
|
+
"content": f"<inbox>{json.dumps(inbox, indent=2)}</inbox>"
|
|
177
|
+
})
|
|
178
|
+
messages.append({
|
|
179
|
+
"role": "assistant",
|
|
180
|
+
"content": "Noted inbox messages."
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
# === LLM 调用 ===
|
|
184
|
+
response = self.provider.chat(
|
|
185
|
+
messages=messages,
|
|
186
|
+
system=system,
|
|
187
|
+
tools=[
|
|
188
|
+
Tool(
|
|
189
|
+
name=t["name"],
|
|
190
|
+
description=t.get("description", ""),
|
|
191
|
+
input_schema=t.get("input_schema", {"type": "object"}),
|
|
192
|
+
)
|
|
193
|
+
for t in self.tools
|
|
194
|
+
],
|
|
195
|
+
max_tokens=8000,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# === 记录响应 ===
|
|
199
|
+
self.logger.info(f"[LLM Call #{loop_count}] Stop reason: {response.stop_reason}")
|
|
200
|
+
try:
|
|
201
|
+
resp_json = json.dumps(
|
|
202
|
+
[b.model_dump() if hasattr(b, 'model_dump') else str(b) for b in response.content],
|
|
203
|
+
ensure_ascii=False
|
|
204
|
+
)
|
|
205
|
+
self.logger.debug(f"[LLM Call #{loop_count}] Response: {resp_json[:2000]}")
|
|
206
|
+
except Exception:
|
|
207
|
+
self.logger.debug(f"[LLM Call #{loop_count}] Response: (encoding error)")
|
|
208
|
+
|
|
209
|
+
messages.append({
|
|
210
|
+
"role": "assistant",
|
|
211
|
+
"content": [
|
|
212
|
+
b if isinstance(b, dict) else dataclasses.asdict(b)
|
|
213
|
+
for b in response.content
|
|
214
|
+
],
|
|
215
|
+
})
|
|
216
|
+
if response.stop_reason != "tool_use":
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# === 工具执行 ===
|
|
220
|
+
results = []
|
|
221
|
+
used_todo = False
|
|
222
|
+
manual_compress = False
|
|
223
|
+
|
|
224
|
+
for block in response.content:
|
|
225
|
+
if isinstance(block, ToolUseBlock) or (
|
|
226
|
+
isinstance(block, dict) and block.get("type") == "tool_use"
|
|
227
|
+
):
|
|
228
|
+
# 触发工具调用回调(用于 CLI 输出)
|
|
229
|
+
if self.tool_callback:
|
|
230
|
+
try:
|
|
231
|
+
self.tool_callback(block.name, block.input)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.logger.debug(f"tool_callback 失败: {block.name}, {e}")
|
|
234
|
+
|
|
235
|
+
# 记录工具调用
|
|
236
|
+
try:
|
|
237
|
+
input_json = json.dumps(block.input, ensure_ascii=False)
|
|
238
|
+
self.logger.info(f"[Tool Call] {block.name} | Input: {input_json}")
|
|
239
|
+
except Exception:
|
|
240
|
+
self.logger.info(f"[Tool Call] {block.name} | Input: (encoding error)")
|
|
241
|
+
|
|
242
|
+
# 检测手动压缩请求
|
|
243
|
+
if block.name == "compress":
|
|
244
|
+
manual_compress = True
|
|
245
|
+
|
|
246
|
+
# 执行工具
|
|
247
|
+
handler = self.tool_handlers.get(block.name)
|
|
248
|
+
try:
|
|
249
|
+
if handler:
|
|
250
|
+
if asyncio.iscoroutinefunction(handler):
|
|
251
|
+
output = asyncio.run(handler(**block.input))
|
|
252
|
+
else:
|
|
253
|
+
output = handler(**block.input)
|
|
254
|
+
else:
|
|
255
|
+
output = f"Unknown tool: {block.name}"
|
|
256
|
+
except Exception as e:
|
|
257
|
+
output = f"Error: {e}"
|
|
258
|
+
|
|
259
|
+
# 记录工具结果
|
|
260
|
+
try:
|
|
261
|
+
output_str = str(output)
|
|
262
|
+
self.logger.info(f"[Tool Result] {block.name} | Output: {output_str[:500]}")
|
|
263
|
+
except Exception:
|
|
264
|
+
self.logger.info(f"[Tool Result] {block.name} | Output: (encoding error)")
|
|
265
|
+
|
|
266
|
+
# 触发工具结果回调
|
|
267
|
+
if self.tool_result_callback:
|
|
268
|
+
try:
|
|
269
|
+
self.tool_result_callback(block.name, str(output))
|
|
270
|
+
except Exception as e:
|
|
271
|
+
self.logger.debug(f"tool_result_callback 失败: {block.name}, {e}")
|
|
272
|
+
|
|
273
|
+
results.append({
|
|
274
|
+
"type": "tool_result",
|
|
275
|
+
"tool_use_id": block.id,
|
|
276
|
+
"content": str(output)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
if block.name == "TodoWrite":
|
|
280
|
+
used_todo = True
|
|
281
|
+
|
|
282
|
+
# === s03: Todo 提醒 ===
|
|
283
|
+
rounds_without_todo = 0 if used_todo else rounds_without_todo + 1
|
|
284
|
+
if self.todo_mgr.has_open_items() and rounds_without_todo >= 3:
|
|
285
|
+
results.insert(0, {"type": "text", "text": "<reminder>Update your todos.</reminder>"})
|
|
286
|
+
|
|
287
|
+
messages.append({"role": "user", "content": results})
|
|
288
|
+
|
|
289
|
+
# === s06: 手动压缩 ===
|
|
290
|
+
if manual_compress:
|
|
291
|
+
transcript_dir = Path(self.config.get("workdir", ".")) / ".transcripts"
|
|
292
|
+
compacted = self._auto_compact(messages, self.provider, transcript_dir)
|
|
293
|
+
|
|
294
|
+
# 同上:本地列表 + runtime 双写(同一列表时幂等)
|
|
295
|
+
messages[:] = compacted
|
|
296
|
+
if self.runtime:
|
|
297
|
+
self.runtime.replace_messages_in_place(compacted)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def has_runtime(self) -> bool:
|
|
301
|
+
"""是否绑定了 AgentRuntime"""
|
|
302
|
+
return self.runtime is not None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_agent_loop(
|
|
306
|
+
messages: List[Dict],
|
|
307
|
+
client: Any,
|
|
308
|
+
model: str,
|
|
309
|
+
tools: List[Dict],
|
|
310
|
+
tool_handlers: Dict[str, Any],
|
|
311
|
+
todo_manager: Any,
|
|
312
|
+
bg_manager: Any,
|
|
313
|
+
bus: Any,
|
|
314
|
+
skills_loader: Any,
|
|
315
|
+
config: Dict[str, Any],
|
|
316
|
+
logger: Any,
|
|
317
|
+
runtime: Optional[Any] = None,
|
|
318
|
+
tool_callback: Optional[callable] = None,
|
|
319
|
+
tool_result_callback: Optional[callable] = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""
|
|
322
|
+
运行代理循环(函数式接口)。
|
|
323
|
+
|
|
324
|
+
此函数持续调用 LLM 并执行工具,直到模型停止调用工具。
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
messages: 消息历史列表(将被就地修改)
|
|
328
|
+
client: Anthropic API 客户端
|
|
329
|
+
model: 模型名称
|
|
330
|
+
tools: 工具定义列表
|
|
331
|
+
tool_handlers: 工具处理器字典
|
|
332
|
+
todo_manager: Todo 管理器
|
|
333
|
+
bg_manager: 后台管理器
|
|
334
|
+
bus: 消息总线
|
|
335
|
+
skills_loader: 技能加载器
|
|
336
|
+
config: 配置字典
|
|
337
|
+
logger: 日志记录器
|
|
338
|
+
runtime: AgentRuntime 实例(可选)
|
|
339
|
+
tool_callback: 工具调用回调函数 (name, args) -> None
|
|
340
|
+
tool_result_callback: 工具结果回调函数 (name, result) -> None
|
|
341
|
+
"""
|
|
342
|
+
agent = Agent(
|
|
343
|
+
client=client,
|
|
344
|
+
model=model,
|
|
345
|
+
tools=tools,
|
|
346
|
+
tool_handlers=tool_handlers,
|
|
347
|
+
todo_manager=todo_manager,
|
|
348
|
+
bg_manager=bg_manager,
|
|
349
|
+
bus=bus,
|
|
350
|
+
skills_loader=skills_loader,
|
|
351
|
+
config=config,
|
|
352
|
+
logger=logger,
|
|
353
|
+
runtime=runtime,
|
|
354
|
+
tool_callback=tool_callback,
|
|
355
|
+
tool_result_callback=tool_result_callback,
|
|
356
|
+
)
|
|
357
|
+
agent.run(messages)
|
planify/bootstrap.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
应用初始化模块
|
|
5
|
+
|
|
6
|
+
负责初始化 RuntimeManager 并提供运行时(AgentRuntime)管理接口。
|
|
7
|
+
不再使用全局单例模式,支持多用户并发访问。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from .core import RuntimeManager, get_config, get_user_config_dict
|
|
14
|
+
from .subagent.runner import run_subagent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# RuntimeManager 单例(通过 get_instance() 访问)
|
|
18
|
+
_manager: Optional[RuntimeManager] = None
|
|
19
|
+
|
|
20
|
+
# Planify 配置注册表(由主应用注入)
|
|
21
|
+
_registered_config: Dict[str, Any] = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# 主应用依赖注册
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
def register_app_dependencies(
|
|
29
|
+
external_tools=None,
|
|
30
|
+
external_handlers=None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
注册主应用(FastAPI backend)提供的外部工具。
|
|
34
|
+
|
|
35
|
+
当 third_party.planify 作为独立模块被主应用集成时,
|
|
36
|
+
主应用通过此函数注册其提供的工具。
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
external_tools: 外部工具定义列表
|
|
40
|
+
external_handlers: 外部工具处理器字典
|
|
41
|
+
"""
|
|
42
|
+
if external_tools or external_handlers:
|
|
43
|
+
from .tools.registry import register_external_tools
|
|
44
|
+
register_external_tools(
|
|
45
|
+
tools=external_tools or [],
|
|
46
|
+
handlers=external_handlers or {},
|
|
47
|
+
)
|
|
48
|
+
print("已注册外部工具")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register_planify_config(
|
|
52
|
+
api_key: str = "",
|
|
53
|
+
model_id: str = "claude-opus-4-6",
|
|
54
|
+
base_url: str = "",
|
|
55
|
+
protocol: str = "",
|
|
56
|
+
baidu_weather_api_url: str = "https://api.map.baidu.com/weather/v1/",
|
|
57
|
+
baidu_weather_ak: str = "",
|
|
58
|
+
baidu_weather_data_type: str = "fc",
|
|
59
|
+
**extra: Any,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""
|
|
62
|
+
注册 Planify 配置(由主应用在启动时调用)。
|
|
63
|
+
|
|
64
|
+
配置优先级:
|
|
65
|
+
1. 通过此函数注册的配置(最高)
|
|
66
|
+
2. 环境变量
|
|
67
|
+
3. 默认值
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
api_key: LLM Provider API Key
|
|
71
|
+
model_id: 模型 ID
|
|
72
|
+
base_url: API 端点(留空则用 SDK 默认)
|
|
73
|
+
protocol: 协议类型("anthropic" / "openai_compat",默认 "anthropic")
|
|
74
|
+
baidu_weather_api_url: 百度天气 API URL
|
|
75
|
+
baidu_weather_ak: 百度天气 AK
|
|
76
|
+
baidu_weather_data_type: 百度天气数据类型
|
|
77
|
+
**extra: 其他额外配置项
|
|
78
|
+
"""
|
|
79
|
+
global _registered_config
|
|
80
|
+
_registered_config = {
|
|
81
|
+
"api_key": api_key,
|
|
82
|
+
"model_id": model_id,
|
|
83
|
+
"base_url": base_url,
|
|
84
|
+
"protocol": protocol,
|
|
85
|
+
"baidu_weather_api_url": baidu_weather_api_url,
|
|
86
|
+
"baidu_weather_ak": baidu_weather_ak,
|
|
87
|
+
"baidu_weather_data_type": baidu_weather_data_type,
|
|
88
|
+
**extra,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# 将配置同步到 core/config.py,使其对 RuntimeManager 可见
|
|
92
|
+
from .core.config import register_config
|
|
93
|
+
register_config(_registered_config)
|
|
94
|
+
|
|
95
|
+
print("已注册 Planify 配置")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_registered_config() -> Dict[str, Any]:
|
|
99
|
+
"""获取已注册的 Planify 配置"""
|
|
100
|
+
return _registered_config
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_manager() -> RuntimeManager:
|
|
104
|
+
"""
|
|
105
|
+
获取 RuntimeManager 单例实例。
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
RuntimeManager 实例
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
RuntimeError: 如果应用尚未初始化
|
|
112
|
+
"""
|
|
113
|
+
global _manager
|
|
114
|
+
if _manager is None:
|
|
115
|
+
raise RuntimeError("应用尚未初始化。请先调用 initialize()。")
|
|
116
|
+
return _manager
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def initialize(base_workdir: Optional[Path] = None) -> RuntimeManager:
|
|
120
|
+
"""
|
|
121
|
+
初始化应用并返回 RuntimeManager 单例。
|
|
122
|
+
|
|
123
|
+
此函数创建 RuntimeManager 单例,用于管理所有用户运行时。
|
|
124
|
+
在初始化时自动检查并迁移旧的多会话数据。
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
base_workdir: 基础工作目录(默认为当前目录)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
RuntimeManager 实例
|
|
131
|
+
"""
|
|
132
|
+
global _manager
|
|
133
|
+
|
|
134
|
+
if _manager is not None:
|
|
135
|
+
return _manager
|
|
136
|
+
|
|
137
|
+
if base_workdir is None:
|
|
138
|
+
base_workdir = Path.cwd()
|
|
139
|
+
|
|
140
|
+
_manager = RuntimeManager(base_workdir)
|
|
141
|
+
|
|
142
|
+
# 自动检查并迁移数据
|
|
143
|
+
print("检查并迁移用户数据...")
|
|
144
|
+
migration_results = _manager.check_and_migrate_all_users()
|
|
145
|
+
successful_migrations = sum(1 for result in migration_results.values() if result)
|
|
146
|
+
if successful_migrations > 0:
|
|
147
|
+
print(f"成功迁移 {successful_migrations} 个用户的数据")
|
|
148
|
+
else:
|
|
149
|
+
print("没有需要迁移的数据")
|
|
150
|
+
|
|
151
|
+
return _manager
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def reset():
|
|
155
|
+
"""
|
|
156
|
+
重置应用状态(主要用于测试)。
|
|
157
|
+
|
|
158
|
+
清除 RuntimeManager 单例,允许重新初始化。
|
|
159
|
+
"""
|
|
160
|
+
global _manager
|
|
161
|
+
_manager = None
|
|
162
|
+
RuntimeManager.reset()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ============================================================================
|
|
166
|
+
# 简化的运行时管理 API
|
|
167
|
+
# ============================================================================
|
|
168
|
+
|
|
169
|
+
def get_or_create_runtime(user_id: str, user_config: dict, **overrides):
|
|
170
|
+
"""
|
|
171
|
+
获取或创建用户的默认运行时。
|
|
172
|
+
|
|
173
|
+
每个用户只有一个默认运行时,如果不存在则自动创建。
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
user_id: 用户 ID
|
|
177
|
+
user_config: 用户配置字典
|
|
178
|
+
**overrides: 覆盖配置的额外参数
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
AgentRuntime 实例
|
|
182
|
+
"""
|
|
183
|
+
manager = get_manager()
|
|
184
|
+
runtime = manager.get_or_create_runtime(user_id, user_config, **overrides)
|
|
185
|
+
manager.initialize_runtime_components(runtime)
|
|
186
|
+
|
|
187
|
+
# 设置子代理处理器(使用运行时隔离的工作目录)
|
|
188
|
+
from .tools import handle_task
|
|
189
|
+
runtime.tool_handlers["task"] = lambda **kw: handle_task(
|
|
190
|
+
kw["prompt"],
|
|
191
|
+
kw.get("agent_type", "Explore"),
|
|
192
|
+
runtime.config.workdir,
|
|
193
|
+
runtime.client,
|
|
194
|
+
runtime.model,
|
|
195
|
+
runtime.tool_handlers,
|
|
196
|
+
run_subagent=run_subagent,
|
|
197
|
+
runtime=runtime
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return runtime
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def list_all_runtimes():
|
|
204
|
+
"""
|
|
205
|
+
列出所有运行时。
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
所有活跃运行时列表
|
|
209
|
+
"""
|
|
210
|
+
manager = get_manager()
|
|
211
|
+
return manager.list_all_runtimes()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================================
|
|
215
|
+
# 简化的运行时管理 API(别名)
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
def get_runtime_simple(user_id: str):
|
|
219
|
+
"""
|
|
220
|
+
获取用户的默认运行时。
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
user_id: 用户 ID
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
AgentRuntime 实例,如果不存在则返回 None
|
|
227
|
+
"""
|
|
228
|
+
manager = get_manager()
|
|
229
|
+
return manager.get_runtime_simple(user_id)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def close_runtime_simple(user_id: str) -> bool:
|
|
233
|
+
"""
|
|
234
|
+
关闭并移除用户的默认运行时。
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
user_id: 用户 ID
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
是否成功关闭
|
|
241
|
+
"""
|
|
242
|
+
manager = get_manager()
|
|
243
|
+
return manager.close_runtime(user_id)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ============================================================================
|
|
247
|
+
# 向后兼容:旧的 API 接口(单用户模式)
|
|
248
|
+
# ============================================================================
|
|
249
|
+
|
|
250
|
+
# 这些全局变量仅用于向后兼容,不推荐在新代码中使用
|
|
251
|
+
config = None
|
|
252
|
+
logger = None
|
|
253
|
+
client = None
|
|
254
|
+
todo_mgr = None
|
|
255
|
+
task_mgr = None
|
|
256
|
+
bg_mgr = None
|
|
257
|
+
bus = None
|
|
258
|
+
team = None
|
|
259
|
+
skills = None
|
|
260
|
+
tools = None
|
|
261
|
+
tool_handlers = None
|
|
262
|
+
workdir = None
|
|
263
|
+
model = None
|
|
264
|
+
token_threshold = None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def init_legacy_runtime(user_id: str = "default", session_id: str = "default"):
|
|
268
|
+
"""
|
|
269
|
+
初始化旧版单用户模式的运行时(向后兼容)。
|
|
270
|
+
|
|
271
|
+
此函数创建一个运行时并将其组件同步到全局变量。
|
|
272
|
+
|
|
273
|
+
现在使用新的简化 API 自动创建用户的默认运行时。
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
user_id: 用户 ID
|
|
277
|
+
session_id: 会话 ID(已废弃,保留参数以兼容)
|
|
278
|
+
"""
|
|
279
|
+
global config, logger, client
|
|
280
|
+
global todo_mgr, task_mgr, bg_mgr, bus, team, skills
|
|
281
|
+
global tools, tool_handlers, workdir, model, token_threshold
|
|
282
|
+
|
|
283
|
+
# 使用新的简化 API 创建默认运行时
|
|
284
|
+
app_config = get_config()
|
|
285
|
+
runtime = get_or_create_runtime(
|
|
286
|
+
user_id,
|
|
287
|
+
user_config=get_user_config_dict(
|
|
288
|
+
model_id=app_config.get("model_id"),
|
|
289
|
+
api_key=app_config.get("api_key"),
|
|
290
|
+
base_url=app_config.get("base_url"),
|
|
291
|
+
token_threshold=app_config.get("token_threshold"),
|
|
292
|
+
poll_interval=app_config.get("poll_interval"),
|
|
293
|
+
idle_timeout=app_config.get("idle_timeout"),
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# 同步到全局变量
|
|
298
|
+
config = runtime.config
|
|
299
|
+
logger = runtime.logger
|
|
300
|
+
client = runtime.client
|
|
301
|
+
todo_mgr = runtime.todo_mgr
|
|
302
|
+
task_mgr = runtime.task_mgr
|
|
303
|
+
bg_mgr = runtime.bg_mgr
|
|
304
|
+
bus = runtime.bus
|
|
305
|
+
team = runtime.team
|
|
306
|
+
skills = runtime.skills
|
|
307
|
+
tools = runtime.tools
|
|
308
|
+
tool_handlers = runtime.tool_handlers
|
|
309
|
+
workdir = runtime.config.workdir
|
|
310
|
+
model = runtime.model
|
|
311
|
+
token_threshold = runtime.token_threshold
|
|
312
|
+
|
|
313
|
+
return runtime
|