loom-agent 0.0.1__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.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/__init__.py +77 -0
- loom/agent.py +217 -0
- loom/agents/__init__.py +10 -0
- loom/agents/refs.py +28 -0
- loom/agents/registry.py +50 -0
- loom/builtin/compression/__init__.py +4 -0
- loom/builtin/compression/structured.py +79 -0
- loom/builtin/embeddings/__init__.py +9 -0
- loom/builtin/embeddings/openai_embedding.py +135 -0
- loom/builtin/embeddings/sentence_transformers_embedding.py +145 -0
- loom/builtin/llms/__init__.py +8 -0
- loom/builtin/llms/mock.py +34 -0
- loom/builtin/llms/openai.py +168 -0
- loom/builtin/llms/rule.py +102 -0
- loom/builtin/memory/__init__.py +5 -0
- loom/builtin/memory/in_memory.py +21 -0
- loom/builtin/memory/persistent_memory.py +278 -0
- loom/builtin/retriever/__init__.py +9 -0
- loom/builtin/retriever/chroma_store.py +265 -0
- loom/builtin/retriever/in_memory.py +106 -0
- loom/builtin/retriever/milvus_store.py +307 -0
- loom/builtin/retriever/pinecone_store.py +237 -0
- loom/builtin/retriever/qdrant_store.py +274 -0
- loom/builtin/retriever/vector_store.py +128 -0
- loom/builtin/retriever/vector_store_config.py +217 -0
- loom/builtin/tools/__init__.py +32 -0
- loom/builtin/tools/calculator.py +49 -0
- loom/builtin/tools/document_search.py +111 -0
- loom/builtin/tools/glob.py +27 -0
- loom/builtin/tools/grep.py +56 -0
- loom/builtin/tools/http_request.py +86 -0
- loom/builtin/tools/python_repl.py +73 -0
- loom/builtin/tools/read_file.py +32 -0
- loom/builtin/tools/task.py +158 -0
- loom/builtin/tools/web_search.py +64 -0
- loom/builtin/tools/write_file.py +31 -0
- loom/callbacks/base.py +9 -0
- loom/callbacks/logging.py +12 -0
- loom/callbacks/metrics.py +27 -0
- loom/callbacks/observability.py +248 -0
- loom/components/agent.py +107 -0
- loom/core/agent_executor.py +450 -0
- loom/core/circuit_breaker.py +178 -0
- loom/core/compression_manager.py +329 -0
- loom/core/context_retriever.py +185 -0
- loom/core/error_classifier.py +193 -0
- loom/core/errors.py +66 -0
- loom/core/message_queue.py +167 -0
- loom/core/permission_store.py +62 -0
- loom/core/permissions.py +69 -0
- loom/core/scheduler.py +125 -0
- loom/core/steering_control.py +47 -0
- loom/core/structured_logger.py +279 -0
- loom/core/subagent_pool.py +232 -0
- loom/core/system_prompt.py +141 -0
- loom/core/system_reminders.py +283 -0
- loom/core/tool_pipeline.py +113 -0
- loom/core/types.py +269 -0
- loom/interfaces/compressor.py +59 -0
- loom/interfaces/embedding.py +51 -0
- loom/interfaces/llm.py +33 -0
- loom/interfaces/memory.py +29 -0
- loom/interfaces/retriever.py +179 -0
- loom/interfaces/tool.py +27 -0
- loom/interfaces/vector_store.py +80 -0
- loom/llm/__init__.py +14 -0
- loom/llm/config.py +228 -0
- loom/llm/factory.py +111 -0
- loom/llm/model_health.py +235 -0
- loom/llm/model_pool_advanced.py +305 -0
- loom/llm/pool.py +170 -0
- loom/llm/registry.py +201 -0
- loom/mcp/__init__.py +4 -0
- loom/mcp/client.py +86 -0
- loom/mcp/registry.py +58 -0
- loom/mcp/tool_adapter.py +48 -0
- loom/observability/__init__.py +5 -0
- loom/patterns/__init__.py +5 -0
- loom/patterns/multi_agent.py +123 -0
- loom/patterns/rag.py +262 -0
- loom/plugins/registry.py +55 -0
- loom/resilience/__init__.py +5 -0
- loom/tooling.py +72 -0
- loom/utils/agent_loader.py +218 -0
- loom/utils/token_counter.py +19 -0
- loom_agent-0.0.1.dist-info/METADATA +457 -0
- loom_agent-0.0.1.dist-info/RECORD +89 -0
- loom_agent-0.0.1.dist-info/WHEEL +4 -0
- loom_agent-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""文档搜索工具 - 主动检索版本"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from loom.interfaces.retriever import BaseRetriever
|
|
13
|
+
except ImportError:
|
|
14
|
+
BaseRetriever = None # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DocumentSearchInput(BaseModel):
|
|
18
|
+
"""文档搜索输入参数"""
|
|
19
|
+
|
|
20
|
+
query: str = Field(description="Search query for documents")
|
|
21
|
+
top_k: int = Field(default=3, description="Number of documents to retrieve")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DocumentSearchTool(BaseTool):
|
|
25
|
+
"""
|
|
26
|
+
文档搜索工具 - 作为普通工具供 Agent 主动调用
|
|
27
|
+
|
|
28
|
+
与 ContextRetriever 的区别:
|
|
29
|
+
- ContextRetriever: 自动检索(每次查询前)- 核心组件
|
|
30
|
+
- DocumentSearchTool: 主动检索(LLM 决定何时)- 工具
|
|
31
|
+
|
|
32
|
+
适用场景:
|
|
33
|
+
- Agent 需要动态决定何时检索文档
|
|
34
|
+
- 可能需要多次检索(不同查询)
|
|
35
|
+
- 与其他工具配合使用
|
|
36
|
+
|
|
37
|
+
示例:
|
|
38
|
+
retriever = VectorStoreRetriever(vector_store)
|
|
39
|
+
search_tool = DocumentSearchTool(retriever)
|
|
40
|
+
|
|
41
|
+
agent = Agent(
|
|
42
|
+
llm=llm,
|
|
43
|
+
tools=[search_tool, Calculator(), ...]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Agent 会自己决定是否需要搜索文档
|
|
47
|
+
result = await agent.run("Calculate 10*20 and search for Python docs")
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name = "search_documents"
|
|
51
|
+
description = (
|
|
52
|
+
"Search for relevant documents from the knowledge base. "
|
|
53
|
+
"Use this when you need specific information that might be in the documents. "
|
|
54
|
+
"Returns document content with relevance scores."
|
|
55
|
+
)
|
|
56
|
+
args_schema = DocumentSearchInput
|
|
57
|
+
is_concurrency_safe = True
|
|
58
|
+
|
|
59
|
+
def __init__(self, retriever: "BaseRetriever"):
|
|
60
|
+
"""
|
|
61
|
+
Parameters:
|
|
62
|
+
retriever: 检索器实例 (例如 VectorStoreRetriever)
|
|
63
|
+
"""
|
|
64
|
+
if BaseRetriever is None:
|
|
65
|
+
raise ImportError("Please install retriever dependencies")
|
|
66
|
+
|
|
67
|
+
self.retriever = retriever
|
|
68
|
+
|
|
69
|
+
async def run(self, query: str, top_k: int = 3, **kwargs: Any) -> str:
|
|
70
|
+
"""
|
|
71
|
+
执行文档搜索
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
query: 搜索查询
|
|
75
|
+
top_k: 返回文档数量
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
格式化的文档搜索结果
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
docs = await self.retriever.retrieve(query, top_k=top_k)
|
|
82
|
+
|
|
83
|
+
if not docs:
|
|
84
|
+
return f"No relevant documents found for query: '{query}'"
|
|
85
|
+
|
|
86
|
+
# 格式化返回结果
|
|
87
|
+
lines = [f"Found {len(docs)} relevant document(s) for: '{query}'\n"]
|
|
88
|
+
|
|
89
|
+
for i, doc in enumerate(docs, 1):
|
|
90
|
+
lines.append(f"**Document {i}**")
|
|
91
|
+
|
|
92
|
+
# 元数据
|
|
93
|
+
if doc.metadata:
|
|
94
|
+
source = doc.metadata.get("source", "Unknown")
|
|
95
|
+
lines.append(f"Source: {source}")
|
|
96
|
+
|
|
97
|
+
# 相关性分数
|
|
98
|
+
if doc.score is not None:
|
|
99
|
+
lines.append(f"Relevance: {doc.score:.2%}")
|
|
100
|
+
|
|
101
|
+
# 内容 (截断长文档)
|
|
102
|
+
content = doc.content
|
|
103
|
+
if len(content) > 500:
|
|
104
|
+
content = content[:500] + "...\n[Content truncated for brevity]"
|
|
105
|
+
|
|
106
|
+
lines.append(f"\n{content}\n")
|
|
107
|
+
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
return f"Error searching documents: {type(e).__name__}: {str(e)}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import glob as _glob
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, List
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GlobArgs(BaseModel):
|
|
13
|
+
pattern: str = Field(description="Glob 匹配模式,例如 **/*.py")
|
|
14
|
+
cwd: str | None = Field(default=None, description="可选工作目录")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GlobTool(BaseTool):
|
|
18
|
+
name = "glob"
|
|
19
|
+
description = "按模式匹配文件路径"
|
|
20
|
+
args_schema = GlobArgs
|
|
21
|
+
|
|
22
|
+
async def run(self, **kwargs) -> Any:
|
|
23
|
+
args = self.args_schema(**kwargs) # type: ignore
|
|
24
|
+
cwd = Path(args.cwd).expanduser() if args.cwd else Path.cwd()
|
|
25
|
+
paths: List[str] = [str(Path(p)) for p in _glob.glob(str(cwd / args.pattern), recursive=True)]
|
|
26
|
+
return "\n".join(paths)
|
|
27
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, List
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GrepArgs(BaseModel):
|
|
13
|
+
pattern: str = Field(description="正则表达式")
|
|
14
|
+
path: str | None = Field(default=None, description="目标文件")
|
|
15
|
+
glob_pattern: str | None = Field(default=None, description="Glob 模式(与 path 二选一)")
|
|
16
|
+
flags: str | None = Field(default="", description="i=IGNORECASE, m=MULTILINE")
|
|
17
|
+
encoding: str = Field(default="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GrepTool(BaseTool):
|
|
21
|
+
name = "grep"
|
|
22
|
+
description = "在文件或文件集内检索正则匹配"
|
|
23
|
+
args_schema = GrepArgs
|
|
24
|
+
|
|
25
|
+
async def run(self, **kwargs) -> Any:
|
|
26
|
+
args = self.args_schema(**kwargs) # type: ignore
|
|
27
|
+
flags = 0
|
|
28
|
+
if args.flags:
|
|
29
|
+
if "i" in args.flags:
|
|
30
|
+
flags |= re.IGNORECASE
|
|
31
|
+
if "m" in args.flags:
|
|
32
|
+
flags |= re.MULTILINE
|
|
33
|
+
regex = re.compile(args.pattern, flags)
|
|
34
|
+
|
|
35
|
+
files: List[Path] = []
|
|
36
|
+
if args.path:
|
|
37
|
+
files = [Path(args.path).expanduser()]
|
|
38
|
+
elif args.glob_pattern:
|
|
39
|
+
from glob import glob
|
|
40
|
+
|
|
41
|
+
files = [Path(p) for p in glob(args.glob_pattern, recursive=True)]
|
|
42
|
+
else:
|
|
43
|
+
return "必须提供 path 或 glob_pattern"
|
|
44
|
+
|
|
45
|
+
matches: List[str] = []
|
|
46
|
+
for f in files:
|
|
47
|
+
if not f.exists() or not f.is_file():
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
for i, line in enumerate(f.read_text(encoding=args.encoding, errors="replace").splitlines(), 1):
|
|
51
|
+
if regex.search(line):
|
|
52
|
+
matches.append(f"{f}:{i}: {line}")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
matches.append(f"{f}: <error {e}>")
|
|
55
|
+
return "\n".join(matches)
|
|
56
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""HTTP 请求工具 - 发送 HTTP 请求"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import httpx
|
|
13
|
+
except ImportError:
|
|
14
|
+
httpx = None # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HTTPRequestInput(BaseModel):
|
|
18
|
+
"""HTTP 请求输入参数"""
|
|
19
|
+
|
|
20
|
+
url: str = Field(description="URL to request")
|
|
21
|
+
method: str = Field(default="GET", description="HTTP method (GET, POST, PUT, DELETE)")
|
|
22
|
+
headers: Optional[dict] = Field(default=None, description="Request headers")
|
|
23
|
+
body: Optional[str] = Field(default=None, description="Request body (for POST/PUT)")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HTTPRequestTool(BaseTool):
|
|
27
|
+
"""
|
|
28
|
+
HTTP 请求工具 - 发送 HTTP 请求并返回响应
|
|
29
|
+
|
|
30
|
+
需要安装: pip install httpx
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name = "http_request"
|
|
34
|
+
description = "Send HTTP requests (GET, POST, PUT, DELETE) to a URL and return the response"
|
|
35
|
+
args_schema = HTTPRequestInput
|
|
36
|
+
is_concurrency_safe = True
|
|
37
|
+
|
|
38
|
+
def __init__(self, timeout: int = 10) -> None:
|
|
39
|
+
if httpx is None:
|
|
40
|
+
raise ImportError("Please install httpx: pip install httpx")
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
|
|
43
|
+
async def run(
|
|
44
|
+
self,
|
|
45
|
+
url: str,
|
|
46
|
+
method: str = "GET",
|
|
47
|
+
headers: Optional[dict] = None,
|
|
48
|
+
body: Optional[str] = None,
|
|
49
|
+
**kwargs: Any,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""执行 HTTP 请求"""
|
|
52
|
+
try:
|
|
53
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
54
|
+
request_kwargs: dict = {"method": method.upper(), "url": url}
|
|
55
|
+
|
|
56
|
+
if headers:
|
|
57
|
+
request_kwargs["headers"] = headers
|
|
58
|
+
|
|
59
|
+
if body and method.upper() in ["POST", "PUT", "PATCH"]:
|
|
60
|
+
request_kwargs["content"] = body
|
|
61
|
+
|
|
62
|
+
response = await client.request(**request_kwargs)
|
|
63
|
+
|
|
64
|
+
# 格式化响应
|
|
65
|
+
result_lines = [
|
|
66
|
+
f"HTTP {response.status_code}",
|
|
67
|
+
f"URL: {url}",
|
|
68
|
+
f"Method: {method.upper()}",
|
|
69
|
+
"",
|
|
70
|
+
"Headers:",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for key, value in response.headers.items():
|
|
74
|
+
result_lines.append(f" {key}: {value}")
|
|
75
|
+
|
|
76
|
+
result_lines.append("")
|
|
77
|
+
result_lines.append("Body:")
|
|
78
|
+
result_lines.append(response.text[:1000]) # 限制输出长度
|
|
79
|
+
|
|
80
|
+
if len(response.text) > 1000:
|
|
81
|
+
result_lines.append(f"\n... (truncated, total {len(response.text)} characters)")
|
|
82
|
+
|
|
83
|
+
return "\n".join(result_lines)
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return f"HTTP request error: {type(e).__name__}: {str(e)}"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Python REPL 工具 - 执行 Python 代码"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from loom.interfaces.tool import BaseTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonREPLInput(BaseModel):
|
|
15
|
+
"""Python REPL 输入参数"""
|
|
16
|
+
|
|
17
|
+
code: str = Field(description="Python code to execute")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PythonREPLTool(BaseTool):
|
|
21
|
+
"""
|
|
22
|
+
Python REPL 工具 - 在隔离环境中执行 Python 代码
|
|
23
|
+
|
|
24
|
+
警告: 不要在生产环境中使用,存在安全风险!
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name = "python_repl"
|
|
28
|
+
description = (
|
|
29
|
+
"Execute Python code and return the output. "
|
|
30
|
+
"Can be used for calculations, data processing, etc. "
|
|
31
|
+
"The code runs in a restricted environment."
|
|
32
|
+
)
|
|
33
|
+
args_schema = PythonREPLInput
|
|
34
|
+
is_concurrency_safe = False # 代码执行不并发安全
|
|
35
|
+
|
|
36
|
+
async def run(self, code: str, **kwargs: Any) -> str:
|
|
37
|
+
"""执行 Python 代码"""
|
|
38
|
+
# 安全性检查 - 禁止危险操作
|
|
39
|
+
dangerous_imports = ["os", "subprocess", "sys", "importlib", "__import__"]
|
|
40
|
+
for dangerous in dangerous_imports:
|
|
41
|
+
if dangerous in code:
|
|
42
|
+
return f"Security error: Import of '{dangerous}' is not allowed"
|
|
43
|
+
|
|
44
|
+
# 捕获标准输出
|
|
45
|
+
old_stdout = sys.stdout
|
|
46
|
+
sys.stdout = captured_output = StringIO()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# 使用受限的全局命名空间
|
|
50
|
+
namespace: dict = {"__builtins__": __builtins__}
|
|
51
|
+
|
|
52
|
+
# 执行代码
|
|
53
|
+
exec(code, namespace)
|
|
54
|
+
|
|
55
|
+
# 获取输出
|
|
56
|
+
output = captured_output.getvalue()
|
|
57
|
+
|
|
58
|
+
if not output:
|
|
59
|
+
# 如果没有打印输出,尝试返回最后一个表达式的值
|
|
60
|
+
try:
|
|
61
|
+
result = eval(code, namespace)
|
|
62
|
+
if result is not None:
|
|
63
|
+
output = str(result)
|
|
64
|
+
except Exception:
|
|
65
|
+
output = "Code executed successfully (no output)"
|
|
66
|
+
|
|
67
|
+
return output.strip()
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return f"Execution error: {type(e).__name__}: {str(e)}"
|
|
71
|
+
|
|
72
|
+
finally:
|
|
73
|
+
sys.stdout = old_stdout
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from loom.interfaces.tool import BaseTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReadArgs(BaseModel):
|
|
12
|
+
path: str = Field(description="文件路径")
|
|
13
|
+
max_bytes: int | None = Field(default=200_000, description="最大读取字节数,默认200KB")
|
|
14
|
+
encoding: str = Field(default="utf-8", description="文本编码")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReadFileTool(BaseTool):
|
|
18
|
+
name = "read_file"
|
|
19
|
+
description = "读取文本文件内容"
|
|
20
|
+
args_schema = ReadArgs
|
|
21
|
+
|
|
22
|
+
async def run(self, **kwargs) -> Any:
|
|
23
|
+
args = self.args_schema(**kwargs) # type: ignore
|
|
24
|
+
p = Path(args.path).expanduser()
|
|
25
|
+
data = p.read_bytes()
|
|
26
|
+
if args.max_bytes is not None and len(data) > args.max_bytes:
|
|
27
|
+
data = data[: args.max_bytes]
|
|
28
|
+
try:
|
|
29
|
+
return data.decode(args.encoding, errors="replace")
|
|
30
|
+
except Exception:
|
|
31
|
+
return data.decode("utf-8", errors="replace")
|
|
32
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Task 工具 - 启动 SubAgent 执行子任务"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Optional, Dict, List
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
from loom.core.permissions import PermissionManager
|
|
11
|
+
from loom.agents.registry import get_agent_by_type as get_registered_agent_by_type
|
|
12
|
+
from loom.utils.agent_loader import (
|
|
13
|
+
get_agent_by_type as get_file_agent_by_type,
|
|
14
|
+
) # fallback to file-based packs
|
|
15
|
+
from loom.agents.refs import AgentRef, ModelRef
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from loom.components.agent import Agent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TaskInput(BaseModel):
|
|
22
|
+
"""Task 工具输入参数"""
|
|
23
|
+
|
|
24
|
+
description: str = Field(description="Short description of the task (3-5 words)")
|
|
25
|
+
prompt: str = Field(description="Detailed task instructions for the sub-agent")
|
|
26
|
+
subagent_type: str | None = Field(
|
|
27
|
+
default=None, description="Optional: agent type from .loom/.claude agent packs"
|
|
28
|
+
)
|
|
29
|
+
model_name: str | None = Field(
|
|
30
|
+
default=None, description="Optional: override model name for this sub-agent"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TaskTool(BaseTool):
|
|
35
|
+
"""
|
|
36
|
+
Task 工具 - 启动 SubAgent 执行专项任务
|
|
37
|
+
|
|
38
|
+
对应 Claude Code 的 Task 工具和 SubAgent 机制
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name = "task"
|
|
42
|
+
description = (
|
|
43
|
+
"Launch a sub-agent to handle a specific subtask independently. "
|
|
44
|
+
"Useful for parallel execution or specialized processing. "
|
|
45
|
+
"The sub-agent has its own execution environment and tool access."
|
|
46
|
+
)
|
|
47
|
+
args_schema = TaskInput
|
|
48
|
+
is_concurrency_safe = True
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
agent_factory: Optional[callable] = None,
|
|
53
|
+
max_iterations: int = 20,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Parameters:
|
|
57
|
+
- agent_factory: 创建 SubAgent 的工厂函数
|
|
58
|
+
- max_iterations: SubAgent 最大迭代次数
|
|
59
|
+
"""
|
|
60
|
+
self.agent_factory = agent_factory
|
|
61
|
+
self.max_iterations = max_iterations
|
|
62
|
+
|
|
63
|
+
async def run(
|
|
64
|
+
self,
|
|
65
|
+
description: str,
|
|
66
|
+
prompt: str,
|
|
67
|
+
subagent_type: str | None = None,
|
|
68
|
+
model_name: str | None = None,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""执行子任务"""
|
|
72
|
+
if not self.agent_factory:
|
|
73
|
+
return "Error: Task tool not configured with agent_factory"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# 解析 Agent Packs(若提供 subagent_type)
|
|
77
|
+
agent_system_instructions: Optional[str] = None
|
|
78
|
+
allowed_tools: Optional[List[str]] = None
|
|
79
|
+
effective_model: Optional[str] = model_name
|
|
80
|
+
|
|
81
|
+
if subagent_type:
|
|
82
|
+
# 1) Prefer programmatic registry
|
|
83
|
+
cfg = get_registered_agent_by_type(subagent_type)
|
|
84
|
+
if cfg:
|
|
85
|
+
agent_system_instructions = cfg.system_instructions or None
|
|
86
|
+
if isinstance(cfg.tools, list):
|
|
87
|
+
allowed_tools = list(cfg.tools)
|
|
88
|
+
elif cfg.tools == "*":
|
|
89
|
+
allowed_tools = None
|
|
90
|
+
if effective_model is None and cfg.model_name:
|
|
91
|
+
effective_model = cfg.model_name
|
|
92
|
+
else:
|
|
93
|
+
# 2) Fallback to file-based packs (optional)
|
|
94
|
+
fcfg = get_file_agent_by_type(subagent_type)
|
|
95
|
+
if fcfg:
|
|
96
|
+
agent_system_instructions = fcfg.system_prompt or None
|
|
97
|
+
if isinstance(fcfg.tools, list):
|
|
98
|
+
allowed_tools = fcfg.tools
|
|
99
|
+
elif fcfg.tools == "*":
|
|
100
|
+
allowed_tools = None
|
|
101
|
+
if effective_model is None and fcfg.model_name:
|
|
102
|
+
effective_model = fcfg.model_name
|
|
103
|
+
|
|
104
|
+
# 构造权限策略(若需要限制工具)
|
|
105
|
+
permission_policy: Optional[Dict[str, str]] = None
|
|
106
|
+
if allowed_tools is not None:
|
|
107
|
+
policy: Dict[str, str] = {"default": "deny"}
|
|
108
|
+
for t in allowed_tools:
|
|
109
|
+
policy[t] = "allow"
|
|
110
|
+
permission_policy = policy
|
|
111
|
+
|
|
112
|
+
# 创建 SubAgent 实例(尽量传递可选参数;不支持则回退)
|
|
113
|
+
try:
|
|
114
|
+
sub_agent: Agent = self.agent_factory( # type: ignore[call-arg]
|
|
115
|
+
max_iterations=self.max_iterations,
|
|
116
|
+
system_instructions=agent_system_instructions,
|
|
117
|
+
permission_policy=permission_policy,
|
|
118
|
+
model_name=effective_model,
|
|
119
|
+
)
|
|
120
|
+
except TypeError:
|
|
121
|
+
# 回退到最小签名
|
|
122
|
+
sub_agent = self.agent_factory(max_iterations=self.max_iterations) # type: ignore[call-arg]
|
|
123
|
+
|
|
124
|
+
# 如果无法通过构造器注入权限,则运行时替换权限管理器
|
|
125
|
+
if permission_policy is not None:
|
|
126
|
+
sub_agent.executor.permission_manager = PermissionManager(
|
|
127
|
+
policy=permission_policy
|
|
128
|
+
)
|
|
129
|
+
sub_agent.executor.tool_pipeline.permission_manager = (
|
|
130
|
+
sub_agent.executor.permission_manager
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 运行子任务(系统提示已注入到 sub_agent,输入仍为原始 prompt)
|
|
134
|
+
result = await sub_agent.run(prompt)
|
|
135
|
+
|
|
136
|
+
# 格式化返回结果
|
|
137
|
+
return f"**SubAgent Task: {description}**\n\nResult:\n{result}"
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return f"SubAgent execution error: {type(e).__name__}: {str(e)}"
|
|
141
|
+
|
|
142
|
+
# Framework-friendly API: set by reference instead of strings
|
|
143
|
+
async def run_ref(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
description: str,
|
|
147
|
+
prompt: str,
|
|
148
|
+
agent: AgentRef | None = None,
|
|
149
|
+
model: ModelRef | None = None,
|
|
150
|
+
) -> str:
|
|
151
|
+
subagent_type = agent.agent_type if isinstance(agent, AgentRef) else None
|
|
152
|
+
model_name = model.name if isinstance(model, ModelRef) else None
|
|
153
|
+
return await self.run(
|
|
154
|
+
description=description,
|
|
155
|
+
prompt=prompt,
|
|
156
|
+
subagent_type=subagent_type,
|
|
157
|
+
model_name=model_name,
|
|
158
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Web 搜索工具 - 使用 DuckDuckGo (无需 API key)"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from loom.interfaces.tool import BaseTool
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from duckduckgo_search import DDGS
|
|
13
|
+
except ImportError:
|
|
14
|
+
DDGS = None # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WebSearchInput(BaseModel):
|
|
18
|
+
"""Web 搜索输入参数"""
|
|
19
|
+
|
|
20
|
+
query: str = Field(description="Search query")
|
|
21
|
+
max_results: int = Field(default=5, description="Maximum number of results to return")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WebSearchTool(BaseTool):
|
|
25
|
+
"""
|
|
26
|
+
Web 搜索工具 - 使用 DuckDuckGo
|
|
27
|
+
|
|
28
|
+
需要安装: pip install duckduckgo-search
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name = "web_search"
|
|
32
|
+
description = "Search the web using DuckDuckGo. Returns titles, snippets, and URLs."
|
|
33
|
+
args_schema = WebSearchInput
|
|
34
|
+
is_concurrency_safe = True
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
if DDGS is None:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"Please install duckduckgo-search: pip install duckduckgo-search"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def run(self, query: str, max_results: int = 5, **kwargs: Any) -> str:
|
|
43
|
+
"""执行搜索"""
|
|
44
|
+
try:
|
|
45
|
+
with DDGS() as ddgs:
|
|
46
|
+
results = list(ddgs.text(query, max_results=max_results))
|
|
47
|
+
|
|
48
|
+
if not results:
|
|
49
|
+
return f"No results found for query: {query}"
|
|
50
|
+
|
|
51
|
+
# 格式化输出
|
|
52
|
+
output_lines = [f"Search results for '{query}':\n"]
|
|
53
|
+
for i, result in enumerate(results, 1):
|
|
54
|
+
title = result.get("title", "No title")
|
|
55
|
+
snippet = result.get("body", "")
|
|
56
|
+
url = result.get("href", "")
|
|
57
|
+
output_lines.append(f"{i}. **{title}**")
|
|
58
|
+
output_lines.append(f" {snippet}")
|
|
59
|
+
output_lines.append(f" URL: {url}\n")
|
|
60
|
+
|
|
61
|
+
return "\n".join(output_lines)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return f"Search error: {str(e)}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from loom.interfaces.tool import BaseTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WriteArgs(BaseModel):
|
|
12
|
+
path: str = Field(description="文件路径")
|
|
13
|
+
content: str = Field(description="写入内容")
|
|
14
|
+
encoding: str = Field(default="utf-8", description="文本编码")
|
|
15
|
+
overwrite: bool = Field(default=True, description="是否覆盖")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WriteFileTool(BaseTool):
|
|
19
|
+
name = "write_file"
|
|
20
|
+
description = "写入文本到文件(可能覆盖)"
|
|
21
|
+
args_schema = WriteArgs
|
|
22
|
+
|
|
23
|
+
async def run(self, **kwargs) -> Any:
|
|
24
|
+
args = self.args_schema(**kwargs) # type: ignore
|
|
25
|
+
p = Path(args.path).expanduser()
|
|
26
|
+
if p.exists() and not args.overwrite:
|
|
27
|
+
return f"File exists and overwrite=False: {p}"
|
|
28
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
p.write_text(args.content, encoding=args.encoding)
|
|
30
|
+
return f"Wrote {len(args.content)} chars to {str(p)}"
|
|
31
|
+
|
loom/callbacks/base.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from .base import BaseCallback
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoggingCallback(BaseCallback):
|
|
9
|
+
async def on_event(self, event_type: str, payload: Dict[str, Any]) -> None:
|
|
10
|
+
# 最小实现:打印事件
|
|
11
|
+
print(f"[loom] {event_type}: {payload}")
|
|
12
|
+
|