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.

Files changed (89) hide show
  1. loom/__init__.py +77 -0
  2. loom/agent.py +217 -0
  3. loom/agents/__init__.py +10 -0
  4. loom/agents/refs.py +28 -0
  5. loom/agents/registry.py +50 -0
  6. loom/builtin/compression/__init__.py +4 -0
  7. loom/builtin/compression/structured.py +79 -0
  8. loom/builtin/embeddings/__init__.py +9 -0
  9. loom/builtin/embeddings/openai_embedding.py +135 -0
  10. loom/builtin/embeddings/sentence_transformers_embedding.py +145 -0
  11. loom/builtin/llms/__init__.py +8 -0
  12. loom/builtin/llms/mock.py +34 -0
  13. loom/builtin/llms/openai.py +168 -0
  14. loom/builtin/llms/rule.py +102 -0
  15. loom/builtin/memory/__init__.py +5 -0
  16. loom/builtin/memory/in_memory.py +21 -0
  17. loom/builtin/memory/persistent_memory.py +278 -0
  18. loom/builtin/retriever/__init__.py +9 -0
  19. loom/builtin/retriever/chroma_store.py +265 -0
  20. loom/builtin/retriever/in_memory.py +106 -0
  21. loom/builtin/retriever/milvus_store.py +307 -0
  22. loom/builtin/retriever/pinecone_store.py +237 -0
  23. loom/builtin/retriever/qdrant_store.py +274 -0
  24. loom/builtin/retriever/vector_store.py +128 -0
  25. loom/builtin/retriever/vector_store_config.py +217 -0
  26. loom/builtin/tools/__init__.py +32 -0
  27. loom/builtin/tools/calculator.py +49 -0
  28. loom/builtin/tools/document_search.py +111 -0
  29. loom/builtin/tools/glob.py +27 -0
  30. loom/builtin/tools/grep.py +56 -0
  31. loom/builtin/tools/http_request.py +86 -0
  32. loom/builtin/tools/python_repl.py +73 -0
  33. loom/builtin/tools/read_file.py +32 -0
  34. loom/builtin/tools/task.py +158 -0
  35. loom/builtin/tools/web_search.py +64 -0
  36. loom/builtin/tools/write_file.py +31 -0
  37. loom/callbacks/base.py +9 -0
  38. loom/callbacks/logging.py +12 -0
  39. loom/callbacks/metrics.py +27 -0
  40. loom/callbacks/observability.py +248 -0
  41. loom/components/agent.py +107 -0
  42. loom/core/agent_executor.py +450 -0
  43. loom/core/circuit_breaker.py +178 -0
  44. loom/core/compression_manager.py +329 -0
  45. loom/core/context_retriever.py +185 -0
  46. loom/core/error_classifier.py +193 -0
  47. loom/core/errors.py +66 -0
  48. loom/core/message_queue.py +167 -0
  49. loom/core/permission_store.py +62 -0
  50. loom/core/permissions.py +69 -0
  51. loom/core/scheduler.py +125 -0
  52. loom/core/steering_control.py +47 -0
  53. loom/core/structured_logger.py +279 -0
  54. loom/core/subagent_pool.py +232 -0
  55. loom/core/system_prompt.py +141 -0
  56. loom/core/system_reminders.py +283 -0
  57. loom/core/tool_pipeline.py +113 -0
  58. loom/core/types.py +269 -0
  59. loom/interfaces/compressor.py +59 -0
  60. loom/interfaces/embedding.py +51 -0
  61. loom/interfaces/llm.py +33 -0
  62. loom/interfaces/memory.py +29 -0
  63. loom/interfaces/retriever.py +179 -0
  64. loom/interfaces/tool.py +27 -0
  65. loom/interfaces/vector_store.py +80 -0
  66. loom/llm/__init__.py +14 -0
  67. loom/llm/config.py +228 -0
  68. loom/llm/factory.py +111 -0
  69. loom/llm/model_health.py +235 -0
  70. loom/llm/model_pool_advanced.py +305 -0
  71. loom/llm/pool.py +170 -0
  72. loom/llm/registry.py +201 -0
  73. loom/mcp/__init__.py +4 -0
  74. loom/mcp/client.py +86 -0
  75. loom/mcp/registry.py +58 -0
  76. loom/mcp/tool_adapter.py +48 -0
  77. loom/observability/__init__.py +5 -0
  78. loom/patterns/__init__.py +5 -0
  79. loom/patterns/multi_agent.py +123 -0
  80. loom/patterns/rag.py +262 -0
  81. loom/plugins/registry.py +55 -0
  82. loom/resilience/__init__.py +5 -0
  83. loom/tooling.py +72 -0
  84. loom/utils/agent_loader.py +218 -0
  85. loom/utils/token_counter.py +19 -0
  86. loom_agent-0.0.1.dist-info/METADATA +457 -0
  87. loom_agent-0.0.1.dist-info/RECORD +89 -0
  88. loom_agent-0.0.1.dist-info/WHEEL +4 -0
  89. 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,9 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ class BaseCallback:
7
+ async def on_event(self, event_type: str, payload: Dict[str, Any]) -> None:
8
+ return None
9
+
@@ -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
+