linhai 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.
- linhai/__init__.py +3 -0
- linhai/__main__.py +10 -0
- linhai/agent/__init__.py +23 -0
- linhai/agent/answer.py +120 -0
- linhai/agent/callback_slot.py +74 -0
- linhai/agent/command_callback.py +177 -0
- linhai/agent/conversation.py +152 -0
- linhai/agent/conversation_save.py +58 -0
- linhai/agent/create.py +757 -0
- linhai/agent/lifecycle.py +185 -0
- linhai/agent/main.py +335 -0
- linhai/agent/message.py +467 -0
- linhai/agent/messages/__init__.py +16 -0
- linhai/agent/messages/compression.py +64 -0
- linhai/agent/messages/file_content.py +57 -0
- linhai/agent/messages/prompt.py +67 -0
- linhai/agent/messages/reasoning.py +63 -0
- linhai/agent/messages/runtime.py +31 -0
- linhai/agent/orchestration.py +797 -0
- linhai/agent/planning.py +88 -0
- linhai/agent/savable_state.py +8 -0
- linhai/agent/state_machine.py +108 -0
- linhai/agent/toolcall.py +578 -0
- linhai/agent/user_message_handler.py +44 -0
- linhai/agent/workflow.py +275 -0
- linhai/base.py +541 -0
- linhai/cl100k_base.tiktoken +100256 -0
- linhai/config.py +506 -0
- linhai/context_statistics.py +314 -0
- linhai/cron.py +168 -0
- linhai/exceptions.py +19 -0
- linhai/init/__init__.py +14 -0
- linhai/init/app.py +160 -0
- linhai/init/config_writer.py +178 -0
- linhai/init/widgets.py +118 -0
- linhai/llm.py +642 -0
- linhai/llm_manager.py +351 -0
- linhai/machine_control/__init__.py +15 -0
- linhai/machine_control/bash_host/__init__.py +5 -0
- linhai/machine_control/bash_host/bash_host.py +449 -0
- linhai/machine_control/bash_host/file.py +234 -0
- linhai/machine_control/bash_host/http.py +162 -0
- linhai/machine_control/bash_host/process.py +139 -0
- linhai/machine_control/bash_host/terminal.py +200 -0
- linhai/machine_control/ether_ghost_host/__init__.py +5 -0
- linhai/machine_control/ether_ghost_host/ether_ghost_host.py +317 -0
- linhai/machine_control/http_message.py +127 -0
- linhai/machine_control/main.py +496 -0
- linhai/machine_control/master_host/__init__.py +49 -0
- linhai/machine_control/master_host/file.py +462 -0
- linhai/machine_control/master_host/http.py +53 -0
- linhai/machine_control/master_host/master_host.py +361 -0
- linhai/machine_control/master_host/process.py +363 -0
- linhai/machine_control/master_host/terminal.py +286 -0
- linhai/machine_control/master_host/tmux_terminal.py +145 -0
- linhai/machine_control/plugin.py +88 -0
- linhai/machine_control/posix_shell/__init__.py +5 -0
- linhai/machine_control/posix_shell/posix_shell_control.py +406 -0
- linhai/machine_control/posix_shell/process.py +92 -0
- linhai/machine_control/process.py +87 -0
- linhai/machine_control/protocol.py +106 -0
- linhai/machine_control/tools.py +1040 -0
- linhai/machine_control/trojan/__init__.py +1 -0
- linhai/machine_control/trojan/shell_transport.py +218 -0
- linhai/machine_control/trojan/transport.py +167 -0
- linhai/machine_control/trojan/trojan.py +828 -0
- linhai/main.py +263 -0
- linhai/markdown_parser.py +217 -0
- linhai/multimodal.py +341 -0
- linhai/parsed_message.py +397 -0
- linhai/plugin/__init__.py +123 -0
- linhai/plugin/afk_plugin.py +39 -0
- linhai/plugin/catgirl_tone.py +64 -0
- linhai/plugin/claw.py +144 -0
- linhai/plugin/command_hints.py +319 -0
- linhai/plugin/file_operations.py +535 -0
- linhai/plugin/file_permission_plugin.py +81 -0
- linhai/plugin/helpers.py +85 -0
- linhai/plugin/message_checkers.py +768 -0
- linhai/plugin/planning.py +484 -0
- linhai/plugin/python_chore.py +133 -0
- linhai/plugin/reminder.py +128 -0
- linhai/plugin/security_config.py +268 -0
- linhai/plugin/stdio_command_checker.py +193 -0
- linhai/plugin/sudo_stdio_checker.py +115 -0
- linhai/plugin/system_message_leaning.py +88 -0
- linhai/plugin/telegram.py +285 -0
- linhai/plugin/tool_call_managers.py +369 -0
- linhai/plugin/user_reminder.py +41 -0
- linhai/prompt.py +1954 -0
- linhai/registry.py +123 -0
- linhai/sandbox.py +130 -0
- linhai/secret.py +512 -0
- linhai/task_supervisor.py +106 -0
- linhai/telegram.py +228 -0
- linhai/token_manager.py +208 -0
- linhai/tool/__init__.py +35 -0
- linhai/tool/base.py +474 -0
- linhai/tool/general.py +221 -0
- linhai/tool/main.py +198 -0
- linhai/tool/mcp_connector.py +288 -0
- linhai/tool/mcp_server_example.py +42 -0
- linhai/tool/search.py +175 -0
- linhai/tui/__init__.py +23 -0
- linhai/tui/app.py +319 -0
- linhai/tui/components.py +1304 -0
- linhai/tui/context_tab.py +412 -0
- linhai/tui/messages_list.py +284 -0
- linhai/tui/planning_tab.py +83 -0
- linhai/tui/process_tab.py +208 -0
- linhai/type_hints.py +170 -0
- linhai/utils/__init__.py +0 -0
- linhai/utils/common.py +191 -0
- linhai/utils/i18n.py +11 -0
- linhai/utils/input_parser.py +42 -0
- linhai/utils/jsonpubsub.py +216 -0
- linhai/utils/pulse_encoding.py +137 -0
- linhai/utils/streamjson.py +397 -0
- linhai/utils/token_parser.py +156 -0
- linhai/utils/tokenizer.py +120 -0
- linhai/webui/__init__.py +6 -0
- linhai/webui/agent_manager.py +393 -0
- linhai/webui/app.py +66 -0
- linhai/webui/routes.py +295 -0
- linhai/webui/schemas.py +155 -0
- linhai-0.1.0.dist-info/METADATA +33 -0
- linhai-0.1.0.dist-info/RECORD +130 -0
- linhai-0.1.0.dist-info/WHEEL +4 -0
- linhai-0.1.0.dist-info/entry_points.txt +2 -0
- linhai-0.1.0.dist-info/licenses/LICENSE +674 -0
linhai/__init__.py
ADDED
linhai/__main__.py
ADDED
linhai/agent/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Agent module for LinHai."""
|
|
2
|
+
|
|
3
|
+
from .messages import DynamicFileContentMessage
|
|
4
|
+
from .main import Agent
|
|
5
|
+
from .lifecycle import Lifecycle
|
|
6
|
+
from .workflow import context_forget_range_step1, context_forget_range_step2
|
|
7
|
+
from .answer import AgentLlm
|
|
8
|
+
from .user_message_handler import UserMessageHandler, ParsedUserMessage
|
|
9
|
+
from .command_callback import CommandCallback
|
|
10
|
+
from .state_machine import AgentStateMachine
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Agent",
|
|
14
|
+
"Lifecycle",
|
|
15
|
+
"DynamicFileContentMessage",
|
|
16
|
+
"context_forget_range_step1",
|
|
17
|
+
"context_forget_range_step2",
|
|
18
|
+
"AgentLlm",
|
|
19
|
+
"UserMessageHandler",
|
|
20
|
+
"ParsedUserMessage",
|
|
21
|
+
"CommandCallback",
|
|
22
|
+
"AgentStateMachine",
|
|
23
|
+
]
|
linhai/agent/answer.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from typing import Tuple, TYPE_CHECKING
|
|
2
|
+
import asyncio
|
|
3
|
+
from linhai.base import Answer, Message
|
|
4
|
+
from linhai.llm_manager import LlmManager
|
|
5
|
+
from linhai.parsed_message import ParsedAnswer
|
|
6
|
+
from linhai.agent.lifecycle import Lifecycle
|
|
7
|
+
from linhai.agent.messages import RuntimeMessage
|
|
8
|
+
from linhai.registry import Registry
|
|
9
|
+
from linhai.utils.common import UiNotice
|
|
10
|
+
from linhai.base import UserMessage, AssistantMessage, ToolCallMessage
|
|
11
|
+
from linhai.agent.user_message_handler import UserMessageHandler
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from linhai.agent.toolcall import AgentToolcall
|
|
15
|
+
from linhai.agent.message import AgentMessage
|
|
16
|
+
from linhai.agent.main import Agent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentLlm:
|
|
20
|
+
"""AgentLlm类,负责管理LLM调用、Answer解析和打断逻辑。"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
llm_manager: LlmManager,
|
|
25
|
+
registry: Registry,
|
|
26
|
+
toolcall_processor: "AgentToolcall",
|
|
27
|
+
message_processor: "AgentMessage",
|
|
28
|
+
):
|
|
29
|
+
"""初始化AgentLlm。
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
llm_manager: LlmManager实例
|
|
33
|
+
registry: Registry实例
|
|
34
|
+
toolcall_processor: AgentToolcall实例
|
|
35
|
+
message_processor: AgentMessage实例
|
|
36
|
+
"""
|
|
37
|
+
self.llm_manager = llm_manager
|
|
38
|
+
self.registry = registry
|
|
39
|
+
self.toolcall_processor = toolcall_processor
|
|
40
|
+
self.message_processor = message_processor
|
|
41
|
+
self._current_parsed_answer: ParsedAnswer | None = None
|
|
42
|
+
self.current_answer: Answer | None = None
|
|
43
|
+
|
|
44
|
+
async def call_and_wait_llm(self) -> Tuple[Answer, ParsedAnswer, bool]:
|
|
45
|
+
"""调用LLM并等待解析完成。
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple[Answer, ParsedAnswer, bool]: (Answer, ParsedAnswer, 是否正常完成)
|
|
49
|
+
"""
|
|
50
|
+
from .main import Agent
|
|
51
|
+
|
|
52
|
+
agent = self.registry.get_member_typechecked("agent", Agent)
|
|
53
|
+
lifecycle = agent.lifecycle
|
|
54
|
+
|
|
55
|
+
await lifecycle.before_message_generation.trigger()
|
|
56
|
+
|
|
57
|
+
answer: Answer = await self.llm_manager.answer_stream(
|
|
58
|
+
self.message_processor.get_messages()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.current_answer = answer
|
|
62
|
+
|
|
63
|
+
parsed_answer = ParsedAnswer(
|
|
64
|
+
answer, lifecycle, agent=agent, registry=self.registry
|
|
65
|
+
)
|
|
66
|
+
await parsed_answer.start_parsing()
|
|
67
|
+
await lifecycle.after_new_parsed_answer.trigger(parsed_answer)
|
|
68
|
+
await self.registry.send("parsed_agent_answer", parsed_answer)
|
|
69
|
+
|
|
70
|
+
completed_normally = await parsed_answer.wait_parsing()
|
|
71
|
+
return answer, parsed_answer, completed_normally
|
|
72
|
+
|
|
73
|
+
async def interrupt(self, agent_message: str, ui_notice: str):
|
|
74
|
+
"""打断当前Answer并添加自定义消息。
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
agent_message: 发送给agent的消息内容,放入RuntimeMessage
|
|
78
|
+
ui_notice: 发送给UI的通知内容,必须提供
|
|
79
|
+
"""
|
|
80
|
+
from .main import Agent
|
|
81
|
+
|
|
82
|
+
message_processor = self.message_processor
|
|
83
|
+
lifecycle = self.registry.get_member_typechecked("lifecycle", Lifecycle)
|
|
84
|
+
agent = self.registry.get_member_typechecked("agent", Agent)
|
|
85
|
+
|
|
86
|
+
if self._current_parsed_answer:
|
|
87
|
+
self._current_parsed_answer.interrupt()
|
|
88
|
+
|
|
89
|
+
await message_processor.add_new_message(RuntimeMessage(agent_message))
|
|
90
|
+
|
|
91
|
+
if ui_notice is not None:
|
|
92
|
+
interrupt_msg = UiNotice(level="WARNING", content=ui_notice)
|
|
93
|
+
else:
|
|
94
|
+
interrupt_msg = UiNotice(level="WARNING", content="Agent被打断")
|
|
95
|
+
|
|
96
|
+
current_content = self._current_parsed_answer._answer.get_current_content()
|
|
97
|
+
has_tool_calls = "```json toolcall" in current_content or bool(
|
|
98
|
+
self._current_parsed_answer._openai_toolcall_segments
|
|
99
|
+
)
|
|
100
|
+
if has_tool_calls:
|
|
101
|
+
await message_processor.add_new_message(
|
|
102
|
+
RuntimeMessage("当前所有工具调用全部被忽略,请重新调用")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self._current_parsed_answer = None
|
|
106
|
+
|
|
107
|
+
await self.registry.send_if_exists("ui_log", interrupt_msg)
|
|
108
|
+
|
|
109
|
+
from .state_machine import AgentStateMachine
|
|
110
|
+
|
|
111
|
+
state_machine = self.registry.get_member_typechecked(
|
|
112
|
+
"state_machine", AgentStateMachine
|
|
113
|
+
)
|
|
114
|
+
state_machine.transition_to_working()
|
|
115
|
+
|
|
116
|
+
user_message_handler = self.registry.get_member_typechecked(
|
|
117
|
+
"user_message_handler", UserMessageHandler
|
|
118
|
+
)
|
|
119
|
+
while user_message_handler.has_message():
|
|
120
|
+
await user_message_handler.receive_and_dispatch()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Generic, TypeVar, Callable, Awaitable
|
|
2
|
+
|
|
3
|
+
R = TypeVar("R")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CallbackSlot(Generic[R]):
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._callbacks: list[Callable[..., Awaitable[R]]] = []
|
|
9
|
+
|
|
10
|
+
def register(self, callback: Callable[..., Awaitable[R]]) -> None:
|
|
11
|
+
self._callbacks.append(callback)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BroadcastSlot(CallbackSlot[None]):
|
|
15
|
+
async def trigger(self, *args, **kwargs) -> None:
|
|
16
|
+
for callback in self._callbacks:
|
|
17
|
+
await callback(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ShortCircuitSlot(CallbackSlot[R]):
|
|
21
|
+
async def trigger(self, *args, **kwargs) -> R | None:
|
|
22
|
+
for callback in self._callbacks:
|
|
23
|
+
result = await callback(*args, **kwargs)
|
|
24
|
+
if result is not None:
|
|
25
|
+
return result
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InterruptSlot(CallbackSlot[bool]):
|
|
30
|
+
async def trigger(self, *args, **kwargs) -> bool:
|
|
31
|
+
for callback in self._callbacks:
|
|
32
|
+
if await callback(*args, **kwargs):
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AfterToolcallSlot(CallbackSlot):
|
|
38
|
+
async def trigger(self, *args, **kwargs):
|
|
39
|
+
from linhai.agent.lifecycle import AfterToolcallResult
|
|
40
|
+
|
|
41
|
+
replacement = None
|
|
42
|
+
warnings = []
|
|
43
|
+
for callback in self._callbacks:
|
|
44
|
+
result = await callback(*args, **kwargs)
|
|
45
|
+
if result is None:
|
|
46
|
+
continue
|
|
47
|
+
if result.replacement is not None and replacement is None:
|
|
48
|
+
replacement = result.replacement
|
|
49
|
+
warnings.extend(result.warnings)
|
|
50
|
+
if replacement is None and not warnings:
|
|
51
|
+
return None
|
|
52
|
+
return AfterToolcallResult(replacement=replacement, warnings=warnings)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ChainSlot(CallbackSlot[R]):
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
chain_arg: int = 0,
|
|
60
|
+
should_stop: Callable[..., bool] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__()
|
|
63
|
+
self._chain_arg = chain_arg
|
|
64
|
+
self._should_stop = should_stop
|
|
65
|
+
|
|
66
|
+
async def trigger(self, *args, **kwargs) -> R:
|
|
67
|
+
args_list = list(args)
|
|
68
|
+
for callback in self._callbacks:
|
|
69
|
+
result = await callback(*args_list, **kwargs)
|
|
70
|
+
if result is not None:
|
|
71
|
+
args_list[self._chain_arg] = result
|
|
72
|
+
if self._should_stop is not None and self._should_stop(result):
|
|
73
|
+
break
|
|
74
|
+
return args_list[self._chain_arg]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from linhai.base import UserMessage, ToolCallMessage
|
|
4
|
+
from linhai.registry import Registry
|
|
5
|
+
from linhai.utils.common import UiNotice
|
|
6
|
+
from linhai.agent.user_message_handler import ParsedUserMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CommandCallback:
|
|
10
|
+
def __init__(self, registry: Registry):
|
|
11
|
+
self.registry = registry
|
|
12
|
+
|
|
13
|
+
async def __call__(self, parsed: ParsedUserMessage) -> bool | None:
|
|
14
|
+
parsed_input = parsed["parsed_input"]
|
|
15
|
+
msg = parsed["raw_message"]
|
|
16
|
+
|
|
17
|
+
if parsed_input.switch_model:
|
|
18
|
+
return await self._handle_switch_model(parsed_input.switch_model, msg)
|
|
19
|
+
|
|
20
|
+
if parsed_input.command:
|
|
21
|
+
if parsed_input.command == "context_forget_large_message":
|
|
22
|
+
return await self._handle_context_tool_command(parsed_input)
|
|
23
|
+
if parsed_input.command == "queue":
|
|
24
|
+
return await self._handle_queue_command(msg)
|
|
25
|
+
if parsed_input.command in ("quit", "exit"):
|
|
26
|
+
return await self._handle_quit_command()
|
|
27
|
+
if parsed_input.command == "help":
|
|
28
|
+
return await self._handle_help_command()
|
|
29
|
+
if parsed_input.command == "status":
|
|
30
|
+
return await self._handle_status_command()
|
|
31
|
+
if parsed_input.command == "save":
|
|
32
|
+
return await self._handle_save_command()
|
|
33
|
+
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def get_command_completions() -> list[str]:
|
|
38
|
+
return [
|
|
39
|
+
"/queue",
|
|
40
|
+
"/help",
|
|
41
|
+
"/status",
|
|
42
|
+
"/save",
|
|
43
|
+
"/quit",
|
|
44
|
+
"/exit",
|
|
45
|
+
"/context_forget_large_message",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
async def _show_runtime_message(
|
|
49
|
+
self, level: Literal["INFO", "WARNING", "ERROR"], content: str
|
|
50
|
+
) -> None:
|
|
51
|
+
await self.registry.send_if_exists(
|
|
52
|
+
"ui_log", UiNotice(level=level, content=content)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def _handle_queue_command(self, msg: UserMessage) -> bool:
|
|
56
|
+
from .message import AgentMessage
|
|
57
|
+
|
|
58
|
+
agent_message = self.registry.get_member_typechecked(
|
|
59
|
+
"agent_message", AgentMessage
|
|
60
|
+
)
|
|
61
|
+
queue_content = msg.message.removeprefix("/queue").strip()
|
|
62
|
+
if not queue_content:
|
|
63
|
+
await self._show_runtime_message("ERROR", "用法: /queue <消息内容>")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
queued_msg = UserMessage(message=queue_content)
|
|
67
|
+
agent_message.add_queued_message(queued_msg)
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
async def _handle_quit_command(self) -> bool:
|
|
71
|
+
await self.registry.send("exit_signal", {"return_code": 0})
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
async def _handle_help_command(self) -> bool:
|
|
75
|
+
help_text = (
|
|
76
|
+
"可用命令:\n"
|
|
77
|
+
"/queue <消息> - 将消息加入排队列表,在下次回答后处理\n"
|
|
78
|
+
"\n"
|
|
79
|
+
"/status - 显示当前状态信息\n"
|
|
80
|
+
"/save - 保存当前会话状态\n"
|
|
81
|
+
"/help - 显示此帮助信息\n"
|
|
82
|
+
"/quit, /exit - 退出程序\n"
|
|
83
|
+
"@<模型名> - 切换底层LLM模型\n"
|
|
84
|
+
"\n"
|
|
85
|
+
"上下文工具:\n"
|
|
86
|
+
"/context_forget_large_message - 清理大消息\n"
|
|
87
|
+
)
|
|
88
|
+
await self._show_runtime_message("INFO", help_text)
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
async def _handle_status_command(self) -> bool:
|
|
92
|
+
from .main import Agent
|
|
93
|
+
|
|
94
|
+
agent = self.registry.get_member_typechecked("agent", Agent)
|
|
95
|
+
llm_name, _llm = agent.get_current_llm_info()
|
|
96
|
+
threshold_info = agent.get_threshold_info()
|
|
97
|
+
|
|
98
|
+
from .state_machine import AgentStateMachine
|
|
99
|
+
|
|
100
|
+
state_machine = self.registry.get_member_typechecked(
|
|
101
|
+
"state_machine", AgentStateMachine
|
|
102
|
+
)
|
|
103
|
+
status_lines = [f"当前LLM: {llm_name}", f"当前状态: {state_machine.state}"]
|
|
104
|
+
|
|
105
|
+
if threshold_info:
|
|
106
|
+
usage_percent = threshold_info["usage_ratio"] * 100
|
|
107
|
+
status_lines.append(
|
|
108
|
+
f"Token使用: {threshold_info['used_tokens']}/{threshold_info['hard_limit']} ({usage_percent:.1f}%)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
await self._show_runtime_message("INFO", "\n".join(status_lines))
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
async def _handle_switch_model(self, model_name: str, msg: UserMessage) -> bool:
|
|
115
|
+
from .main import Agent
|
|
116
|
+
|
|
117
|
+
agent = self.registry.get_member_typechecked("agent", Agent)
|
|
118
|
+
llm_manager = agent.llm_manager
|
|
119
|
+
|
|
120
|
+
await agent.message_processor.add_new_message(msg)
|
|
121
|
+
|
|
122
|
+
if model_name == "default":
|
|
123
|
+
await llm_manager.switch_to_llm(llm_manager.default_llm_name)
|
|
124
|
+
await self._show_runtime_message("INFO", "已将底层LLM切换为默认LLM")
|
|
125
|
+
elif model_name in llm_manager.llm_names:
|
|
126
|
+
await llm_manager.switch_to_llm(model_name)
|
|
127
|
+
await self._show_runtime_message(
|
|
128
|
+
"INFO", f"已将底层LLM切换为 {model_name!r}"
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
await self._show_runtime_message(
|
|
132
|
+
"ERROR",
|
|
133
|
+
f"错误:LLM名称 {model_name!r} 不存在.可用的LLM包括: {', '.join(llm_manager.llm_names)}",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
async def _handle_save_command(self) -> bool:
|
|
139
|
+
from pathlib import Path
|
|
140
|
+
from linhai.agent.conversation_save import save_conversation
|
|
141
|
+
|
|
142
|
+
conversation_folder = self.registry.get_member_typechecked(
|
|
143
|
+
"conversation_folder", Path
|
|
144
|
+
)
|
|
145
|
+
filepath = conversation_folder / "saved.json"
|
|
146
|
+
|
|
147
|
+
await self._show_runtime_message(
|
|
148
|
+
"INFO", f"Saving conversation to {filepath}..."
|
|
149
|
+
)
|
|
150
|
+
await save_conversation(self.registry, filepath)
|
|
151
|
+
await self._show_runtime_message("INFO", f"Conversation saved to {filepath}")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
async def _handle_context_tool_command(self, parsed_input) -> bool:
|
|
155
|
+
from .main import Agent
|
|
156
|
+
|
|
157
|
+
agent = self.registry.get_member_typechecked("agent", Agent)
|
|
158
|
+
|
|
159
|
+
await self._show_runtime_message(
|
|
160
|
+
"INFO", f"正在执行命令: {parsed_input.command}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
tool_call = ToolCallMessage(
|
|
164
|
+
function_name=parsed_input.command,
|
|
165
|
+
function_arguments={},
|
|
166
|
+
assert_success=False,
|
|
167
|
+
with_secret={"in_arguments": [], "in_result": []},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
await agent.toolcall_processor.call_tool(tool_call, tool_index=1)
|
|
171
|
+
|
|
172
|
+
await self._show_runtime_message(
|
|
173
|
+
"INFO",
|
|
174
|
+
f"上下文工具命令执行成功: {parsed_input.command}",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return True
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""对话管理系统,负责对话历史、大消息、secret等的保存。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from linhai.registry import Registry
|
|
10
|
+
from linhai.base import Message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_conversation_folder(registry: Registry) -> Path:
|
|
14
|
+
"""注册conversation_folder到registry,创建并返回对话目录路径。
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
registry: Registry实例
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
创建的对话目录路径
|
|
21
|
+
"""
|
|
22
|
+
conversation_id = str(uuid.uuid4())
|
|
23
|
+
base_dir = Path.home() / ".local" / "share" / "linhai" / "conversation"
|
|
24
|
+
conversation_dir = base_dir / conversation_id
|
|
25
|
+
conversation_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
(conversation_dir / "cleaned_messages").mkdir(exist_ok=True)
|
|
28
|
+
(conversation_dir / "large_messages").mkdir(exist_ok=True)
|
|
29
|
+
(conversation_dir / "long_toolcall").mkdir(exist_ok=True)
|
|
30
|
+
(conversation_dir / "secret_intercepted").mkdir(exist_ok=True)
|
|
31
|
+
(conversation_dir / "cron").mkdir(exist_ok=True)
|
|
32
|
+
|
|
33
|
+
registry.register_member("conversation_folder", conversation_dir)
|
|
34
|
+
return conversation_dir
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save_context(conversation_dir: Path, messages: List[Message]) -> Path:
|
|
38
|
+
"""保存消息历史到context.json。
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
conversation_dir: 对话目录路径
|
|
42
|
+
messages: 消息列表
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
保存的文件路径
|
|
46
|
+
"""
|
|
47
|
+
context_file = conversation_dir / "context.json"
|
|
48
|
+
|
|
49
|
+
history_data = [
|
|
50
|
+
{"type": msg.__class__.__name__, "data": msg.to_json()} for msg in messages
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
with open(context_file, "w", encoding="utf-8") as f:
|
|
54
|
+
json.dump(history_data, f, ensure_ascii=False, indent=2)
|
|
55
|
+
|
|
56
|
+
return context_file
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_cleaned_messages(
|
|
60
|
+
conversation_dir: Path, messages: List[Message], prefix: str
|
|
61
|
+
) -> Path:
|
|
62
|
+
"""保存被清理的消息到cleaned_messages目录。
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
conversation_dir: 对话目录路径
|
|
66
|
+
messages: 被清理的消息列表
|
|
67
|
+
prefix: 文件名前缀
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
保存的文件路径
|
|
71
|
+
"""
|
|
72
|
+
timestamp = int(time.time())
|
|
73
|
+
filename = f"{prefix}_{timestamp}.json"
|
|
74
|
+
cleaned_messages_dir = conversation_dir / "cleaned_messages"
|
|
75
|
+
cleaned_messages_dir.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
filepath = cleaned_messages_dir / filename
|
|
77
|
+
|
|
78
|
+
history_data = [
|
|
79
|
+
{"type": msg.__class__.__name__, "data": msg.to_json()} for msg in messages
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
83
|
+
json.dump(history_data, f, ensure_ascii=False, indent=2)
|
|
84
|
+
|
|
85
|
+
return filepath
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def save_large_message_chunk(
|
|
89
|
+
conversation_dir: Path, content: str, chunk_index: int
|
|
90
|
+
) -> Path:
|
|
91
|
+
"""保存大消息分块到large_messages目录。
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
conversation_dir: 对话目录路径
|
|
95
|
+
content: 消息内容
|
|
96
|
+
chunk_index: 分块索引
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
保存的文件路径
|
|
100
|
+
"""
|
|
101
|
+
timestamp = int(time.time())
|
|
102
|
+
filename = f"large_message_{timestamp}_{chunk_index}.txt"
|
|
103
|
+
filepath = conversation_dir / "large_messages" / filename
|
|
104
|
+
|
|
105
|
+
filepath.write_text(content, encoding="utf-8")
|
|
106
|
+
return filepath
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def save_long_toolcall_output(
|
|
110
|
+
conversation_dir: Path, content: str, tool_name: str, part_index: int | None = None
|
|
111
|
+
) -> Path:
|
|
112
|
+
"""保存大工具输出到long_toolcall目录。
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
conversation_dir: 对话目录路径
|
|
116
|
+
content: 输出内容
|
|
117
|
+
tool_name: 工具名称
|
|
118
|
+
part_index: 分块索引,None表示不分块
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
保存的文件路径
|
|
122
|
+
"""
|
|
123
|
+
timestamp = int(time.time())
|
|
124
|
+
if part_index is not None:
|
|
125
|
+
filename = f"{tool_name}_{timestamp}_part{part_index}.txt"
|
|
126
|
+
else:
|
|
127
|
+
filename = f"{tool_name}_{timestamp}.txt"
|
|
128
|
+
filepath = conversation_dir / "long_toolcall" / filename
|
|
129
|
+
|
|
130
|
+
filepath.write_text(content, encoding="utf-8")
|
|
131
|
+
return filepath
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_secret_intercepted(
|
|
135
|
+
conversation_dir: Path, content: str, tool_name: str
|
|
136
|
+
) -> Path:
|
|
137
|
+
"""保存被拦截的含secret内容到secret_intercepted目录。
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
conversation_dir: 对话目录路径
|
|
141
|
+
content: 被拦截的内容
|
|
142
|
+
tool_name: 工具名称
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
保存的文件路径
|
|
146
|
+
"""
|
|
147
|
+
timestamp = int(time.time())
|
|
148
|
+
filename = f"secret_intercepted_{timestamp}_{tool_name}.txt"
|
|
149
|
+
filepath = conversation_dir / "secret_intercepted" / filename
|
|
150
|
+
|
|
151
|
+
filepath.write_text(content, encoding="utf-8")
|
|
152
|
+
return filepath
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from linhai.agent.savable_state import SavableState
|
|
5
|
+
|
|
6
|
+
CONVERSATION_VERSION = "1"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_savable_members(registry) -> dict[str, SavableState]:
|
|
10
|
+
result = {}
|
|
11
|
+
for name, obj in registry.members.items():
|
|
12
|
+
if isinstance(obj, SavableState):
|
|
13
|
+
result[name] = obj
|
|
14
|
+
return result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def save_conversation(registry, filepath: Path) -> None:
|
|
18
|
+
savable = _get_savable_members(registry)
|
|
19
|
+
data = {
|
|
20
|
+
"version": CONVERSATION_VERSION,
|
|
21
|
+
"members": {name: obj.serialize() for name, obj in savable.items()},
|
|
22
|
+
}
|
|
23
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
filepath.write_text(
|
|
25
|
+
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def restore_conversation(registry, filepath: Path) -> None:
|
|
30
|
+
content = filepath.read_text(encoding="utf-8")
|
|
31
|
+
data = json.loads(content)
|
|
32
|
+
|
|
33
|
+
if data.get("version") != CONVERSATION_VERSION:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"conversation version mismatch: expected {CONVERSATION_VERSION!r}, got {data.get('version')!r}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
saved_members = set(data.get("members", {}).keys())
|
|
39
|
+
savable = _get_savable_members(registry)
|
|
40
|
+
current_members = set(savable.keys())
|
|
41
|
+
|
|
42
|
+
missing = current_members - saved_members
|
|
43
|
+
extra = saved_members - current_members
|
|
44
|
+
if missing or extra:
|
|
45
|
+
parts = []
|
|
46
|
+
if missing:
|
|
47
|
+
parts.append(f"missing members: {sorted(missing)}")
|
|
48
|
+
if extra:
|
|
49
|
+
parts.append(f"extra members: {sorted(extra)}")
|
|
50
|
+
raise RuntimeError("conversation restore failed: " + ", ".join(parts))
|
|
51
|
+
|
|
52
|
+
for name, obj in savable.items():
|
|
53
|
+
obj.restore_from(data["members"][name])
|
|
54
|
+
|
|
55
|
+
from linhai.agent.lifecycle import Lifecycle
|
|
56
|
+
|
|
57
|
+
lifecycle = registry.get_member_typechecked("lifecycle", Lifecycle)
|
|
58
|
+
await lifecycle.after_conversation_restore.trigger()
|