aury-agent 0.0.4__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.
Files changed (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,337 @@
1
+ """
2
+ 会话管理命令
3
+ ============
4
+
5
+ 管理 Agent 会话。
6
+ """
7
+ import asyncio
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+
17
+ from aury.agents.cli.config import load_config, DEFAULT_CONFIG_DIR
18
+
19
+ session_app = typer.Typer(help="会话管理命令")
20
+ console = Console()
21
+
22
+ # 默认会话存储路径
23
+ DEFAULT_SESSIONS_DIR = DEFAULT_CONFIG_DIR / "sessions"
24
+
25
+
26
+ def _get_session_service(config: dict):
27
+ """获取会话服务实例。"""
28
+ from aury.agents.core.services import FileSessionService
29
+
30
+ storage_path = config.get("session", {}).get(
31
+ "storage_path",
32
+ str(DEFAULT_SESSIONS_DIR)
33
+ )
34
+
35
+ return FileSessionService(storage_path=storage_path)
36
+
37
+
38
+ @session_app.command("list")
39
+ def list_sessions(
40
+ config_file: Optional[Path] = typer.Option(
41
+ None,
42
+ "--config", "-c",
43
+ help="配置文件路径",
44
+ ),
45
+ limit: int = typer.Option(
46
+ 20,
47
+ "--limit", "-n",
48
+ help="显示数量限制",
49
+ ),
50
+ ):
51
+ """列出所有会话。"""
52
+ config = load_config(config_file)
53
+ service = _get_session_service(config)
54
+
55
+ # 获取会话列表
56
+ sessions = asyncio.run(service.list())
57
+
58
+ if not sessions:
59
+ console.print("[dim]暂无会话[/dim]")
60
+ return
61
+
62
+ table = Table(title=f"会话列表 (共 {len(sessions)} 个)")
63
+ table.add_column("会话 ID", style="cyan")
64
+ table.add_column("创建时间")
65
+ table.add_column("消息数")
66
+ table.add_column("状态")
67
+
68
+ for session_id in sessions[:limit]:
69
+ try:
70
+ session = asyncio.run(service.load(session_id))
71
+ created = session.metadata.get("created_at", "-")
72
+ msg_count = len(session.invocations)
73
+ status = "活跃" if session.invocations else "空闲"
74
+ table.add_row(session_id, str(created)[:19], str(msg_count), status)
75
+ except Exception:
76
+ table.add_row(session_id, "-", "-", "[red]错误[/red]")
77
+
78
+ console.print(table)
79
+
80
+ if len(sessions) > limit:
81
+ console.print(f"[dim]还有 {len(sessions) - limit} 个会话未显示[/dim]")
82
+
83
+
84
+ @session_app.command("show")
85
+ def show_session(
86
+ session_id: str = typer.Argument(
87
+ ...,
88
+ help="会话 ID",
89
+ ),
90
+ config_file: Optional[Path] = typer.Option(
91
+ None,
92
+ "--config", "-c",
93
+ help="配置文件路径",
94
+ ),
95
+ ):
96
+ """显示会话详情。"""
97
+ config = load_config(config_file)
98
+ service = _get_session_service(config)
99
+
100
+ try:
101
+ session = asyncio.run(service.load(session_id))
102
+ except Exception as e:
103
+ console.print(f"[red]无法加载会话: {e}[/red]")
104
+ raise typer.Exit(1)
105
+
106
+ # 会话信息
107
+ info = [
108
+ f"会话 ID: {session.session_id}",
109
+ f"调用数: {len(session.invocations)}",
110
+ ]
111
+
112
+ # 元数据
113
+ if session.metadata:
114
+ info.append("")
115
+ info.append("[bold]元数据:[/bold]")
116
+ for key, value in session.metadata.items():
117
+ info.append(f" {key}: {value}")
118
+
119
+ # 调用历史
120
+ if session.invocations:
121
+ info.append("")
122
+ info.append("[bold]最近调用:[/bold]")
123
+ for inv in session.invocations[-5:]:
124
+ info.append(f" - {inv.invocation_id}: {inv.status}")
125
+
126
+ console.print(Panel(
127
+ "\n".join(info),
128
+ title=f"会话: {session_id}",
129
+ ))
130
+
131
+
132
+ @session_app.command("delete")
133
+ def delete_session(
134
+ session_id: str = typer.Argument(
135
+ ...,
136
+ help="会话 ID",
137
+ ),
138
+ force: bool = typer.Option(
139
+ False,
140
+ "--force", "-f",
141
+ help="强制删除,不询问确认",
142
+ ),
143
+ config_file: Optional[Path] = typer.Option(
144
+ None,
145
+ "--config", "-c",
146
+ help="配置文件路径",
147
+ ),
148
+ ):
149
+ """删除会话。"""
150
+ config = load_config(config_file)
151
+ service = _get_session_service(config)
152
+
153
+ if not force:
154
+ confirm = typer.confirm(f"确定要删除会话 {session_id} 吗?")
155
+ if not confirm:
156
+ console.print("[dim]已取消[/dim]")
157
+ return
158
+
159
+ try:
160
+ asyncio.run(service.delete(session_id))
161
+ console.print(f"[green]会话已删除: {session_id}[/green]")
162
+ except Exception as e:
163
+ console.print(f"[red]删除失败: {e}[/red]")
164
+ raise typer.Exit(1)
165
+
166
+
167
+ @session_app.command("clear")
168
+ def clear_sessions(
169
+ force: bool = typer.Option(
170
+ False,
171
+ "--force", "-f",
172
+ help="强制删除,不询问确认",
173
+ ),
174
+ older_than: Optional[int] = typer.Option(
175
+ None,
176
+ "--older-than",
177
+ help="删除超过指定天数的会话",
178
+ ),
179
+ config_file: Optional[Path] = typer.Option(
180
+ None,
181
+ "--config", "-c",
182
+ help="配置文件路径",
183
+ ),
184
+ ):
185
+ """清理会话。"""
186
+ config = load_config(config_file)
187
+ service = _get_session_service(config)
188
+
189
+ sessions = asyncio.run(service.list())
190
+
191
+ if not sessions:
192
+ console.print("[dim]暂无会话[/dim]")
193
+ return
194
+
195
+ to_delete = []
196
+
197
+ if older_than:
198
+ # 筛选旧会话
199
+ cutoff = datetime.now().timestamp() - (older_than * 86400)
200
+
201
+ for session_id in sessions:
202
+ try:
203
+ session = asyncio.run(service.load(session_id))
204
+ created = session.metadata.get("created_at")
205
+ if created:
206
+ created_ts = datetime.fromisoformat(created).timestamp()
207
+ if created_ts < cutoff:
208
+ to_delete.append(session_id)
209
+ except Exception:
210
+ pass
211
+ else:
212
+ to_delete = sessions
213
+
214
+ if not to_delete:
215
+ console.print("[dim]没有符合条件的会话[/dim]")
216
+ return
217
+
218
+ if not force:
219
+ console.print(f"将删除 {len(to_delete)} 个会话:")
220
+ for sid in to_delete[:5]:
221
+ console.print(f" - {sid}")
222
+ if len(to_delete) > 5:
223
+ console.print(f" ... 还有 {len(to_delete) - 5} 个")
224
+
225
+ confirm = typer.confirm("确定要删除这些会话吗?")
226
+ if not confirm:
227
+ console.print("[dim]已取消[/dim]")
228
+ return
229
+
230
+ # 执行删除
231
+ deleted = 0
232
+ for session_id in to_delete:
233
+ try:
234
+ asyncio.run(service.delete(session_id))
235
+ deleted += 1
236
+ except Exception:
237
+ pass
238
+
239
+ console.print(f"[green]已删除 {deleted} 个会话[/green]")
240
+
241
+
242
+ @session_app.command("resume")
243
+ def resume_session(
244
+ session_id: str = typer.Argument(
245
+ ...,
246
+ help="会话 ID",
247
+ ),
248
+ config_file: Optional[Path] = typer.Option(
249
+ None,
250
+ "--config", "-c",
251
+ help="配置文件路径",
252
+ ),
253
+ ):
254
+ """恢复会话并继续对话。"""
255
+ from aury.agents.cli.chat import chat_command
256
+
257
+ config = load_config(config_file)
258
+
259
+ # 验证会话存在
260
+ service = _get_session_service(config)
261
+ try:
262
+ asyncio.run(service.load(session_id))
263
+ except Exception as e:
264
+ console.print(f"[red]会话不存在: {e}[/red]")
265
+ raise typer.Exit(1)
266
+
267
+ console.print(f"[dim]恢复会话: {session_id}[/dim]")
268
+
269
+ # 启动对话
270
+ asyncio.run(chat_command(
271
+ session_id=session_id,
272
+ config=config,
273
+ verbose=False,
274
+ ))
275
+
276
+
277
+ @session_app.command("export")
278
+ def export_session(
279
+ session_id: str = typer.Argument(
280
+ ...,
281
+ help="会话 ID",
282
+ ),
283
+ output: Path = typer.Option(
284
+ None,
285
+ "--output", "-o",
286
+ help="输出文件路径",
287
+ ),
288
+ format: str = typer.Option(
289
+ "json",
290
+ "--format",
291
+ help="输出格式 (json/yaml)",
292
+ ),
293
+ config_file: Optional[Path] = typer.Option(
294
+ None,
295
+ "--config", "-c",
296
+ help="配置文件路径",
297
+ ),
298
+ ):
299
+ """导出会话数据。"""
300
+ import json
301
+ import yaml
302
+
303
+ config = load_config(config_file)
304
+ service = _get_session_service(config)
305
+
306
+ try:
307
+ session = asyncio.run(service.load(session_id))
308
+ except Exception as e:
309
+ console.print(f"[red]无法加载会话: {e}[/red]")
310
+ raise typer.Exit(1)
311
+
312
+ # 转换为字典
313
+ data = {
314
+ "session_id": session.session_id,
315
+ "metadata": session.metadata,
316
+ "invocations": [
317
+ {
318
+ "invocation_id": inv.invocation_id,
319
+ "status": inv.status,
320
+ }
321
+ for inv in session.invocations
322
+ ],
323
+ }
324
+
325
+ # 格式化输出
326
+ if format == "yaml":
327
+ content = yaml.safe_dump(data, allow_unicode=True, default_flow_style=False)
328
+ else:
329
+ content = json.dumps(data, ensure_ascii=False, indent=2)
330
+
331
+ # 输出
332
+ if output:
333
+ with open(output, "w", encoding="utf-8") as f:
334
+ f.write(content)
335
+ console.print(f"[green]已导出到: {output}[/green]")
336
+ else:
337
+ console.print(content)
@@ -0,0 +1,276 @@
1
+ """
2
+ Workflow 命令
3
+ =============
4
+
5
+ 运行和管理 Workflow。
6
+ """
7
+ import asyncio
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.progress import Progress, SpinnerColumn, TextColumn
16
+ from rich.table import Table
17
+
18
+ from aury.agents.cli.config import load_config
19
+
20
+ workflow_app = typer.Typer(help="Workflow 相关命令")
21
+ console = Console()
22
+
23
+
24
+ @workflow_app.command("run")
25
+ def run_workflow(
26
+ workflow_file: Path = typer.Argument(
27
+ ...,
28
+ help="Workflow 定义文件路径(.py 或 .yaml)",
29
+ ),
30
+ input_data: Optional[str] = typer.Option(
31
+ None,
32
+ "--input", "-i",
33
+ help="输入数据(JSON 格式)",
34
+ ),
35
+ config_file: Optional[Path] = typer.Option(
36
+ None,
37
+ "--config", "-c",
38
+ help="配置文件路径",
39
+ ),
40
+ verbose: bool = typer.Option(
41
+ False,
42
+ "--verbose", "-v",
43
+ help="显示详细输出",
44
+ ),
45
+ dry_run: bool = typer.Option(
46
+ False,
47
+ "--dry-run",
48
+ help="模拟运行,不实际执行",
49
+ ),
50
+ ):
51
+ """
52
+ 运行 Workflow。
53
+
54
+ 示例:
55
+ aury-agent workflow run pipeline.py
56
+ aury-agent workflow run pipeline.yaml --input '{"data": "test"}'
57
+ """
58
+ # 加载配置
59
+ cfg = load_config(config_file)
60
+
61
+ if not workflow_file.exists():
62
+ console.print(f"[red]错误: Workflow 文件不存在: {workflow_file}[/red]")
63
+ raise typer.Exit(1)
64
+
65
+ # 解析输入
66
+ input_dict = {}
67
+ if input_data:
68
+ try:
69
+ input_dict = json.loads(input_data)
70
+ except json.JSONDecodeError as e:
71
+ console.print(f"[red]错误: 无效的 JSON 输入: {e}[/red]")
72
+ raise typer.Exit(1)
73
+
74
+ console.print(f"[bold]运行 Workflow: {workflow_file}[/bold]")
75
+
76
+ if dry_run:
77
+ console.print("[yellow]模拟运行模式[/yellow]")
78
+ _dry_run_workflow(workflow_file, input_dict, verbose)
79
+ return
80
+
81
+ # 实际运行
82
+ asyncio.run(_execute_workflow(workflow_file, input_dict, cfg, verbose))
83
+
84
+
85
+ async def _execute_workflow(
86
+ workflow_file: Path,
87
+ input_data: dict,
88
+ config: dict,
89
+ verbose: bool,
90
+ ):
91
+ """执行 Workflow。"""
92
+ from aury.agents.workflow import WorkflowExecutor
93
+ from aury.agents.core import Session, InvocationContext
94
+
95
+ # 加载 Workflow
96
+ workflow = await _load_workflow(workflow_file)
97
+
98
+ if workflow is None:
99
+ console.print("[red]无法加载 Workflow[/red]")
100
+ return
101
+
102
+ console.print(f"[dim]Workflow: {workflow.name}[/dim]")
103
+ console.print(f"[dim]步骤数: {len(workflow.step_map)}[/dim]")
104
+
105
+ # 创建上下文
106
+ session = Session(session_id=f"workflow-{workflow.name}")
107
+ context = InvocationContext(session=session)
108
+
109
+ # 创建执行器
110
+ executor = WorkflowExecutor(workflow)
111
+
112
+ # 执行
113
+ with Progress(
114
+ SpinnerColumn(),
115
+ TextColumn("[progress.description]{task.description}"),
116
+ console=console,
117
+ ) as progress:
118
+ task = progress.add_task("执行中...", total=None)
119
+
120
+ try:
121
+ result = await executor.execute(context, input_data=input_data)
122
+ progress.update(task, description="完成")
123
+ except Exception as e:
124
+ progress.update(task, description=f"[red]失败: {e}[/red]")
125
+ raise
126
+
127
+ # 显示结果
128
+ console.print()
129
+
130
+ if result.success:
131
+ console.print(Panel(
132
+ f"[green]Workflow 执行成功[/green]\n\n"
133
+ f"完成步骤: {', '.join(result.state.get_completed_steps())}",
134
+ title="结果",
135
+ ))
136
+ else:
137
+ console.print(Panel(
138
+ f"[red]Workflow 执行失败[/red]\n\n"
139
+ f"失败步骤: {', '.join([s for s in result.state.step_statuses if result.state.is_step_failed(s)])}",
140
+ title="结果",
141
+ ))
142
+
143
+ # 详细输出
144
+ if verbose:
145
+ console.print("\n[bold]步骤详情:[/bold]")
146
+ for step_id, status in result.state.step_statuses.items():
147
+ step_result = result.state.get_step_result(step_id)
148
+ console.print(f" {step_id}: {status}")
149
+ if step_result:
150
+ console.print(f" 结果: {step_result}")
151
+
152
+
153
+ async def _load_workflow(workflow_file: Path):
154
+ """加载 Workflow 定义。"""
155
+ if workflow_file.suffix == ".py":
156
+ # 从 Python 文件加载
157
+ import importlib.util
158
+
159
+ spec = importlib.util.spec_from_file_location("workflow_module", workflow_file)
160
+ module = importlib.util.module_from_spec(spec)
161
+ spec.loader.exec_module(module)
162
+
163
+ # 查找 Workflow 对象或构建函数
164
+ if hasattr(module, "workflow"):
165
+ return module.workflow
166
+ elif hasattr(module, "build_workflow"):
167
+ return module.build_workflow()
168
+ elif hasattr(module, "create_workflow"):
169
+ return module.create_workflow()
170
+ else:
171
+ console.print("[yellow]警告: 未找到 workflow 对象或构建函数[/yellow]")
172
+ return None
173
+
174
+ elif workflow_file.suffix in [".yaml", ".yml"]:
175
+ # 从 YAML 文件加载
176
+ # TODO: 实现 YAML 格式的 Workflow 定义
177
+ console.print("[yellow]YAML Workflow 格式暂未支持[/yellow]")
178
+ return None
179
+
180
+ else:
181
+ console.print(f"[red]不支持的文件格式: {workflow_file.suffix}[/red]")
182
+ return None
183
+
184
+
185
+ def _dry_run_workflow(workflow_file: Path, input_data: dict, verbose: bool):
186
+ """模拟运行 Workflow。"""
187
+ console.print("\n[dim]模拟运行步骤:[/dim]")
188
+ console.print(f" 1. 加载 Workflow: {workflow_file}")
189
+ console.print(f" 2. 验证输入: {input_data or '(无输入)'}")
190
+ console.print(" 3. 执行各步骤 (模拟)")
191
+ console.print(" 4. 返回结果")
192
+
193
+
194
+ @workflow_app.command("list")
195
+ def list_workflows(
196
+ directory: Path = typer.Option(
197
+ Path("."),
198
+ "--dir", "-d",
199
+ help="搜索目录",
200
+ ),
201
+ ):
202
+ """列出可用的 Workflow 文件。"""
203
+ workflows = []
204
+
205
+ # 搜索 Python 文件
206
+ for py_file in directory.glob("**/*.py"):
207
+ if "workflow" in py_file.name.lower():
208
+ workflows.append(py_file)
209
+
210
+ # 搜索 YAML 文件
211
+ for yaml_file in directory.glob("**/*.yaml"):
212
+ if "workflow" in yaml_file.name.lower():
213
+ workflows.append(yaml_file)
214
+
215
+ if not workflows:
216
+ console.print("[dim]未找到 Workflow 文件[/dim]")
217
+ return
218
+
219
+ table = Table(title="可用 Workflow")
220
+ table.add_column("文件", style="cyan")
221
+ table.add_column("类型")
222
+
223
+ for wf in workflows:
224
+ file_type = "Python" if wf.suffix == ".py" else "YAML"
225
+ table.add_row(str(wf.relative_to(directory)), file_type)
226
+
227
+ console.print(table)
228
+
229
+
230
+ @workflow_app.command("validate")
231
+ def validate_workflow(
232
+ workflow_file: Path = typer.Argument(
233
+ ...,
234
+ help="Workflow 定义文件路径",
235
+ ),
236
+ ):
237
+ """验证 Workflow 定义。"""
238
+ if not workflow_file.exists():
239
+ console.print(f"[red]文件不存在: {workflow_file}[/red]")
240
+ raise typer.Exit(1)
241
+
242
+ console.print(f"验证 Workflow: {workflow_file}")
243
+
244
+ try:
245
+ workflow = asyncio.run(_load_workflow(workflow_file))
246
+
247
+ if workflow is None:
248
+ console.print("[red]验证失败: 无法加载 Workflow[/red]")
249
+ raise typer.Exit(1)
250
+
251
+ # 验证
252
+ errors = []
253
+
254
+ if not workflow.entry_step_id:
255
+ errors.append("未定义入口步骤")
256
+
257
+ if not workflow.step_map:
258
+ errors.append("没有定义任何步骤")
259
+
260
+ if workflow.entry_step_id and workflow.entry_step_id not in workflow.step_map:
261
+ errors.append(f"入口步骤不存在: {workflow.entry_step_id}")
262
+
263
+ if errors:
264
+ console.print("[red]验证失败:[/red]")
265
+ for err in errors:
266
+ console.print(f" - {err}")
267
+ raise typer.Exit(1)
268
+
269
+ console.print(f"[green]验证通过[/green]")
270
+ console.print(f" 名称: {workflow.name}")
271
+ console.print(f" 步骤数: {len(workflow.step_map)}")
272
+ console.print(f" 入口: {workflow.entry_step_id}")
273
+
274
+ except Exception as e:
275
+ console.print(f"[red]验证失败: {e}[/red]")
276
+ raise typer.Exit(1)
@@ -0,0 +1,66 @@
1
+ """ContextProvider system for context engineering.
2
+
3
+ ContextProviders provide a unified abstraction for fetching LLM context:
4
+ - Messages (conversation history)
5
+ - Memory (long-term memory summaries/recalls)
6
+ - Artifacts (with pluggable loaders for different types)
7
+ - SubAgents (sub-agent configurations)
8
+ - Skills (capability descriptions)
9
+
10
+ Core Design:
11
+ - Provider: ONLY responsible for FETCHING context (read)
12
+ - Middleware: Responsible for WRITING/SAVING (via hooks like on_message_save)
13
+ - Manager: Service layer providing APIs for both Provider and Middleware
14
+
15
+ - Each ContextProvider implements `fetch(ctx) -> AgentContext`
16
+ - AgentContext contains: system_content, user_content, messages, tools, subagents, skills
17
+ - Multiple AgentContexts are merged before LLM call
18
+
19
+ Usage:
20
+ agent = ReactAgent.create(
21
+ llm=llm,
22
+ tools=[my_tool],
23
+ providers=[
24
+ ArtifactContextProvider(backend=artifact_backend),
25
+ MemoryContextProvider(memory_manager=memory),
26
+ ],
27
+ enable_history=True, # Auto-creates MessageContextProvider + MessageBackendMiddleware
28
+ )
29
+
30
+ Built-in ContextProviders:
31
+ - MessageContextProvider: Fetch conversation history
32
+ - MemoryContextProvider: Fetch memory summaries/recalls
33
+ - ArtifactContextProvider: Artifact index with ReadArtifactTool + pluggable loaders
34
+ - SubAgentContextProvider: Provides sub-agent configurations
35
+ - SkillContextProvider: Skill descriptions in system prompt
36
+ """
37
+ from .base import ContextProvider, AgentContext, BaseContextProvider
38
+ from .message import MessageContextProvider
39
+ from .memory import MemoryContextProvider
40
+ from .artifact import (
41
+ ArtifactContextProvider,
42
+ ArtifactLoader,
43
+ ReadArtifactTool,
44
+ register_loader,
45
+ get_loader,
46
+ )
47
+ from .subagent import SubAgentContextProvider
48
+ from .skill import SkillContextProvider
49
+
50
+ __all__ = [
51
+ # Base protocol
52
+ "ContextProvider",
53
+ "AgentContext",
54
+ "BaseContextProvider",
55
+ # Built-in providers
56
+ "MessageContextProvider",
57
+ "MemoryContextProvider",
58
+ "ArtifactContextProvider",
59
+ "SubAgentContextProvider",
60
+ "SkillContextProvider",
61
+ # Artifact utilities
62
+ "ArtifactLoader",
63
+ "ReadArtifactTool",
64
+ "register_loader",
65
+ "get_loader",
66
+ ]