jarvis-ai-assistant 0.3.25__py3-none-any.whl → 0.3.27__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +290 -169
- jarvis/jarvis_agent/config.py +92 -0
- jarvis/jarvis_agent/event_bus.py +48 -0
- jarvis/jarvis_agent/jarvis.py +69 -35
- jarvis/jarvis_agent/memory_manager.py +70 -2
- jarvis/jarvis_agent/prompt_manager.py +82 -0
- jarvis/jarvis_agent/run_loop.py +130 -0
- jarvis/jarvis_agent/task_analyzer.py +88 -9
- jarvis/jarvis_agent/task_manager.py +26 -0
- jarvis/jarvis_agent/user_interaction.py +42 -0
- jarvis/jarvis_code_agent/code_agent.py +18 -3
- jarvis/jarvis_code_agent/lint.py +5 -5
- jarvis/jarvis_data/config_schema.json +7 -6
- jarvis/jarvis_git_squash/main.py +6 -1
- jarvis/jarvis_git_utils/git_commiter.py +38 -12
- jarvis/jarvis_platform/base.py +4 -5
- jarvis/jarvis_platform_manager/main.py +28 -11
- jarvis/jarvis_rag/cli.py +5 -9
- jarvis/jarvis_stats/cli.py +13 -32
- jarvis/jarvis_stats/stats.py +179 -51
- jarvis/jarvis_tools/registry.py +15 -0
- jarvis/jarvis_tools/sub_agent.py +94 -84
- jarvis/jarvis_tools/sub_code_agent.py +12 -6
- jarvis/jarvis_utils/config.py +14 -0
- jarvis/jarvis_utils/fzf.py +56 -0
- jarvis/jarvis_utils/git_utils.py +3 -8
- jarvis/jarvis_utils/input.py +0 -3
- jarvis/jarvis_utils/utils.py +96 -16
- {jarvis_ai_assistant-0.3.25.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/METADATA +2 -3
- {jarvis_ai_assistant-0.3.25.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/RECORD +35 -29
- {jarvis_ai_assistant-0.3.25.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.25.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.25.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.25.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
AgentConfig: 聚合 Agent 的初始化配置并提供默认值解析。
|
4
|
+
|
5
|
+
目标(阶段一,最小变更):
|
6
|
+
- 提供独立的配置承载类,封装 __init__ 中的配置项
|
7
|
+
- 支持从全局配置与上下文推导默认值
|
8
|
+
- 暂不强制替换 Agent 的现有参数与流程,后续逐步接入
|
9
|
+
"""
|
10
|
+
from dataclasses import dataclass, field
|
11
|
+
from typing import List, Optional
|
12
|
+
|
13
|
+
from jarvis.jarvis_agent.prompts import DEFAULT_SUMMARY_PROMPT
|
14
|
+
from jarvis.jarvis_utils.config import (
|
15
|
+
get_max_token_count,
|
16
|
+
is_execute_tool_confirm,
|
17
|
+
is_force_save_memory,
|
18
|
+
is_use_analysis,
|
19
|
+
is_use_methodology,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass
|
24
|
+
class AgentConfig:
|
25
|
+
# 核心身份与系统参数
|
26
|
+
system_prompt: str
|
27
|
+
name: str = "Jarvis"
|
28
|
+
description: str = ""
|
29
|
+
model_group: Optional[str] = None
|
30
|
+
|
31
|
+
# 运行行为
|
32
|
+
auto_complete: bool = False
|
33
|
+
need_summary: bool = True
|
34
|
+
|
35
|
+
# 可选配置(None 表示使用默认策略解析)
|
36
|
+
summary_prompt: Optional[str] = None
|
37
|
+
execute_tool_confirm: Optional[bool] = None
|
38
|
+
use_methodology: Optional[bool] = None
|
39
|
+
use_analysis: Optional[bool] = None
|
40
|
+
force_save_memory: Optional[bool] = None
|
41
|
+
files: Optional[List[str]] = field(default_factory=list)
|
42
|
+
max_token_count: Optional[int] = None
|
43
|
+
|
44
|
+
def resolve_defaults(self) -> "AgentConfig":
|
45
|
+
"""
|
46
|
+
解析并填充默认值,返回新的 AgentConfig 实例,不修改原对象。
|
47
|
+
策略与 Agent._init_config 中的逻辑保持一致,确保兼容。
|
48
|
+
"""
|
49
|
+
# 复制当前实例的浅拷贝数据
|
50
|
+
cfg = AgentConfig(
|
51
|
+
system_prompt=self.system_prompt,
|
52
|
+
name=self.name,
|
53
|
+
description=self.description,
|
54
|
+
model_group=self.model_group,
|
55
|
+
auto_complete=self.auto_complete,
|
56
|
+
need_summary=self.need_summary,
|
57
|
+
summary_prompt=self.summary_prompt,
|
58
|
+
execute_tool_confirm=self.execute_tool_confirm,
|
59
|
+
use_methodology=self.use_methodology,
|
60
|
+
use_analysis=self.use_analysis,
|
61
|
+
force_save_memory=self.force_save_memory,
|
62
|
+
files=list(self.files or []),
|
63
|
+
max_token_count=self.max_token_count,
|
64
|
+
)
|
65
|
+
|
66
|
+
# use_methodology: 若存在上传文件则禁用;否则按照外部传入或全局默认
|
67
|
+
if cfg.files:
|
68
|
+
cfg.use_methodology = False
|
69
|
+
elif cfg.use_methodology is None:
|
70
|
+
cfg.use_methodology = is_use_methodology()
|
71
|
+
|
72
|
+
# use_analysis
|
73
|
+
if cfg.use_analysis is None:
|
74
|
+
cfg.use_analysis = is_use_analysis()
|
75
|
+
|
76
|
+
# execute_tool_confirm
|
77
|
+
if cfg.execute_tool_confirm is None:
|
78
|
+
cfg.execute_tool_confirm = is_execute_tool_confirm()
|
79
|
+
|
80
|
+
# summary_prompt
|
81
|
+
if cfg.summary_prompt is None:
|
82
|
+
cfg.summary_prompt = DEFAULT_SUMMARY_PROMPT
|
83
|
+
|
84
|
+
# max_token_count
|
85
|
+
if cfg.max_token_count is None:
|
86
|
+
cfg.max_token_count = get_max_token_count(cfg.model_group)
|
87
|
+
|
88
|
+
# force_save_memory
|
89
|
+
if cfg.force_save_memory is None:
|
90
|
+
cfg.force_save_memory = is_force_save_memory()
|
91
|
+
|
92
|
+
return cfg
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
事件总线(EventBus)
|
4
|
+
|
5
|
+
目标(阶段一,最小变更):
|
6
|
+
- 提供简单可靠的发布/订阅机制
|
7
|
+
- 回调异常隔离,避免影响主流程
|
8
|
+
- 不引入额外依赖,便于在 Agent 中渐进集成
|
9
|
+
"""
|
10
|
+
from collections import defaultdict
|
11
|
+
from typing import Callable, DefaultDict, Dict, List
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
class EventBus:
|
16
|
+
"""
|
17
|
+
简单的同步事件总线。
|
18
|
+
- subscribe(event, callback): 订阅事件
|
19
|
+
- emit(event, **kwargs): 广播事件
|
20
|
+
- unsubscribe(event, callback): 取消订阅
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self) -> None:
|
24
|
+
self._listeners: DefaultDict[str, List[Callable[..., None]]] = defaultdict(list)
|
25
|
+
|
26
|
+
def subscribe(self, event: str, callback: Callable[..., None]) -> None:
|
27
|
+
if not callable(callback):
|
28
|
+
raise TypeError("callback must be callable")
|
29
|
+
self._listeners[event].append(callback)
|
30
|
+
|
31
|
+
def unsubscribe(self, event: str, callback: Callable[..., None]) -> None:
|
32
|
+
if event not in self._listeners:
|
33
|
+
return
|
34
|
+
try:
|
35
|
+
self._listeners[event].remove(callback)
|
36
|
+
except ValueError:
|
37
|
+
pass
|
38
|
+
|
39
|
+
def emit(self, event: str, **payload: Dict) -> None:
|
40
|
+
"""
|
41
|
+
广播事件。回调中的异常将被捕获并忽略,以保证主流程稳定。
|
42
|
+
"""
|
43
|
+
for cb in list(self._listeners.get(event, [])):
|
44
|
+
try:
|
45
|
+
cb(**payload)
|
46
|
+
except Exception:
|
47
|
+
# 避免回调异常中断主流程
|
48
|
+
continue
|
jarvis/jarvis_agent/jarvis.py
CHANGED
@@ -19,6 +19,7 @@ from jarvis.jarvis_utils.config import (
|
|
19
19
|
)
|
20
20
|
import jarvis.jarvis_utils.utils as jutils
|
21
21
|
from jarvis.jarvis_utils.input import user_confirm, get_single_line_input
|
22
|
+
from jarvis.jarvis_utils.fzf import fzf_select
|
22
23
|
import os
|
23
24
|
import sys
|
24
25
|
import subprocess
|
@@ -403,47 +404,80 @@ def handle_builtin_config_selector(
|
|
403
404
|
|
404
405
|
Console().print(table)
|
405
406
|
|
406
|
-
|
407
|
-
|
407
|
+
# Try to use fzf for selection if available (include No. to support number-based filtering)
|
408
|
+
fzf_options = [
|
409
|
+
f"{idx:>3} | {opt['category']:<12} | {opt['name']:<30} | {opt.get('desc', '')}"
|
410
|
+
for idx, opt in enumerate(options, 1)
|
411
|
+
]
|
412
|
+
selected_str = fzf_select(
|
413
|
+
fzf_options, prompt="选择要启动的配置编号 (ESC跳过) > "
|
408
414
|
)
|
409
415
|
|
410
|
-
|
416
|
+
choice_index = -1
|
417
|
+
if selected_str:
|
418
|
+
# Try to parse leading number before first '|'
|
411
419
|
try:
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
420
|
+
num_part = selected_str.split("|", 1)[0].strip()
|
421
|
+
selected_index = int(num_part)
|
422
|
+
if 1 <= selected_index <= len(options):
|
423
|
+
choice_index = selected_index - 1
|
424
|
+
except Exception:
|
425
|
+
# Fallback to equality matching if parsing fails
|
426
|
+
for i, fzf_opt in enumerate(fzf_options):
|
427
|
+
if fzf_opt == selected_str:
|
428
|
+
choice_index = i
|
429
|
+
break
|
430
|
+
else:
|
431
|
+
# Fallback to manual input if fzf is not used or available
|
432
|
+
choice = get_single_line_input(
|
433
|
+
"选择要启动的配置编号,直接回车使用默认通用代理(jvs): ", default=""
|
434
|
+
)
|
435
|
+
if choice.strip():
|
436
|
+
try:
|
437
|
+
selected_index = int(choice.strip())
|
438
|
+
if 1 <= selected_index <= len(options):
|
439
|
+
choice_index = selected_index - 1
|
440
|
+
except ValueError:
|
441
|
+
pass # Invalid input
|
442
|
+
|
443
|
+
if choice_index != -1:
|
444
|
+
try:
|
445
|
+
sel = options[choice_index]
|
446
|
+
args: list[str] = []
|
447
|
+
|
448
|
+
if sel["category"] == "agent":
|
449
|
+
# jarvis-agent 支持 -f/--config(全局配置)与 -c/--agent-definition
|
450
|
+
args = [str(sel["cmd"]), "-c", str(sel["file"])]
|
451
|
+
if model_group:
|
452
|
+
args += ["-g", str(model_group)]
|
453
|
+
if config_file:
|
454
|
+
args += ["-f", str(config_file)]
|
455
|
+
if task:
|
456
|
+
args += ["--task", str(task)]
|
457
|
+
|
458
|
+
elif sel["category"] == "multi_agent":
|
459
|
+
# jarvis-multi-agent 需要 -c/--config,用户输入通过 -i/--input 传递
|
460
|
+
args = [str(sel["cmd"]), "-c", str(sel["file"])]
|
461
|
+
if task:
|
462
|
+
args += ["-i", str(task)]
|
463
|
+
|
464
|
+
elif sel["category"] == "roles":
|
465
|
+
# jarvis-platform-manager role 子命令,支持 -c/-t/-g
|
466
|
+
args = [str(sel["cmd"]), "role", "-c", str(sel["file"])]
|
467
|
+
if model_group:
|
468
|
+
args += ["-g", str(model_group)]
|
469
|
+
|
470
|
+
if args:
|
471
|
+
PrettyOutput.print(
|
472
|
+
f"正在启动: {' '.join(args)}", OutputType.INFO
|
473
|
+
)
|
474
|
+
os.execvp(args[0], args)
|
444
475
|
except Exception:
|
445
476
|
# 任何异常都不影响默认流程
|
446
477
|
pass
|
478
|
+
else:
|
479
|
+
# User pressed Enter or provided invalid input
|
480
|
+
pass
|
447
481
|
except Exception:
|
448
482
|
# 静默忽略内置配置扫描错误,不影响主流程
|
449
483
|
pass
|
@@ -20,6 +20,13 @@ class MemoryManager:
|
|
20
20
|
agent: Agent实例
|
21
21
|
"""
|
22
22
|
self.agent = agent
|
23
|
+
# 本轮任务是否已进行过记忆保存提示/处理的标记,用于事件去重
|
24
|
+
self._memory_prompted = False
|
25
|
+
# 订阅 Agent 事件(旁路集成,失败不影响主流程)
|
26
|
+
try:
|
27
|
+
self._subscribe_events()
|
28
|
+
except Exception:
|
29
|
+
pass
|
23
30
|
|
24
31
|
def prepare_memory_tags_prompt(self) -> str:
|
25
32
|
"""准备记忆标签提示"""
|
@@ -88,13 +95,27 @@ class MemoryManager:
|
|
88
95
|
|
89
96
|
# 处理记忆保存
|
90
97
|
try:
|
98
|
+
# 清空本轮执行标记,便于准确判断是否调用了 save_memory
|
99
|
+
try:
|
100
|
+
self.agent.set_user_data("__last_executed_tool__", "")
|
101
|
+
self.agent.set_user_data("__executed_tools__", [])
|
102
|
+
except Exception:
|
103
|
+
pass
|
104
|
+
|
91
105
|
response = self.agent.model.chat_until_success(prompt) # type: ignore
|
92
106
|
|
93
107
|
# 执行工具调用(如果有)
|
94
108
|
need_return, result = self.agent._call_tools(response)
|
95
109
|
|
96
|
-
#
|
97
|
-
|
110
|
+
# 根据实际执行的工具判断是否保存了记忆
|
111
|
+
saved = False
|
112
|
+
try:
|
113
|
+
last_tool = self.agent.get_user_data("__last_executed_tool__")
|
114
|
+
saved = last_tool == "save_memory"
|
115
|
+
except Exception:
|
116
|
+
saved = False
|
117
|
+
|
118
|
+
if saved:
|
98
119
|
PrettyOutput.print(
|
99
120
|
"已自动保存有价值的信息到记忆系统", OutputType.SUCCESS
|
100
121
|
)
|
@@ -103,6 +124,13 @@ class MemoryManager:
|
|
103
124
|
|
104
125
|
except Exception as e:
|
105
126
|
PrettyOutput.print(f"记忆分析失败: {str(e)}", OutputType.ERROR)
|
127
|
+
finally:
|
128
|
+
# 设置记忆提示完成标记,避免事件触发造成重复处理
|
129
|
+
self._memory_prompted = True
|
130
|
+
try:
|
131
|
+
self.agent.set_user_data("__memory_save_prompted__", True)
|
132
|
+
except Exception:
|
133
|
+
pass
|
106
134
|
|
107
135
|
def add_memory_prompts_to_addon(self, addon_prompt: str, tool_registry) -> str:
|
108
136
|
"""在附加提示中添加记忆相关提示"""
|
@@ -125,3 +153,43 @@ class MemoryManager:
|
|
125
153
|
memory_prompts += "\n - 如果需要获取上下文或寻找解决方案,请调用retrieve_memory工具检索相关记忆"
|
126
154
|
|
127
155
|
return memory_prompts
|
156
|
+
|
157
|
+
# -----------------------
|
158
|
+
# 事件订阅与处理(旁路)
|
159
|
+
# -----------------------
|
160
|
+
def _subscribe_events(self) -> None:
|
161
|
+
bus = self.agent.get_event_bus() # type: ignore[attr-defined]
|
162
|
+
# 任务开始时重置去重标记
|
163
|
+
bus.subscribe("task_started", self._on_task_started)
|
164
|
+
# 在清理历史前尝试保存记忆(若开启强制保存且尚未处理)
|
165
|
+
bus.subscribe("before_history_clear", self._ensure_memory_prompt)
|
166
|
+
# 任务完成时作为兜底再尝试一次
|
167
|
+
bus.subscribe("task_completed", self._ensure_memory_prompt)
|
168
|
+
|
169
|
+
def _on_task_started(self, **payload) -> None:
|
170
|
+
self._memory_prompted = False
|
171
|
+
try:
|
172
|
+
self.agent.set_user_data("__memory_save_prompted__", False)
|
173
|
+
except Exception:
|
174
|
+
pass
|
175
|
+
|
176
|
+
def _ensure_memory_prompt(self, **payload) -> None:
|
177
|
+
# 仅在开启强制保存记忆时启用
|
178
|
+
if not getattr(self.agent, "force_save_memory", False):
|
179
|
+
return
|
180
|
+
# 避免在同一任务内重复提示/处理
|
181
|
+
if self._memory_prompted:
|
182
|
+
return
|
183
|
+
try:
|
184
|
+
already = bool(self.agent.get_user_data("__memory_save_prompted__"))
|
185
|
+
if already:
|
186
|
+
self._memory_prompted = True
|
187
|
+
return
|
188
|
+
except Exception:
|
189
|
+
pass
|
190
|
+
# 静默执行保存逻辑,失败不影响主流程
|
191
|
+
try:
|
192
|
+
self.prompt_memory_save()
|
193
|
+
except Exception:
|
194
|
+
# 忽略异常,保持主流程稳定
|
195
|
+
self._memory_prompted = True
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
PromptManager: 统一管理 Agent 的系统提示词与附加提示词的构建逻辑。
|
4
|
+
|
5
|
+
设计目标(阶段一,最小变更):
|
6
|
+
- 提供独立的提示构建类,不改变现有行为
|
7
|
+
- 先行落地构建逻辑,后续在 Agent 中逐步委派使用
|
8
|
+
- 保持与现有工具/记忆系统兼容
|
9
|
+
"""
|
10
|
+
from typing import TYPE_CHECKING
|
11
|
+
|
12
|
+
from jarvis.jarvis_tools.registry import ToolRegistry
|
13
|
+
from jarvis.jarvis_utils.tag import ot
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
# 避免运行时循环依赖,仅用于类型标注
|
17
|
+
from . import Agent # noqa: F401
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
class PromptManager:
|
22
|
+
"""
|
23
|
+
提示管理器:负责构建系统提示与默认附加提示。
|
24
|
+
注意:该类不直接访问模型,只负责拼装字符串。
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, agent: "Agent"):
|
28
|
+
self.agent = agent
|
29
|
+
|
30
|
+
# ----------------------------
|
31
|
+
# 系统提示词构建
|
32
|
+
# ----------------------------
|
33
|
+
def build_system_prompt(self) -> str:
|
34
|
+
"""
|
35
|
+
构建系统提示词,复用现有的工具使用提示生成逻辑,保持行为一致。
|
36
|
+
"""
|
37
|
+
action_prompt = self.agent.get_tool_usage_prompt()
|
38
|
+
return f"""
|
39
|
+
{self.agent.system_prompt}
|
40
|
+
|
41
|
+
{action_prompt}
|
42
|
+
"""
|
43
|
+
|
44
|
+
# ----------------------------
|
45
|
+
# 附加提示词构建
|
46
|
+
# ----------------------------
|
47
|
+
def build_default_addon_prompt(self, need_complete: bool) -> str:
|
48
|
+
"""
|
49
|
+
构建默认附加提示词(与 Agent.make_default_addon_prompt 行为保持一致)。
|
50
|
+
仅进行字符串拼装,不操作会话状态。
|
51
|
+
"""
|
52
|
+
# 结构化系统指令
|
53
|
+
action_handlers = ", ".join([handler.name() for handler in self.agent.output_handler])
|
54
|
+
|
55
|
+
# 任务完成提示
|
56
|
+
complete_prompt = (
|
57
|
+
f"- 输出{ot('!!!COMPLETE!!!')}"
|
58
|
+
if need_complete and self.agent.auto_complete
|
59
|
+
else ""
|
60
|
+
)
|
61
|
+
|
62
|
+
# 工具与记忆相关提示
|
63
|
+
tool_registry = self.agent.get_tool_registry()
|
64
|
+
memory_prompts = self.agent.memory_manager.add_memory_prompts_to_addon(
|
65
|
+
"", tool_registry if isinstance(tool_registry, ToolRegistry) else None
|
66
|
+
)
|
67
|
+
|
68
|
+
addon_prompt = f"""
|
69
|
+
<system_prompt>
|
70
|
+
请判断是否已经完成任务,如果已经完成:
|
71
|
+
- 直接输出完成原因,不需要再有新的操作,不要输出{ot("TOOL_CALL")}标签
|
72
|
+
{complete_prompt}
|
73
|
+
如果没有完成,请进行下一步操作:
|
74
|
+
- 仅包含一个操作
|
75
|
+
- 如果信息不明确,请请求用户补充
|
76
|
+
- 如果执行过程中连续失败5次,请使用ask_user询问用户操作
|
77
|
+
- 操作列表:{action_handlers}{memory_prompts}
|
78
|
+
</system_prompt>
|
79
|
+
|
80
|
+
请继续。
|
81
|
+
"""
|
82
|
+
return addon_prompt
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
AgentRunLoop: 承载 Agent 的主运行循环逻辑。
|
4
|
+
|
5
|
+
阶段一目标(最小变更):
|
6
|
+
- 复制现有 _main_loop 逻辑到独立类,使用传入的 agent 实例进行委派调用
|
7
|
+
- 暂不变更外部调用入口,后续在 Agent._main_loop 中委派到该类
|
8
|
+
- 保持与现有异常处理、工具调用、用户交互完全一致
|
9
|
+
"""
|
10
|
+
from enum import Enum
|
11
|
+
from typing import Any, TYPE_CHECKING
|
12
|
+
|
13
|
+
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
14
|
+
from jarvis.jarvis_utils.tag import ot
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
# 仅用于类型标注,避免运行时循环依赖
|
18
|
+
from . import Agent # noqa: F401
|
19
|
+
|
20
|
+
|
21
|
+
class AgentRunLoop:
|
22
|
+
def __init__(self, agent: "Agent") -> None:
|
23
|
+
self.agent = agent
|
24
|
+
|
25
|
+
def run(self) -> Any:
|
26
|
+
"""主运行循环(委派到传入的 agent 实例的方法与属性)"""
|
27
|
+
run_input_handlers = True
|
28
|
+
|
29
|
+
while True:
|
30
|
+
try:
|
31
|
+
ag = self.agent
|
32
|
+
|
33
|
+
# 更新输入处理器标志
|
34
|
+
if ag.run_input_handlers_next_turn:
|
35
|
+
run_input_handlers = True
|
36
|
+
ag.run_input_handlers_next_turn = False
|
37
|
+
|
38
|
+
# 首次运行初始化
|
39
|
+
if ag.first:
|
40
|
+
ag._first_run()
|
41
|
+
|
42
|
+
# 调用模型获取响应
|
43
|
+
current_response = ag._call_model(
|
44
|
+
ag.session.prompt, True, run_input_handlers
|
45
|
+
)
|
46
|
+
|
47
|
+
ag.session.prompt = ""
|
48
|
+
run_input_handlers = False
|
49
|
+
|
50
|
+
# 处理中断
|
51
|
+
interrupt_result = ag._handle_run_interrupt(current_response)
|
52
|
+
if (
|
53
|
+
isinstance(interrupt_result, Enum)
|
54
|
+
and getattr(interrupt_result, "value", None) == "skip_turn"
|
55
|
+
):
|
56
|
+
# 中断处理器请求跳过本轮剩余部分,直接开始下一次循环
|
57
|
+
continue
|
58
|
+
elif interrupt_result is not None and not isinstance(interrupt_result, Enum):
|
59
|
+
# 中断处理器返回了最终结果,任务结束
|
60
|
+
return interrupt_result
|
61
|
+
|
62
|
+
# 处理工具调用
|
63
|
+
# 广播工具调用前事件(不影响主流程)
|
64
|
+
try:
|
65
|
+
ag.event_bus.emit(
|
66
|
+
"before_tool_call",
|
67
|
+
agent=ag,
|
68
|
+
current_response=current_response,
|
69
|
+
)
|
70
|
+
except Exception:
|
71
|
+
pass
|
72
|
+
need_return, tool_prompt = ag._call_tools(current_response)
|
73
|
+
|
74
|
+
# 将上一个提示和工具提示安全地拼接起来
|
75
|
+
prompt_parts = []
|
76
|
+
if ag.session.prompt:
|
77
|
+
prompt_parts.append(ag.session.prompt)
|
78
|
+
if tool_prompt:
|
79
|
+
prompt_parts.append(tool_prompt)
|
80
|
+
ag.session.prompt = "\n\n".join(prompt_parts)
|
81
|
+
|
82
|
+
if need_return:
|
83
|
+
return ag.session.prompt
|
84
|
+
|
85
|
+
# 执行回调
|
86
|
+
if ag.after_tool_call_cb:
|
87
|
+
ag.after_tool_call_cb(ag)
|
88
|
+
# 广播工具调用后的事件(不影响主流程)
|
89
|
+
try:
|
90
|
+
ag.event_bus.emit(
|
91
|
+
"after_tool_call",
|
92
|
+
agent=ag,
|
93
|
+
current_response=current_response,
|
94
|
+
need_return=need_return,
|
95
|
+
tool_prompt=tool_prompt,
|
96
|
+
)
|
97
|
+
except Exception:
|
98
|
+
pass
|
99
|
+
|
100
|
+
# 检查是否需要继续
|
101
|
+
if ag.session.prompt or ag.session.addon_prompt:
|
102
|
+
continue
|
103
|
+
|
104
|
+
# 检查自动完成
|
105
|
+
if ag.auto_complete and ot("!!!COMPLETE!!!") in current_response:
|
106
|
+
return ag._complete_task(auto_completed=True)
|
107
|
+
|
108
|
+
# 获取下一步用户输入
|
109
|
+
next_action = ag._get_next_user_action()
|
110
|
+
if (
|
111
|
+
next_action == "continue"
|
112
|
+
or (
|
113
|
+
isinstance(next_action, Enum)
|
114
|
+
and getattr(next_action, "value", None) == "continue"
|
115
|
+
)
|
116
|
+
):
|
117
|
+
run_input_handlers = True
|
118
|
+
continue
|
119
|
+
elif (
|
120
|
+
next_action == "complete"
|
121
|
+
or (
|
122
|
+
isinstance(next_action, Enum)
|
123
|
+
and getattr(next_action, "value", None) == "complete"
|
124
|
+
)
|
125
|
+
):
|
126
|
+
return ag._complete_task(auto_completed=False)
|
127
|
+
|
128
|
+
except Exception as e:
|
129
|
+
PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
|
130
|
+
return f"Task failed: {str(e)}"
|