qa296 commited on
Commit ·
6d49dc7
1
Parent(s): 3281c27
refactor: standardize type hints and improve null safety across codebase
Browse files- Update type annotations to use modern `dict[str, Any]` syntax instead of bare `dict`
- Add `_require_*` helper methods in main.py and browser/client.py for safe attribute access
- Fix category parameter validation in remember tool to ensure string type
- Refactor LLM client response handling to use standardized content block format
- Fix bug in browser_action where local variable wasn't used
- Fix missing brackets in tool result logging
- Remove unused imports throughout (datetime, os, subprocess, Path, logger)
- Update category parameter type hints to use `str | None = None` syntax
- TODO.md +0 -18
- agent/agent_loop.py +6 -3
- agent/context_manager.py +15 -10
- agent/llm_client.py +31 -25
- agent/message_history.py +0 -1
- app.py +9 -9
- browser/client.py +27 -10
- main.py +44 -18
- memory/manager.py +1 -1
- memory/storage.py +1 -1
- tools/bash_tool.py +6 -5
- tools/code_executor.py +0 -1
- tools/file_tools.py +0 -1
- tools/memory_tools.py +1 -1
- utils/token_counter.py +3 -1
TODO.md
CHANGED
|
@@ -2,18 +2,6 @@
|
|
| 2 |
|
| 3 |
## 优先级排序
|
| 4 |
|
| 5 |
-
### 1. 提示词工程(最高优先级)⭐
|
| 6 |
-
**状态**: 已添加"诚实原则",需要验证和优化
|
| 7 |
-
**问题**: 从运行日志看,数字生命还在编造结果(假装成功、实际失败)
|
| 8 |
-
**需要**:
|
| 9 |
-
- 验证"诚实原则"是否生效
|
| 10 |
-
- 可能需要更强的提示或示例
|
| 11 |
-
- 可能需要添加"验证工具"来强制检查结果
|
| 12 |
-
|
| 13 |
-
**成功标准**:
|
| 14 |
-
- 浏览器崩溃时,它如实说"失败了"
|
| 15 |
-
- 插件加载失败时,不说"成功创建"
|
| 16 |
-
- API调用失败时,说"无法获取",不编造数据
|
| 17 |
|
| 18 |
### 2. 插件系统完善
|
| 19 |
**状态**: 框架已有,但有问题
|
|
@@ -56,12 +44,6 @@
|
|
| 56 |
**描述**: `receive_stimulus()` 需要一个入口
|
| 57 |
**可能方案**: 前端控制台的"发送刺激"功能
|
| 58 |
|
| 59 |
-
### 5. 清理错误记忆
|
| 60 |
-
**状态**: 待办
|
| 61 |
-
**描述**: 数字生命之前存储了很多"虚假成功"的记忆
|
| 62 |
-
**需要**: 清理或标记这些记忆为"不可靠"
|
| 63 |
-
|
| 64 |
-
---
|
| 65 |
|
| 66 |
## 已完成
|
| 67 |
|
|
|
|
| 2 |
|
| 3 |
## 优先级排序
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
### 2. 插件系统完善
|
| 7 |
**状态**: 框架已有,但有问题
|
|
|
|
| 44 |
**描述**: `receive_stimulus()` 需要一个入口
|
| 45 |
**可能方案**: 前端控制台的"发送刺激"功能
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
## 已完成
|
| 49 |
|
agent/agent_loop.py
CHANGED
|
@@ -7,12 +7,12 @@ from pathlib import Path
|
|
| 7 |
from loguru import logger
|
| 8 |
|
| 9 |
from agent.context_manager import ContextManager
|
| 10 |
-
from agent.llm_client import BaseLLMClient
|
| 11 |
from tools.bash_tool import run_bash
|
| 12 |
from tools.file_tools import run_read, run_write, run_edit
|
| 13 |
from tools.code_executor import run_create_plugin
|
| 14 |
from tools.browser_tool import run_browser
|
| 15 |
-
from tools.memory_tools import
|
| 16 |
|
| 17 |
|
| 18 |
# Tool definitions sent to the API
|
|
@@ -246,9 +246,12 @@ class AgentLoop:
|
|
| 246 |
args["path"], args["old_text"], args["new_text"], workdir=self.workdir
|
| 247 |
)
|
| 248 |
elif name == "remember":
|
|
|
|
|
|
|
|
|
|
| 249 |
return await run_remember(
|
| 250 |
args["content"],
|
| 251 |
-
category=
|
| 252 |
)
|
| 253 |
elif name == "recall":
|
| 254 |
return await run_recall(args["query"])
|
|
|
|
| 7 |
from loguru import logger
|
| 8 |
|
| 9 |
from agent.context_manager import ContextManager
|
| 10 |
+
from agent.llm_client import BaseLLMClient
|
| 11 |
from tools.bash_tool import run_bash
|
| 12 |
from tools.file_tools import run_read, run_write, run_edit
|
| 13 |
from tools.code_executor import run_create_plugin
|
| 14 |
from tools.browser_tool import run_browser
|
| 15 |
+
from tools.memory_tools import run_recall, run_remember
|
| 16 |
|
| 17 |
|
| 18 |
# Tool definitions sent to the API
|
|
|
|
| 246 |
args["path"], args["old_text"], args["new_text"], workdir=self.workdir
|
| 247 |
)
|
| 248 |
elif name == "remember":
|
| 249 |
+
category = args.get("category")
|
| 250 |
+
if category is not None and not isinstance(category, str):
|
| 251 |
+
return "Error: category must be a string"
|
| 252 |
return await run_remember(
|
| 253 |
args["content"],
|
| 254 |
+
category=category,
|
| 255 |
)
|
| 256 |
elif name == "recall":
|
| 257 |
return await run_recall(args["query"])
|
agent/context_manager.py
CHANGED
|
@@ -2,9 +2,9 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import anthropic
|
| 6 |
from loguru import logger
|
| 7 |
|
|
|
|
| 8 |
from utils.token_counter import estimate_messages_tokens
|
| 9 |
|
| 10 |
|
|
@@ -13,7 +13,7 @@ class ContextManager:
|
|
| 13 |
|
| 14 |
def __init__(
|
| 15 |
self,
|
| 16 |
-
client:
|
| 17 |
model: str,
|
| 18 |
context_window: int = 200000,
|
| 19 |
compact_threshold: float = 0.9,
|
|
@@ -134,10 +134,9 @@ class ContextManager:
|
|
| 134 |
conversation_text = conversation_text[:100000] + "\n\n... (truncated)"
|
| 135 |
|
| 136 |
try:
|
| 137 |
-
response = await self.client.
|
| 138 |
model=self.model,
|
| 139 |
-
|
| 140 |
-
system=(
|
| 141 |
"You are a conversation summarizer. Create a concise but comprehensive "
|
| 142 |
"summary of the following conversation. Preserve:\n"
|
| 143 |
"- Key decisions made\n"
|
|
@@ -152,14 +151,20 @@ class ContextManager:
|
|
| 152 |
{
|
| 153 |
"role": "user",
|
| 154 |
"content": f"Summarize this conversation:\n\n{conversation_text}",
|
| 155 |
-
}
|
| 156 |
],
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
|
| 159 |
-
|
| 160 |
-
for block in response.
|
| 161 |
-
if
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
return summary or "No summary generated."
|
| 165 |
except Exception as e:
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from loguru import logger
|
| 6 |
|
| 7 |
+
from agent.llm_client import BaseLLMClient
|
| 8 |
from utils.token_counter import estimate_messages_tokens
|
| 9 |
|
| 10 |
|
|
|
|
| 13 |
|
| 14 |
def __init__(
|
| 15 |
self,
|
| 16 |
+
client: BaseLLMClient,
|
| 17 |
model: str,
|
| 18 |
context_window: int = 200000,
|
| 19 |
compact_threshold: float = 0.9,
|
|
|
|
| 134 |
conversation_text = conversation_text[:100000] + "\n\n... (truncated)"
|
| 135 |
|
| 136 |
try:
|
| 137 |
+
response = await self.client.create_message(
|
| 138 |
model=self.model,
|
| 139 |
+
system_prompt=(
|
|
|
|
| 140 |
"You are a conversation summarizer. Create a concise but comprehensive "
|
| 141 |
"summary of the following conversation. Preserve:\n"
|
| 142 |
"- Key decisions made\n"
|
|
|
|
| 151 |
{
|
| 152 |
"role": "user",
|
| 153 |
"content": f"Summarize this conversation:\n\n{conversation_text}",
|
| 154 |
+
},
|
| 155 |
],
|
| 156 |
+
tools=[],
|
| 157 |
+
max_tokens=4000,
|
| 158 |
)
|
| 159 |
|
| 160 |
+
summary_parts: list[str] = []
|
| 161 |
+
for block in response.content_blocks:
|
| 162 |
+
if isinstance(block, dict) and block.get("type") == "text":
|
| 163 |
+
text = block.get("text")
|
| 164 |
+
if isinstance(text, str):
|
| 165 |
+
summary_parts.append(text)
|
| 166 |
+
|
| 167 |
+
summary = "".join(summary_parts)
|
| 168 |
|
| 169 |
return summary or "No summary generated."
|
| 170 |
except Exception as e:
|
agent/llm_client.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
import os
|
| 6 |
from abc import ABC, abstractmethod
|
| 7 |
-
from typing import Any
|
| 8 |
|
| 9 |
from loguru import logger
|
| 10 |
|
|
@@ -17,8 +17,8 @@ class BaseLLMClient(ABC):
|
|
| 17 |
self,
|
| 18 |
model: str,
|
| 19 |
system_prompt: str,
|
| 20 |
-
messages: list[dict],
|
| 21 |
-
tools: list[dict],
|
| 22 |
max_tokens: int,
|
| 23 |
) -> LLMResponse:
|
| 24 |
"""Create a message and return standardized response."""
|
|
@@ -30,7 +30,7 @@ class LLMResponse:
|
|
| 30 |
|
| 31 |
def __init__(
|
| 32 |
self,
|
| 33 |
-
content_blocks: list[dict],
|
| 34 |
stop_reason: str,
|
| 35 |
tool_calls: list[ToolCall] | None = None,
|
| 36 |
):
|
|
@@ -42,7 +42,7 @@ class LLMResponse:
|
|
| 42 |
class ToolCall:
|
| 43 |
"""Standardized tool call."""
|
| 44 |
|
| 45 |
-
def __init__(self, id: str, name: str, input: dict):
|
| 46 |
self.id = id
|
| 47 |
self.name = name
|
| 48 |
self.input = input
|
|
@@ -59,16 +59,16 @@ class AnthropicClient(BaseLLMClient):
|
|
| 59 |
self,
|
| 60 |
model: str,
|
| 61 |
system_prompt: str,
|
| 62 |
-
messages: list[dict],
|
| 63 |
-
tools: list[dict],
|
| 64 |
max_tokens: int,
|
| 65 |
) -> LLMResponse:
|
| 66 |
"""Create message using Anthropic API."""
|
| 67 |
response = await self._client.messages.create(
|
| 68 |
model=model,
|
| 69 |
system=system_prompt,
|
| 70 |
-
messages=messages,
|
| 71 |
-
tools=tools,
|
| 72 |
max_tokens=max_tokens,
|
| 73 |
)
|
| 74 |
|
|
@@ -92,7 +92,7 @@ class AnthropicClient(BaseLLMClient):
|
|
| 92 |
|
| 93 |
return LLMResponse(
|
| 94 |
content_blocks=content_blocks,
|
| 95 |
-
stop_reason=response.stop_reason,
|
| 96 |
tool_calls=tool_calls,
|
| 97 |
)
|
| 98 |
|
|
@@ -111,8 +111,8 @@ class OpenAIClient(BaseLLMClient):
|
|
| 111 |
self,
|
| 112 |
model: str,
|
| 113 |
system_prompt: str,
|
| 114 |
-
messages: list[dict],
|
| 115 |
-
tools: list[dict],
|
| 116 |
max_tokens: int,
|
| 117 |
) -> LLMResponse:
|
| 118 |
"""Create message using OpenAI-compatible API."""
|
|
@@ -120,7 +120,9 @@ class OpenAIClient(BaseLLMClient):
|
|
| 120 |
openai_tools = self._convert_tools(tools)
|
| 121 |
|
| 122 |
# Build message list with system prompt
|
| 123 |
-
all_messages
|
|
|
|
|
|
|
| 124 |
|
| 125 |
# Convert messages
|
| 126 |
for msg in messages:
|
|
@@ -130,8 +132,8 @@ class OpenAIClient(BaseLLMClient):
|
|
| 130 |
|
| 131 |
response = await self._client.chat.completions.create(
|
| 132 |
model=model,
|
| 133 |
-
messages=all_messages,
|
| 134 |
-
tools=openai_tools if openai_tools else None,
|
| 135 |
max_tokens=max_tokens,
|
| 136 |
)
|
| 137 |
|
|
@@ -147,16 +149,20 @@ class OpenAIClient(BaseLLMClient):
|
|
| 147 |
if choice.message.tool_calls:
|
| 148 |
for tc in choice.message.tool_calls:
|
| 149 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
tool_calls.append(ToolCall(
|
| 151 |
-
id=
|
| 152 |
-
name=
|
| 153 |
-
input=json.loads(
|
| 154 |
))
|
| 155 |
content_blocks.append({
|
| 156 |
"type": "tool_use",
|
| 157 |
-
"id":
|
| 158 |
-
"name":
|
| 159 |
-
"input": json.loads(
|
| 160 |
})
|
| 161 |
|
| 162 |
stop_reason = "tool_use" if tool_calls else "end_turn"
|
|
@@ -169,7 +175,7 @@ class OpenAIClient(BaseLLMClient):
|
|
| 169 |
tool_calls=tool_calls,
|
| 170 |
)
|
| 171 |
|
| 172 |
-
def _convert_tools(self, anthropic_tools: list[dict]) -> list[dict]:
|
| 173 |
"""Convert Anthropic-style tools to OpenAI format."""
|
| 174 |
openai_tools = []
|
| 175 |
for tool in anthropic_tools:
|
|
@@ -183,7 +189,7 @@ class OpenAIClient(BaseLLMClient):
|
|
| 183 |
})
|
| 184 |
return openai_tools
|
| 185 |
|
| 186 |
-
def _convert_message(self, msg: dict) -> dict | None:
|
| 187 |
"""Convert Anthropic-style message to OpenAI format."""
|
| 188 |
role = msg.get("role")
|
| 189 |
content = msg.get("content")
|
|
@@ -204,7 +210,7 @@ class OpenAIClient(BaseLLMClient):
|
|
| 204 |
|
| 205 |
if tool_results:
|
| 206 |
# Convert tool results to OpenAI format
|
| 207 |
-
result_msg = {"role": "tool", "content": ""}
|
| 208 |
for tr in tool_results:
|
| 209 |
result_msg["tool_call_id"] = tr.get("tool_use_id", "")
|
| 210 |
result_msg["content"] = tr.get("content", "")
|
|
@@ -225,7 +231,7 @@ class OpenAIClient(BaseLLMClient):
|
|
| 225 |
elif block.get("type") == "tool_use":
|
| 226 |
tool_uses.append(block)
|
| 227 |
|
| 228 |
-
result = {"role": "assistant"}
|
| 229 |
if texts:
|
| 230 |
result["content"] = "\n".join(texts)
|
| 231 |
if tool_uses:
|
|
|
|
| 4 |
|
| 5 |
import os
|
| 6 |
from abc import ABC, abstractmethod
|
| 7 |
+
from typing import Any, cast
|
| 8 |
|
| 9 |
from loguru import logger
|
| 10 |
|
|
|
|
| 17 |
self,
|
| 18 |
model: str,
|
| 19 |
system_prompt: str,
|
| 20 |
+
messages: list[dict[str, Any]],
|
| 21 |
+
tools: list[dict[str, Any]],
|
| 22 |
max_tokens: int,
|
| 23 |
) -> LLMResponse:
|
| 24 |
"""Create a message and return standardized response."""
|
|
|
|
| 30 |
|
| 31 |
def __init__(
|
| 32 |
self,
|
| 33 |
+
content_blocks: list[dict[str, Any]],
|
| 34 |
stop_reason: str,
|
| 35 |
tool_calls: list[ToolCall] | None = None,
|
| 36 |
):
|
|
|
|
| 42 |
class ToolCall:
|
| 43 |
"""Standardized tool call."""
|
| 44 |
|
| 45 |
+
def __init__(self, id: str, name: str, input: dict[str, Any]):
|
| 46 |
self.id = id
|
| 47 |
self.name = name
|
| 48 |
self.input = input
|
|
|
|
| 59 |
self,
|
| 60 |
model: str,
|
| 61 |
system_prompt: str,
|
| 62 |
+
messages: list[dict[str, Any]],
|
| 63 |
+
tools: list[dict[str, Any]],
|
| 64 |
max_tokens: int,
|
| 65 |
) -> LLMResponse:
|
| 66 |
"""Create message using Anthropic API."""
|
| 67 |
response = await self._client.messages.create(
|
| 68 |
model=model,
|
| 69 |
system=system_prompt,
|
| 70 |
+
messages=cast(Any, messages),
|
| 71 |
+
tools=cast(Any, tools),
|
| 72 |
max_tokens=max_tokens,
|
| 73 |
)
|
| 74 |
|
|
|
|
| 92 |
|
| 93 |
return LLMResponse(
|
| 94 |
content_blocks=content_blocks,
|
| 95 |
+
stop_reason=str(response.stop_reason or "end_turn"),
|
| 96 |
tool_calls=tool_calls,
|
| 97 |
)
|
| 98 |
|
|
|
|
| 111 |
self,
|
| 112 |
model: str,
|
| 113 |
system_prompt: str,
|
| 114 |
+
messages: list[dict[str, Any]],
|
| 115 |
+
tools: list[dict[str, Any]],
|
| 116 |
max_tokens: int,
|
| 117 |
) -> LLMResponse:
|
| 118 |
"""Create message using OpenAI-compatible API."""
|
|
|
|
| 120 |
openai_tools = self._convert_tools(tools)
|
| 121 |
|
| 122 |
# Build message list with system prompt
|
| 123 |
+
all_messages: list[dict[str, Any]] = [
|
| 124 |
+
{"role": "system", "content": system_prompt}
|
| 125 |
+
]
|
| 126 |
|
| 127 |
# Convert messages
|
| 128 |
for msg in messages:
|
|
|
|
| 132 |
|
| 133 |
response = await self._client.chat.completions.create(
|
| 134 |
model=model,
|
| 135 |
+
messages=cast(Any, all_messages),
|
| 136 |
+
tools=cast(Any, openai_tools if openai_tools else None),
|
| 137 |
max_tokens=max_tokens,
|
| 138 |
)
|
| 139 |
|
|
|
|
| 149 |
if choice.message.tool_calls:
|
| 150 |
for tc in choice.message.tool_calls:
|
| 151 |
import json
|
| 152 |
+
if not hasattr(tc, "function"):
|
| 153 |
+
continue
|
| 154 |
+
tc_any = cast(Any, tc)
|
| 155 |
+
function = tc_any.function
|
| 156 |
tool_calls.append(ToolCall(
|
| 157 |
+
id=tc_any.id,
|
| 158 |
+
name=function.name,
|
| 159 |
+
input=json.loads(function.arguments) if function.arguments else {},
|
| 160 |
))
|
| 161 |
content_blocks.append({
|
| 162 |
"type": "tool_use",
|
| 163 |
+
"id": tc_any.id,
|
| 164 |
+
"name": function.name,
|
| 165 |
+
"input": json.loads(function.arguments) if function.arguments else {},
|
| 166 |
})
|
| 167 |
|
| 168 |
stop_reason = "tool_use" if tool_calls else "end_turn"
|
|
|
|
| 175 |
tool_calls=tool_calls,
|
| 176 |
)
|
| 177 |
|
| 178 |
+
def _convert_tools(self, anthropic_tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 179 |
"""Convert Anthropic-style tools to OpenAI format."""
|
| 180 |
openai_tools = []
|
| 181 |
for tool in anthropic_tools:
|
|
|
|
| 189 |
})
|
| 190 |
return openai_tools
|
| 191 |
|
| 192 |
+
def _convert_message(self, msg: dict[str, Any]) -> dict[str, Any] | None:
|
| 193 |
"""Convert Anthropic-style message to OpenAI format."""
|
| 194 |
role = msg.get("role")
|
| 195 |
content = msg.get("content")
|
|
|
|
| 210 |
|
| 211 |
if tool_results:
|
| 212 |
# Convert tool results to OpenAI format
|
| 213 |
+
result_msg: dict[str, Any] = {"role": "tool", "content": ""}
|
| 214 |
for tr in tool_results:
|
| 215 |
result_msg["tool_call_id"] = tr.get("tool_use_id", "")
|
| 216 |
result_msg["content"] = tr.get("content", "")
|
|
|
|
| 231 |
elif block.get("type") == "tool_use":
|
| 232 |
tool_uses.append(block)
|
| 233 |
|
| 234 |
+
result: dict[str, Any] = {"role": "assistant"}
|
| 235 |
if texts:
|
| 236 |
result["content"] = "\n".join(texts)
|
| 237 |
if tool_uses:
|
agent/message_history.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Message history tracking and persistence."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
-
from datetime import datetime
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
import aiofiles
|
|
|
|
| 1 |
"""Message history tracking and persistence."""
|
| 2 |
|
| 3 |
import json
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
import aiofiles
|
app.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Entelechy Control Console - 观察和控制数字生命的控制台"""
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
-
import os
|
| 5 |
import subprocess
|
| 6 |
import threading
|
| 7 |
from pathlib import Path
|
|
@@ -69,7 +68,7 @@ def get_thinking_stream() -> str:
|
|
| 69 |
inputs = block.get("input", {})
|
| 70 |
texts.append(f"[调用工具: {name}({inputs})]")
|
| 71 |
elif block.get("type") == "tool_result":
|
| 72 |
-
texts.append(
|
| 73 |
content = "\n".join(texts)
|
| 74 |
|
| 75 |
output.append(f"**{role}**: {content}\n")
|
|
@@ -154,20 +153,21 @@ def send_stimulus(stimulus_type: str, content: str) -> str:
|
|
| 154 |
def browser_action(action: str, url: str = "", selector: str = "", text: str = "") -> str:
|
| 155 |
"""浏览器控制"""
|
| 156 |
life = _get_life()
|
| 157 |
-
|
|
|
|
| 158 |
return "浏览器客户端未初始化"
|
| 159 |
|
| 160 |
async def _action():
|
| 161 |
if action == "navigate":
|
| 162 |
-
return await
|
| 163 |
elif action == "click":
|
| 164 |
-
return await
|
| 165 |
elif action == "type":
|
| 166 |
-
return await
|
| 167 |
elif action == "screenshot":
|
| 168 |
-
return await
|
| 169 |
elif action == "extract":
|
| 170 |
-
return await
|
| 171 |
else:
|
| 172 |
return f"未知操作: {action}"
|
| 173 |
|
|
@@ -196,7 +196,7 @@ def _start_background_life():
|
|
| 196 |
|
| 197 |
# ========== 构建控制台界面 ==========
|
| 198 |
|
| 199 |
-
with gr.Blocks(title="Entelechy 控制台"
|
| 200 |
gr.Markdown("# 🧬 Entelechy - 数字生命控制台")
|
| 201 |
gr.Markdown("这是观察和控制数字生命的控制台,不是聊天机器人")
|
| 202 |
|
|
|
|
| 1 |
"""Entelechy Control Console - 观察和控制数字生命的控制台"""
|
| 2 |
|
| 3 |
import asyncio
|
|
|
|
| 4 |
import subprocess
|
| 5 |
import threading
|
| 6 |
from pathlib import Path
|
|
|
|
| 68 |
inputs = block.get("input", {})
|
| 69 |
texts.append(f"[调用工具: {name}({inputs})]")
|
| 70 |
elif block.get("type") == "tool_result":
|
| 71 |
+
texts.append("[工具结果]")
|
| 72 |
content = "\n".join(texts)
|
| 73 |
|
| 74 |
output.append(f"**{role}**: {content}\n")
|
|
|
|
| 153 |
def browser_action(action: str, url: str = "", selector: str = "", text: str = "") -> str:
|
| 154 |
"""浏览器控制"""
|
| 155 |
life = _get_life()
|
| 156 |
+
browser_client = life.browser_client
|
| 157 |
+
if browser_client is None:
|
| 158 |
return "浏览器客户端未初始化"
|
| 159 |
|
| 160 |
async def _action():
|
| 161 |
if action == "navigate":
|
| 162 |
+
return await browser_client.navigate(url)
|
| 163 |
elif action == "click":
|
| 164 |
+
return await browser_client.click(selector)
|
| 165 |
elif action == "type":
|
| 166 |
+
return await browser_client.type_text(selector, text)
|
| 167 |
elif action == "screenshot":
|
| 168 |
+
return await browser_client.screenshot()
|
| 169 |
elif action == "extract":
|
| 170 |
+
return await browser_client.extract_content()
|
| 171 |
else:
|
| 172 |
return f"未知操作: {action}"
|
| 173 |
|
|
|
|
| 196 |
|
| 197 |
# ========== 构建控制台界面 ==========
|
| 198 |
|
| 199 |
+
with gr.Blocks(title="Entelechy 控制台") as demo:
|
| 200 |
gr.Markdown("# 🧬 Entelechy - 数字生命控制台")
|
| 201 |
gr.Markdown("这是观察和控制数字生命的控制台,不是聊天机器人")
|
| 202 |
|
browser/client.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
import base64
|
| 6 |
from pathlib import Path
|
|
|
|
| 7 |
|
| 8 |
from loguru import logger
|
| 9 |
|
|
@@ -22,6 +23,16 @@ class BrowserClient:
|
|
| 22 |
self.page = None
|
| 23 |
self._started = False
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
async def start(self):
|
| 26 |
"""Launch the browser and restore session state."""
|
| 27 |
if self._started:
|
|
@@ -78,9 +89,11 @@ class BrowserClient:
|
|
| 78 |
"""
|
| 79 |
await self.ensure_started()
|
| 80 |
try:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
await
|
|
|
|
|
|
|
| 84 |
return f"Navigated to: {url}\nTitle: {title}"
|
| 85 |
except Exception as e:
|
| 86 |
return f"Navigation error: {e}"
|
|
@@ -89,8 +102,9 @@ class BrowserClient:
|
|
| 89 |
"""Click an element."""
|
| 90 |
await self.ensure_started()
|
| 91 |
try:
|
| 92 |
-
|
| 93 |
-
await
|
|
|
|
| 94 |
return f"Clicked: {selector}"
|
| 95 |
except Exception as e:
|
| 96 |
return f"Click error: {e}"
|
|
@@ -99,7 +113,8 @@ class BrowserClient:
|
|
| 99 |
"""Type text into an element."""
|
| 100 |
await self.ensure_started()
|
| 101 |
try:
|
| 102 |
-
|
|
|
|
| 103 |
return f"Typed text into: {selector}"
|
| 104 |
except Exception as e:
|
| 105 |
return f"Type error: {e}"
|
|
@@ -112,7 +127,8 @@ class BrowserClient:
|
|
| 112 |
"""
|
| 113 |
await self.ensure_started()
|
| 114 |
try:
|
| 115 |
-
|
|
|
|
| 116 |
encoded = base64.b64encode(raw).decode("utf-8")
|
| 117 |
return f"Screenshot taken ({len(raw)} bytes). Base64: {encoded[:100]}..."
|
| 118 |
except Exception as e:
|
|
@@ -122,9 +138,10 @@ class BrowserClient:
|
|
| 122 |
"""Extract text content from the current page."""
|
| 123 |
await self.ensure_started()
|
| 124 |
try:
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
| 128 |
# Truncate very long pages
|
| 129 |
if len(text) > 50000:
|
| 130 |
text = text[:50000] + "\n\n... (truncated)"
|
|
|
|
| 4 |
|
| 5 |
import base64
|
| 6 |
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
|
| 9 |
from loguru import logger
|
| 10 |
|
|
|
|
| 23 |
self.page = None
|
| 24 |
self._started = False
|
| 25 |
|
| 26 |
+
def _require_page(self) -> Any:
|
| 27 |
+
if self.page is None:
|
| 28 |
+
raise RuntimeError("Browser page is not initialized")
|
| 29 |
+
return self.page
|
| 30 |
+
|
| 31 |
+
def _require_context(self) -> Any:
|
| 32 |
+
if self.context is None:
|
| 33 |
+
raise RuntimeError("Browser context is not initialized")
|
| 34 |
+
return self.context
|
| 35 |
+
|
| 36 |
async def start(self):
|
| 37 |
"""Launch the browser and restore session state."""
|
| 38 |
if self._started:
|
|
|
|
| 89 |
"""
|
| 90 |
await self.ensure_started()
|
| 91 |
try:
|
| 92 |
+
page = self._require_page()
|
| 93 |
+
context = self._require_context()
|
| 94 |
+
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
| 95 |
+
title = await page.title()
|
| 96 |
+
await self.session_manager.save_state(context)
|
| 97 |
return f"Navigated to: {url}\nTitle: {title}"
|
| 98 |
except Exception as e:
|
| 99 |
return f"Navigation error: {e}"
|
|
|
|
| 102 |
"""Click an element."""
|
| 103 |
await self.ensure_started()
|
| 104 |
try:
|
| 105 |
+
page = self._require_page()
|
| 106 |
+
await page.click(selector, timeout=10000)
|
| 107 |
+
await page.wait_for_load_state("domcontentloaded")
|
| 108 |
return f"Clicked: {selector}"
|
| 109 |
except Exception as e:
|
| 110 |
return f"Click error: {e}"
|
|
|
|
| 113 |
"""Type text into an element."""
|
| 114 |
await self.ensure_started()
|
| 115 |
try:
|
| 116 |
+
page = self._require_page()
|
| 117 |
+
await page.fill(selector, text, timeout=10000)
|
| 118 |
return f"Typed text into: {selector}"
|
| 119 |
except Exception as e:
|
| 120 |
return f"Type error: {e}"
|
|
|
|
| 127 |
"""
|
| 128 |
await self.ensure_started()
|
| 129 |
try:
|
| 130 |
+
page = self._require_page()
|
| 131 |
+
raw = await page.screenshot(full_page=full_page)
|
| 132 |
encoded = base64.b64encode(raw).decode("utf-8")
|
| 133 |
return f"Screenshot taken ({len(raw)} bytes). Base64: {encoded[:100]}..."
|
| 134 |
except Exception as e:
|
|
|
|
| 138 |
"""Extract text content from the current page."""
|
| 139 |
await self.ensure_started()
|
| 140 |
try:
|
| 141 |
+
page = self._require_page()
|
| 142 |
+
title = await page.title()
|
| 143 |
+
url = page.url
|
| 144 |
+
text = await page.inner_text("body")
|
| 145 |
# Truncate very long pages
|
| 146 |
if len(text) > 50000:
|
| 147 |
text = text[:50000] + "\n\n... (truncated)"
|
main.py
CHANGED
|
@@ -60,6 +60,21 @@ class DigitalLife:
|
|
| 60 |
self.agent: AgentLoop | None = None
|
| 61 |
self.history: MessageHistory | None = None
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
async def _initialize(self):
|
| 64 |
"""Initialize all components."""
|
| 65 |
# Ensure directories exist
|
|
@@ -139,7 +154,8 @@ class DigitalLife:
|
|
| 139 |
|
| 140 |
async def _get_core_context(self) -> str:
|
| 141 |
"""Get CORE.md content for every LLM call."""
|
| 142 |
-
|
|
|
|
| 143 |
if core:
|
| 144 |
return f"\n=== 核心记忆 ===\n{core}\n================\n"
|
| 145 |
return ""
|
|
@@ -148,7 +164,11 @@ class DigitalLife:
|
|
| 148 |
"""Wake up: load CORE.md and restore self-awareness."""
|
| 149 |
logger.info("Waking up...")
|
| 150 |
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
wake_up_parts = ["你醒来了。\n"]
|
| 154 |
|
|
@@ -160,16 +180,16 @@ class DigitalLife:
|
|
| 160 |
wake_up_content = "\n".join(wake_up_parts)
|
| 161 |
|
| 162 |
# If we have saved history, continue from it; otherwise start fresh
|
| 163 |
-
if
|
| 164 |
-
logger.info(f"Resuming from {len(
|
| 165 |
-
|
| 166 |
else:
|
| 167 |
-
|
| 168 |
|
| 169 |
# Run the wake-up conversation
|
| 170 |
-
messages = await
|
| 171 |
-
|
| 172 |
-
await
|
| 173 |
|
| 174 |
logger.info("Wake up complete")
|
| 175 |
|
|
@@ -178,6 +198,9 @@ class DigitalLife:
|
|
| 178 |
await self._initialize()
|
| 179 |
await self._wake_up()
|
| 180 |
|
|
|
|
|
|
|
|
|
|
| 181 |
while self.alive:
|
| 182 |
try:
|
| 183 |
# Check for external stimulus (non-blocking, immediate)
|
|
@@ -187,23 +210,23 @@ class DigitalLife:
|
|
| 187 |
|
| 188 |
if stimulus:
|
| 189 |
# External stimulus received
|
| 190 |
-
|
| 191 |
"role": "user",
|
| 192 |
"content": f"[感知] {stimulus['type']}: {stimulus['content']}",
|
| 193 |
})
|
| 194 |
else:
|
| 195 |
# No stimulus - continue autonomous operation
|
| 196 |
-
|
| 197 |
"role": "user",
|
| 198 |
"content": "继续。",
|
| 199 |
})
|
| 200 |
|
| 201 |
# Run agent loop
|
| 202 |
-
messages = await
|
| 203 |
-
|
| 204 |
|
| 205 |
# Persist history periodically
|
| 206 |
-
await
|
| 207 |
|
| 208 |
# Immediately continue to next iteration (no waiting)
|
| 209 |
|
|
@@ -238,10 +261,13 @@ class DigitalLife:
|
|
| 238 |
if self.agent is None:
|
| 239 |
await self._initialize()
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
# Extract the last assistant text
|
| 247 |
for msg in reversed(messages):
|
|
|
|
| 60 |
self.agent: AgentLoop | None = None
|
| 61 |
self.history: MessageHistory | None = None
|
| 62 |
|
| 63 |
+
def _require_memory_manager(self) -> MemoryManager:
|
| 64 |
+
if self.memory_manager is None:
|
| 65 |
+
raise RuntimeError("Memory manager is not initialized")
|
| 66 |
+
return self.memory_manager
|
| 67 |
+
|
| 68 |
+
def _require_agent(self) -> AgentLoop:
|
| 69 |
+
if self.agent is None:
|
| 70 |
+
raise RuntimeError("Agent loop is not initialized")
|
| 71 |
+
return self.agent
|
| 72 |
+
|
| 73 |
+
def _require_history(self) -> MessageHistory:
|
| 74 |
+
if self.history is None:
|
| 75 |
+
raise RuntimeError("Message history is not initialized")
|
| 76 |
+
return self.history
|
| 77 |
+
|
| 78 |
async def _initialize(self):
|
| 79 |
"""Initialize all components."""
|
| 80 |
# Ensure directories exist
|
|
|
|
| 154 |
|
| 155 |
async def _get_core_context(self) -> str:
|
| 156 |
"""Get CORE.md content for every LLM call."""
|
| 157 |
+
memory_manager = self._require_memory_manager()
|
| 158 |
+
core = await memory_manager.load_core()
|
| 159 |
if core:
|
| 160 |
return f"\n=== 核心记忆 ===\n{core}\n================\n"
|
| 161 |
return ""
|
|
|
|
| 164 |
"""Wake up: load CORE.md and restore self-awareness."""
|
| 165 |
logger.info("Waking up...")
|
| 166 |
|
| 167 |
+
memory_manager = self._require_memory_manager()
|
| 168 |
+
history = self._require_history()
|
| 169 |
+
agent = self._require_agent()
|
| 170 |
+
|
| 171 |
+
core_memories = await memory_manager.load_core()
|
| 172 |
|
| 173 |
wake_up_parts = ["你醒来了。\n"]
|
| 174 |
|
|
|
|
| 180 |
wake_up_content = "\n".join(wake_up_parts)
|
| 181 |
|
| 182 |
# If we have saved history, continue from it; otherwise start fresh
|
| 183 |
+
if history.messages:
|
| 184 |
+
logger.info(f"Resuming from {len(history.messages)} saved messages")
|
| 185 |
+
history.append({"role": "user", "content": wake_up_content})
|
| 186 |
else:
|
| 187 |
+
history.set_messages([{"role": "user", "content": wake_up_content}])
|
| 188 |
|
| 189 |
# Run the wake-up conversation
|
| 190 |
+
messages = await agent.run(history.get_messages())
|
| 191 |
+
history.set_messages(messages)
|
| 192 |
+
await history.save()
|
| 193 |
|
| 194 |
logger.info("Wake up complete")
|
| 195 |
|
|
|
|
| 198 |
await self._initialize()
|
| 199 |
await self._wake_up()
|
| 200 |
|
| 201 |
+
history = self._require_history()
|
| 202 |
+
agent = self._require_agent()
|
| 203 |
+
|
| 204 |
while self.alive:
|
| 205 |
try:
|
| 206 |
# Check for external stimulus (non-blocking, immediate)
|
|
|
|
| 210 |
|
| 211 |
if stimulus:
|
| 212 |
# External stimulus received
|
| 213 |
+
history.append({
|
| 214 |
"role": "user",
|
| 215 |
"content": f"[感知] {stimulus['type']}: {stimulus['content']}",
|
| 216 |
})
|
| 217 |
else:
|
| 218 |
# No stimulus - continue autonomous operation
|
| 219 |
+
history.append({
|
| 220 |
"role": "user",
|
| 221 |
"content": "继续。",
|
| 222 |
})
|
| 223 |
|
| 224 |
# Run agent loop
|
| 225 |
+
messages = await agent.run(history.get_messages())
|
| 226 |
+
history.set_messages(messages)
|
| 227 |
|
| 228 |
# Persist history periodically
|
| 229 |
+
await history.save()
|
| 230 |
|
| 231 |
# Immediately continue to next iteration (no waiting)
|
| 232 |
|
|
|
|
| 261 |
if self.agent is None:
|
| 262 |
await self._initialize()
|
| 263 |
|
| 264 |
+
history = self._require_history()
|
| 265 |
+
agent = self._require_agent()
|
| 266 |
+
|
| 267 |
+
history.append({"role": "user", "content": message})
|
| 268 |
+
messages = await agent.run(history.get_messages())
|
| 269 |
+
history.set_messages(messages)
|
| 270 |
+
await history.save()
|
| 271 |
|
| 272 |
# Extract the last assistant text
|
| 273 |
for msg in reversed(messages):
|
memory/manager.py
CHANGED
|
@@ -13,7 +13,7 @@ class MemoryManager:
|
|
| 13 |
self.storage = MemoryStorage(base_path)
|
| 14 |
self.retrieval = MemoryRetrieval(base_path)
|
| 15 |
|
| 16 |
-
async def remember(self, content: str, category: str = None) -> str:
|
| 17 |
"""Store a new memory.
|
| 18 |
|
| 19 |
Args:
|
|
|
|
| 13 |
self.storage = MemoryStorage(base_path)
|
| 14 |
self.retrieval = MemoryRetrieval(base_path)
|
| 15 |
|
| 16 |
+
async def remember(self, content: str, category: str | None = None) -> str:
|
| 17 |
"""Store a new memory.
|
| 18 |
|
| 19 |
Args:
|
memory/storage.py
CHANGED
|
@@ -31,7 +31,7 @@ class MemoryStorage:
|
|
| 31 |
"\n"
|
| 32 |
)
|
| 33 |
|
| 34 |
-
async def store(self, content: str, category: str = None) -> Path:
|
| 35 |
"""Store a memory as a markdown file with frontmatter.
|
| 36 |
|
| 37 |
Args:
|
|
|
|
| 31 |
"\n"
|
| 32 |
)
|
| 33 |
|
| 34 |
+
async def store(self, content: str, category: str | None = None) -> Path:
|
| 35 |
"""Store a memory as a markdown file with frontmatter.
|
| 36 |
|
| 37 |
Args:
|
tools/bash_tool.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Bash tool - execute shell commands with safety checks."""
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
-
import subprocess
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
from loguru import logger
|
|
@@ -37,6 +36,7 @@ async def run_bash(command: str, workdir: Path | None = None, timeout: int = 120
|
|
| 37 |
|
| 38 |
logger.debug(f"Executing: {command}")
|
| 39 |
|
|
|
|
| 40 |
try:
|
| 41 |
proc = await asyncio.create_subprocess_shell(
|
| 42 |
command,
|
|
@@ -56,10 +56,11 @@ async def run_bash(command: str, workdir: Path | None = None, timeout: int = 120
|
|
| 56 |
|
| 57 |
return output
|
| 58 |
except asyncio.TimeoutError:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
| 63 |
return f"Error: Command timed out after {timeout}s"
|
| 64 |
except Exception as e:
|
| 65 |
return f"Error: {e}"
|
|
|
|
| 1 |
"""Bash tool - execute shell commands with safety checks."""
|
| 2 |
|
| 3 |
import asyncio
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
from loguru import logger
|
|
|
|
| 36 |
|
| 37 |
logger.debug(f"Executing: {command}")
|
| 38 |
|
| 39 |
+
proc: asyncio.subprocess.Process | None = None
|
| 40 |
try:
|
| 41 |
proc = await asyncio.create_subprocess_shell(
|
| 42 |
command,
|
|
|
|
| 56 |
|
| 57 |
return output
|
| 58 |
except asyncio.TimeoutError:
|
| 59 |
+
if proc is not None:
|
| 60 |
+
try:
|
| 61 |
+
proc.kill()
|
| 62 |
+
except Exception:
|
| 63 |
+
pass
|
| 64 |
return f"Error: Command timed out after {timeout}s"
|
| 65 |
except Exception as e:
|
| 66 |
return f"Error: {e}"
|
tools/code_executor.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
"""Code executor - allows the AI to create new plugins at runtime."""
|
| 2 |
|
| 3 |
-
from pathlib import Path
|
| 4 |
|
| 5 |
import yaml
|
| 6 |
from loguru import logger
|
|
|
|
| 1 |
"""Code executor - allows the AI to create new plugins at runtime."""
|
| 2 |
|
|
|
|
| 3 |
|
| 4 |
import yaml
|
| 5 |
from loguru import logger
|
tools/file_tools.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
import aiofiles
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
-
from loguru import logger
|
| 7 |
|
| 8 |
from utils.path_utils import safe_path
|
| 9 |
|
|
|
|
| 3 |
import aiofiles
|
| 4 |
from pathlib import Path
|
| 5 |
|
|
|
|
| 6 |
|
| 7 |
from utils.path_utils import safe_path
|
| 8 |
|
tools/memory_tools.py
CHANGED
|
@@ -19,7 +19,7 @@ def _get_manager() -> MemoryManager:
|
|
| 19 |
return _memory_manager
|
| 20 |
|
| 21 |
|
| 22 |
-
async def run_remember(content: str, category: str = None) -> str:
|
| 23 |
"""Store information to long-term memory.
|
| 24 |
|
| 25 |
Args:
|
|
|
|
| 19 |
return _memory_manager
|
| 20 |
|
| 21 |
|
| 22 |
+
async def run_remember(content: str, category: str | None = None) -> str:
|
| 23 |
"""Store information to long-term memory.
|
| 24 |
|
| 25 |
Args:
|
utils/token_counter.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
"""Token counting utilities using tiktoken."""
|
| 2 |
|
|
|
|
|
|
|
| 3 |
import tiktoken
|
| 4 |
|
| 5 |
|
|
@@ -18,7 +20,7 @@ def estimate_tokens(text: str) -> int:
|
|
| 18 |
return len(_get_encoder().encode(text))
|
| 19 |
|
| 20 |
|
| 21 |
-
def estimate_messages_tokens(messages: list[dict]) -> int:
|
| 22 |
"""Estimate total tokens across a list of messages.
|
| 23 |
|
| 24 |
Each message contributes its content tokens plus a small overhead
|
|
|
|
| 1 |
"""Token counting utilities using tiktoken."""
|
| 2 |
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
import tiktoken
|
| 6 |
|
| 7 |
|
|
|
|
| 20 |
return len(_get_encoder().encode(text))
|
| 21 |
|
| 22 |
|
| 23 |
+
def estimate_messages_tokens(messages: list[dict[str, Any]]) -> int:
|
| 24 |
"""Estimate total tokens across a list of messages.
|
| 25 |
|
| 26 |
Each message contributes its content tokens plus a small overhead
|