agentcomet 0.1.0b1__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.
- agentcomet/__init__.py +10 -0
- agentcomet/agents/__init__.py +10 -0
- agentcomet/agents/agent.py +638 -0
- agentcomet/agents/base_agent.py +19 -0
- agentcomet/agents/factory.py +21 -0
- agentcomet/agents/loader.py +96 -0
- agentcomet/agents/registry.py +34 -0
- agentcomet/cli.py +95 -0
- agentcomet/communication/__init__.py +5 -0
- agentcomet/communication/message_broker.py +45 -0
- agentcomet/communication/protocols.py +17 -0
- agentcomet/communication/state_manager.py +20 -0
- agentcomet/memory.py +66 -0
- agentcomet/models/__init__.py +4 -0
- agentcomet/models/base_model.py +17 -0
- agentcomet/models/providers.py +271 -0
- agentcomet/orchestrators/__init__.py +4 -0
- agentcomet/orchestrators/agent_orchestrator.py +70 -0
- agentcomet/orchestrators/execution_engine.py +83 -0
- agentcomet/settings.py +22 -0
- agentcomet/tools/__init__.py +10 -0
- agentcomet/tools/core.py +81 -0
- agentcomet/tools/registry.py +41 -0
- agentcomet/utils/__init__.py +4 -0
- agentcomet/utils/serialization.py +24 -0
- agentcomet/utils/validation.py +15 -0
- agentcomet/vcs/__init__.py +3 -0
- agentcomet/vcs/repository.py +156 -0
- agentcomet/workflows/__init__.py +7 -0
- agentcomet/workflows/conditional_router.py +29 -0
- agentcomet/workflows/dag_builder.py +47 -0
- agentcomet/workflows/patterns.py +11 -0
- agentcomet/workflows/templates.py +74 -0
- agentcomet/workflows/workflow_builder.py +58 -0
- agentcomet-0.1.0b1.dist-info/METADATA +238 -0
- agentcomet-0.1.0b1.dist-info/RECORD +40 -0
- agentcomet-0.1.0b1.dist-info/WHEEL +5 -0
- agentcomet-0.1.0b1.dist-info/licenses/LICENSE +201 -0
- agentcomet-0.1.0b1.dist-info/licenses/NOTICE +6 -0
- agentcomet-0.1.0b1.dist-info/top_level.txt +1 -0
agentcomet/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
__version__ = "0.1.0b1"
|
|
2
|
+
|
|
3
|
+
from .agents.agent import Agent
|
|
4
|
+
from .agents.factory import create_agent
|
|
5
|
+
from .agents.loader import load_agent
|
|
6
|
+
from .tools.core import tool
|
|
7
|
+
from .memory import Memory
|
|
8
|
+
from .settings import Settings
|
|
9
|
+
|
|
10
|
+
__all__ = ["Agent", "create_agent", "load_agent", "tool", "Memory", "Settings", "__version__"]
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
from agentcomet.agents.base_agent import BaseAgent
|
|
8
|
+
from agentcomet.models.base_model import BaseLLM
|
|
9
|
+
from agentcomet.memory import Memory
|
|
10
|
+
from agentcomet.tools import ToolSpec, ToolRegistry, default_registry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Agent(BaseAgent):
|
|
14
|
+
"""
|
|
15
|
+
Agent class for AgentComet SDK.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Declarative setup via setup() override
|
|
19
|
+
- Tool calling with @tool decorator
|
|
20
|
+
- Key-value memory (self.memory.save / self.memory.get)
|
|
21
|
+
- State persistence (save_state / load_state / show_states)
|
|
22
|
+
- UAF export/load (agent.export / load_agent)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
STATE_DIR = ".agentcomet"
|
|
26
|
+
|
|
27
|
+
def __init__(self, name: Optional[str] = None, description: Optional[str] = None,
|
|
28
|
+
author: Optional[str] = None, llm: Optional[Union[str, BaseLLM]] = None):
|
|
29
|
+
self.description = description or "This is a tool calling agent that solves tasks using its assigned tools."
|
|
30
|
+
self.author = author or "AgentComet"
|
|
31
|
+
self._llm_provider = None
|
|
32
|
+
self._llm_instance = None
|
|
33
|
+
self.memory = Memory()
|
|
34
|
+
self.registry = ToolRegistry()
|
|
35
|
+
self._states_index = {} # name -> hash mapping
|
|
36
|
+
|
|
37
|
+
# Pull in default builtins
|
|
38
|
+
for tool_name, tool_spec in default_registry.builtin_tools.items():
|
|
39
|
+
self.registry.register_builtin(tool_spec)
|
|
40
|
+
|
|
41
|
+
self.setup()
|
|
42
|
+
|
|
43
|
+
if name is not None:
|
|
44
|
+
self.name = name
|
|
45
|
+
if description is not None:
|
|
46
|
+
self.description = description
|
|
47
|
+
if author is not None:
|
|
48
|
+
self.author = author
|
|
49
|
+
if llm is not None:
|
|
50
|
+
self.use_llm(llm)
|
|
51
|
+
|
|
52
|
+
if not hasattr(self, 'name') or not self.name:
|
|
53
|
+
self.name = "default-agent"
|
|
54
|
+
|
|
55
|
+
super().__init__(name=self.name)
|
|
56
|
+
|
|
57
|
+
def setup(self):
|
|
58
|
+
"""Intended to be overridden by subclasses to configure the agent."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# ── LLM ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def use_llm(self, model: Union[str, BaseLLM]):
|
|
64
|
+
"""Configure the LLM provider. Accepts a BaseLLM instance or a string like 'ollama:gemma3:4b'."""
|
|
65
|
+
if isinstance(model, BaseLLM):
|
|
66
|
+
self._llm_instance = model
|
|
67
|
+
cls_name = type(model).__name__.lower()
|
|
68
|
+
model_name = getattr(model, 'model', 'unknown')
|
|
69
|
+
self._llm_provider = f"{cls_name}:{model_name}"
|
|
70
|
+
elif isinstance(model, str):
|
|
71
|
+
self._llm_provider = model
|
|
72
|
+
provider, _, model_name = model.partition(":")
|
|
73
|
+
if provider == "ollama":
|
|
74
|
+
from agentcomet.models.providers import Ollama
|
|
75
|
+
self._llm_instance = Ollama(model=model_name or "llama3")
|
|
76
|
+
|
|
77
|
+
def use_memory(self, enabled: bool = True):
|
|
78
|
+
"""Legacy compatibility — memory is always available via self.memory."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# ── Tools ───────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def add_tools(self, *tools: ToolSpec):
|
|
84
|
+
"""Add tools to the agent's registry."""
|
|
85
|
+
for tool in tools:
|
|
86
|
+
self.registry.register_agent_tool(tool)
|
|
87
|
+
|
|
88
|
+
# ── Invoke / Chat / Run ─────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
def invoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
91
|
+
"""Base implementation from BaseAgent. Wraps chat."""
|
|
92
|
+
user_input = state.get("input", "")
|
|
93
|
+
if not user_input and "messages" in state:
|
|
94
|
+
messages = state["messages"]
|
|
95
|
+
if messages:
|
|
96
|
+
user_input = messages[-1].content
|
|
97
|
+
|
|
98
|
+
response = self.chat(user_input)
|
|
99
|
+
return {"output": response}
|
|
100
|
+
|
|
101
|
+
def _build_tool_descriptions(self) -> str:
|
|
102
|
+
"""Build detailed tool descriptions for the LLM prompt."""
|
|
103
|
+
tools = self.registry.get_all_tools()
|
|
104
|
+
if not tools:
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
desc = "You have access to the following tools. To use a tool, respond with EXACTLY this format on its own line:\n"
|
|
108
|
+
desc += "TOOL_CALL: tool_name(arg1=value1, arg2=value2)\n\n"
|
|
109
|
+
desc += "Available tools:\n"
|
|
110
|
+
for t in tools:
|
|
111
|
+
params = ""
|
|
112
|
+
if t.schema and "properties" in t.schema:
|
|
113
|
+
props = t.schema["properties"]
|
|
114
|
+
param_parts = []
|
|
115
|
+
for pname, pinfo in props.items():
|
|
116
|
+
ptype = pinfo.get("type", "any")
|
|
117
|
+
pdesc = pinfo.get("description", "")
|
|
118
|
+
param_parts.append(f"{pname}: {ptype}" + (f" - {pdesc}" if pdesc else ""))
|
|
119
|
+
params = ", ".join(param_parts)
|
|
120
|
+
desc += f" - {t.name}({params}): {t.description}\n"
|
|
121
|
+
|
|
122
|
+
desc += "\nCRITICAL INSTRUCTIONS:\n"
|
|
123
|
+
desc += "1. ONLY use tools if the user EXPLICITLY asks you to perform an action that requires them.\n"
|
|
124
|
+
desc += "2. DO NOT use tools (like write/read files) just to remember user information. I automatically save our conversation history.\n"
|
|
125
|
+
desc += "3. If you do not need a tool, just respond directly.\n"
|
|
126
|
+
desc += "4. After receiving a tool result, provide your final answer to the user.\n"
|
|
127
|
+
return desc
|
|
128
|
+
|
|
129
|
+
def _parse_tool_call(self, response: str):
|
|
130
|
+
"""Parse a TOOL_CALL from the LLM response. Returns (tool_name, kwargs) or None."""
|
|
131
|
+
import re
|
|
132
|
+
# Match: TOOL_CALL: func_name(arg=val, ...)
|
|
133
|
+
pattern = r'TOOL_CALL:\s*(\w+)\(([^)]*)\)'
|
|
134
|
+
match = re.search(pattern, response)
|
|
135
|
+
if not match:
|
|
136
|
+
# Also try: ```tool_code\nfunc(args)\n```
|
|
137
|
+
pattern2 = r'```tool_code\s*\n\s*(\w+)\(([^)]*)\)\s*\n```'
|
|
138
|
+
match = re.search(pattern2, response)
|
|
139
|
+
if not match:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
func_name = match.group(1)
|
|
143
|
+
args_str = match.group(2).strip()
|
|
144
|
+
|
|
145
|
+
kwargs = {}
|
|
146
|
+
if args_str:
|
|
147
|
+
for part in re.split(r',\s*(?=\w+=)', args_str):
|
|
148
|
+
part = part.strip()
|
|
149
|
+
if '=' in part:
|
|
150
|
+
key, val = part.split('=', 1)
|
|
151
|
+
key = key.strip()
|
|
152
|
+
val = val.strip().strip("'\"")
|
|
153
|
+
# Try to convert to int/float
|
|
154
|
+
try:
|
|
155
|
+
val = int(val)
|
|
156
|
+
except ValueError:
|
|
157
|
+
try:
|
|
158
|
+
val = float(val)
|
|
159
|
+
except ValueError:
|
|
160
|
+
pass
|
|
161
|
+
kwargs[key] = val
|
|
162
|
+
|
|
163
|
+
return func_name, kwargs
|
|
164
|
+
|
|
165
|
+
def _execute_tool(self, tool_name: str, kwargs: dict) -> str:
|
|
166
|
+
"""Execute a tool by name with given kwargs."""
|
|
167
|
+
tools = {t.name: t for t in self.registry.get_all_tools()}
|
|
168
|
+
if tool_name not in tools:
|
|
169
|
+
return f"Error: Tool '{tool_name}' not found."
|
|
170
|
+
|
|
171
|
+
tool_spec = tools[tool_name]
|
|
172
|
+
try:
|
|
173
|
+
result = tool_spec.fn(**kwargs)
|
|
174
|
+
return str(result)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return f"Error executing {tool_name}: {e}"
|
|
177
|
+
|
|
178
|
+
def chat(self, input: str) -> str:
|
|
179
|
+
"""Core method to invoke the model with memory context and tool calling."""
|
|
180
|
+
# Build memory context for the prompt
|
|
181
|
+
memory_context = ""
|
|
182
|
+
mem = self.memory.to_dict()
|
|
183
|
+
|
|
184
|
+
# Include stored facts (non-messages keys)
|
|
185
|
+
facts = {k: v for k, v in mem.items() if k != "messages"}
|
|
186
|
+
if facts:
|
|
187
|
+
memory_context += "Known Information:\n"
|
|
188
|
+
for k, v in facts.items():
|
|
189
|
+
memory_context += f" {k}: {v}\n"
|
|
190
|
+
|
|
191
|
+
# Include conversation history
|
|
192
|
+
messages = mem.get("messages", [])
|
|
193
|
+
if messages:
|
|
194
|
+
memory_context += "\nConversation History:\n"
|
|
195
|
+
for msg in messages[-10:]:
|
|
196
|
+
role = msg.get("role", "unknown")
|
|
197
|
+
text = msg.get("text", "")
|
|
198
|
+
memory_context += f" [{role}] {text}\n"
|
|
199
|
+
|
|
200
|
+
if self._llm_instance:
|
|
201
|
+
try:
|
|
202
|
+
# Build prompt with tool descriptions
|
|
203
|
+
prompt_parts = []
|
|
204
|
+
if memory_context:
|
|
205
|
+
prompt_parts.append(memory_context)
|
|
206
|
+
|
|
207
|
+
tool_desc = self._build_tool_descriptions()
|
|
208
|
+
if tool_desc:
|
|
209
|
+
prompt_parts.append(tool_desc)
|
|
210
|
+
|
|
211
|
+
prompt_parts.append(f"User: {input}")
|
|
212
|
+
prompt = "\n".join(prompt_parts)
|
|
213
|
+
|
|
214
|
+
# Tool-calling loop (up to 3 rounds)
|
|
215
|
+
final_response = ""
|
|
216
|
+
for _ in range(3):
|
|
217
|
+
response = self._llm_instance.generate(prompt)
|
|
218
|
+
|
|
219
|
+
tool_call = self._parse_tool_call(response)
|
|
220
|
+
if tool_call:
|
|
221
|
+
tool_name, kwargs = tool_call
|
|
222
|
+
result = self._execute_tool(tool_name, kwargs)
|
|
223
|
+
# Feed result back to LLM
|
|
224
|
+
prompt += f"\n\nAssistant: {response}\n\nTool Result for {tool_name}: {result}\n\nNow provide your final answer to the user based on the tool result."
|
|
225
|
+
else:
|
|
226
|
+
final_response = response
|
|
227
|
+
break
|
|
228
|
+
else:
|
|
229
|
+
final_response = response
|
|
230
|
+
|
|
231
|
+
# Auto-append to conversation history
|
|
232
|
+
if "messages" not in mem:
|
|
233
|
+
mem["messages"] = []
|
|
234
|
+
mem["messages"].append({"role": "user", "text": input})
|
|
235
|
+
mem["messages"].append({"role": "agent", "text": final_response})
|
|
236
|
+
self.memory.save("messages", mem["messages"])
|
|
237
|
+
|
|
238
|
+
return final_response
|
|
239
|
+
except Exception as e:
|
|
240
|
+
return f"[Agent: {self.name}] Error invoking LLM {self._llm_provider}: {e}"
|
|
241
|
+
|
|
242
|
+
return f"[Agent: {self.name}] No LLM configured. Input: {input}"
|
|
243
|
+
|
|
244
|
+
def run(self, input: str) -> str:
|
|
245
|
+
"""Run the agent."""
|
|
246
|
+
return self.chat(input)
|
|
247
|
+
|
|
248
|
+
# ── State Persistence ───────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
def _get_state_dir(self) -> str:
|
|
251
|
+
"""Get the state directory for this agent."""
|
|
252
|
+
state_dir = os.path.join(self.STATE_DIR, "states", self.name)
|
|
253
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
254
|
+
return state_dir
|
|
255
|
+
|
|
256
|
+
def _get_index_path(self) -> str:
|
|
257
|
+
return os.path.join(self._get_state_dir(), "index.json")
|
|
258
|
+
|
|
259
|
+
def _read_index(self) -> dict:
|
|
260
|
+
path = self._get_index_path()
|
|
261
|
+
if os.path.exists(path):
|
|
262
|
+
with open(path, 'r') as f:
|
|
263
|
+
return json.load(f)
|
|
264
|
+
return {"states": [], "latest": None, "names": {}}
|
|
265
|
+
|
|
266
|
+
def _write_index(self, index: dict):
|
|
267
|
+
with open(self._get_index_path(), 'w') as f:
|
|
268
|
+
json.dump(index, f, indent=2)
|
|
269
|
+
|
|
270
|
+
def _generate_hash(self, data: dict) -> str:
|
|
271
|
+
content = json.dumps(data, sort_keys=True, default=str)
|
|
272
|
+
return hashlib.sha256(content.encode()).hexdigest()[:8]
|
|
273
|
+
|
|
274
|
+
def save_state(self, name: Optional[str] = None) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Save current memory state. Returns hash or name.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
name: Optional friendly name (e.g. "checkpoint1").
|
|
280
|
+
If not provided, an auto-generated hash is returned.
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
hash = agent.save_state() # -> "a1b2c3d4"
|
|
284
|
+
agent.save_state("before-training") # -> "before-training"
|
|
285
|
+
"""
|
|
286
|
+
now = datetime.now()
|
|
287
|
+
state_data = {
|
|
288
|
+
"agent_name": self.name,
|
|
289
|
+
"created_at": now.isoformat(),
|
|
290
|
+
"memory": self.memory.to_dict()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
state_hash = self._generate_hash(state_data)
|
|
294
|
+
state_data["hash"] = state_hash
|
|
295
|
+
|
|
296
|
+
state_dir = self._get_state_dir()
|
|
297
|
+
|
|
298
|
+
# Write the state file (always keyed by hash)
|
|
299
|
+
state_path = os.path.join(state_dir, f"{state_hash}.state")
|
|
300
|
+
with open(state_path, 'w') as f:
|
|
301
|
+
json.dump(state_data, f, indent=2)
|
|
302
|
+
|
|
303
|
+
# Update index
|
|
304
|
+
index = self._read_index()
|
|
305
|
+
existing = [s["hash"] for s in index["states"]]
|
|
306
|
+
if state_hash not in existing:
|
|
307
|
+
index["states"].append({
|
|
308
|
+
"hash": state_hash,
|
|
309
|
+
"created_at": now.strftime("%Y-%m-%d %H:%M:%S"),
|
|
310
|
+
"key_count": len(self.memory.to_dict())
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
index["latest"] = state_hash
|
|
314
|
+
|
|
315
|
+
# Register name -> hash mapping
|
|
316
|
+
if "names" not in index:
|
|
317
|
+
index["names"] = {}
|
|
318
|
+
if name:
|
|
319
|
+
index["names"][name] = state_hash
|
|
320
|
+
|
|
321
|
+
self._write_index(index)
|
|
322
|
+
|
|
323
|
+
label = name or state_hash
|
|
324
|
+
print(f"[{label}] State saved ({len(self.memory.to_dict())} keys)")
|
|
325
|
+
return label
|
|
326
|
+
|
|
327
|
+
def load_state(self, identifier: Optional[str] = None) -> bool:
|
|
328
|
+
"""
|
|
329
|
+
Load a saved state by hash, name, or latest.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
identifier: Hash, friendly name, or None for latest.
|
|
333
|
+
|
|
334
|
+
Examples:
|
|
335
|
+
agent.load_state("a1b2c3d4") # by hash
|
|
336
|
+
agent.load_state("before-training") # by name
|
|
337
|
+
agent.load_state() # latest
|
|
338
|
+
"""
|
|
339
|
+
index = self._read_index()
|
|
340
|
+
|
|
341
|
+
if identifier is None:
|
|
342
|
+
state_hash = index.get("latest")
|
|
343
|
+
if not state_hash:
|
|
344
|
+
print("No saved states found.")
|
|
345
|
+
return False
|
|
346
|
+
else:
|
|
347
|
+
# Check if it's a name first
|
|
348
|
+
names = index.get("names", {})
|
|
349
|
+
state_hash = names.get(identifier, identifier)
|
|
350
|
+
|
|
351
|
+
state_path = os.path.join(self._get_state_dir(), f"{state_hash}.state")
|
|
352
|
+
if not os.path.exists(state_path):
|
|
353
|
+
print(f"State '{identifier or state_hash}' not found.")
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
with open(state_path, 'r') as f:
|
|
357
|
+
state_data = json.load(f)
|
|
358
|
+
|
|
359
|
+
self.memory.from_dict(state_data.get("memory", {}))
|
|
360
|
+
print(f"Loaded state '{identifier or state_hash}' ({len(self.memory.to_dict())} keys)")
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
def show_states(self) -> list:
|
|
364
|
+
"""Show all saved states for this agent."""
|
|
365
|
+
index = self._read_index()
|
|
366
|
+
states = index.get("states", [])
|
|
367
|
+
latest = index.get("latest")
|
|
368
|
+
names = index.get("names", {})
|
|
369
|
+
|
|
370
|
+
# Reverse name map: hash -> name
|
|
371
|
+
hash_to_name = {v: k for k, v in names.items()}
|
|
372
|
+
|
|
373
|
+
if not states:
|
|
374
|
+
print(f"No saved states for '{self.name}'")
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
print(f"\nStates for '{self.name}':")
|
|
378
|
+
result = []
|
|
379
|
+
for s in reversed(states):
|
|
380
|
+
is_latest = s["hash"] == latest
|
|
381
|
+
tag = "[latest] " if is_latest else " "
|
|
382
|
+
friendly = f" ({hash_to_name[s['hash']]})" if s["hash"] in hash_to_name else ""
|
|
383
|
+
print(f" {tag}{s['hash']}{friendly} {s['created_at']} ({s['key_count']} keys)")
|
|
384
|
+
result.append(s)
|
|
385
|
+
print()
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
def delete_state(self, identifier: str):
|
|
389
|
+
"""Delete a saved state by hash or name."""
|
|
390
|
+
index = self._read_index()
|
|
391
|
+
names = index.get("names", {})
|
|
392
|
+
state_hash = names.get(identifier, identifier)
|
|
393
|
+
|
|
394
|
+
state_path = os.path.join(self._get_state_dir(), f"{state_hash}.state")
|
|
395
|
+
if os.path.exists(state_path):
|
|
396
|
+
os.remove(state_path)
|
|
397
|
+
|
|
398
|
+
index["states"] = [s for s in index["states"] if s["hash"] != state_hash]
|
|
399
|
+
|
|
400
|
+
# Remove name mapping if exists
|
|
401
|
+
names_to_remove = [k for k, v in names.items() if v == state_hash]
|
|
402
|
+
for k in names_to_remove:
|
|
403
|
+
del names[k]
|
|
404
|
+
|
|
405
|
+
if index.get("latest") == state_hash:
|
|
406
|
+
index["latest"] = index["states"][-1]["hash"] if index["states"] else None
|
|
407
|
+
|
|
408
|
+
self._write_index(index)
|
|
409
|
+
print(f"Deleted state '{identifier}'")
|
|
410
|
+
|
|
411
|
+
# ── UAF Export ──────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
def export(self, path: str, version: str = "0.1.0"):
|
|
414
|
+
"""
|
|
415
|
+
Export the Agent into a UAF format using the v2 Manifest structure.
|
|
416
|
+
Memory is auto-serialized into agent.state inside the archive.
|
|
417
|
+
"""
|
|
418
|
+
import tempfile
|
|
419
|
+
import shutil
|
|
420
|
+
import os
|
|
421
|
+
|
|
422
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
423
|
+
all_t = self.registry.get_all_tools()
|
|
424
|
+
builtin_names = [t.name for t in all_t if t.name in default_registry.builtin_tools]
|
|
425
|
+
custom_names = [t.name for t in all_t if t.name not in default_registry.builtin_tools]
|
|
426
|
+
|
|
427
|
+
has_state = len(self.memory.to_dict()) > 0
|
|
428
|
+
|
|
429
|
+
# --- 1. agent.yaml (v2 schema — matches UAFv2AgentYaml) ---
|
|
430
|
+
manifest = {
|
|
431
|
+
"uaf_version": 2,
|
|
432
|
+
"agent": {
|
|
433
|
+
"name": self.name,
|
|
434
|
+
"version": version,
|
|
435
|
+
"description": self.description,
|
|
436
|
+
"author": self.author
|
|
437
|
+
},
|
|
438
|
+
"runtime": {
|
|
439
|
+
"engine": "python",
|
|
440
|
+
"entrypoint": "agent.py"
|
|
441
|
+
},
|
|
442
|
+
"sdk": {
|
|
443
|
+
"name": "agentcomet",
|
|
444
|
+
"version": "0.1.0"
|
|
445
|
+
},
|
|
446
|
+
"tools": {
|
|
447
|
+
"builtin": builtin_names,
|
|
448
|
+
"custom": custom_names
|
|
449
|
+
},
|
|
450
|
+
"state": {
|
|
451
|
+
"enabled": has_state,
|
|
452
|
+
"file": "agent.state" if has_state else None
|
|
453
|
+
},
|
|
454
|
+
"dependencies": {
|
|
455
|
+
"auto": True
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
with open(os.path.join(temp_dir, 'agent.yaml'), 'w') as f:
|
|
460
|
+
import yaml
|
|
461
|
+
yaml.dump(manifest, f, sort_keys=False)
|
|
462
|
+
|
|
463
|
+
# --- 2. agent.py ---
|
|
464
|
+
with open(os.path.join(temp_dir, 'agent.py'), 'w') as f:
|
|
465
|
+
f.write("from agentcomet.agents.factory import create_agent\n")
|
|
466
|
+
if builtin_names:
|
|
467
|
+
f.write("from agentcomet.tools import " + ", ".join(builtin_names) + "\n")
|
|
468
|
+
if custom_names:
|
|
469
|
+
f.write("from tools import " + ", ".join(custom_names) + "\n")
|
|
470
|
+
f.write("\n")
|
|
471
|
+
|
|
472
|
+
tool_list_str = "[" + ", ".join(builtin_names + custom_names) + "]"
|
|
473
|
+
|
|
474
|
+
f.write(f"agent = create_agent(\n")
|
|
475
|
+
f.write(f" name='{self.name}',\n")
|
|
476
|
+
f.write(f" description='{self.description}',\n")
|
|
477
|
+
f.write(f" author='{self.author}',\n")
|
|
478
|
+
f.write(f" llm='{self._llm_provider}',\n")
|
|
479
|
+
if builtin_names or custom_names:
|
|
480
|
+
f.write(f" tools={tool_list_str},\n")
|
|
481
|
+
f.write(")\n")
|
|
482
|
+
|
|
483
|
+
# --- 3. tools.py ---
|
|
484
|
+
if custom_names:
|
|
485
|
+
with open(os.path.join(temp_dir, 'tools.py'), 'w') as f:
|
|
486
|
+
f.write("from agentcomet.tools import tool\n\n")
|
|
487
|
+
import inspect
|
|
488
|
+
for t_name in custom_names:
|
|
489
|
+
t_spec = [t for t in all_t if t.name == t_name][0]
|
|
490
|
+
try:
|
|
491
|
+
source = inspect.getsource(t_spec.fn)
|
|
492
|
+
f.write(source + "\n\n")
|
|
493
|
+
except Exception as e:
|
|
494
|
+
f.write(f"# Could not extract source for {t_name}: {e}\n")
|
|
495
|
+
|
|
496
|
+
# --- 4. sdk metadata ---
|
|
497
|
+
os.makedirs(os.path.join(temp_dir, 'sdk'))
|
|
498
|
+
with open(os.path.join(temp_dir, 'sdk', 'agentcomet.json'), 'w') as f:
|
|
499
|
+
json.dump({"framework": "AgentComet", "compatible_version": ">=0.1.0"}, f)
|
|
500
|
+
|
|
501
|
+
# --- 5. requirements.txt ---
|
|
502
|
+
with open(os.path.join(temp_dir, 'requirements.txt'), 'w') as f:
|
|
503
|
+
f.write("agentcomet\n")
|
|
504
|
+
|
|
505
|
+
# --- 6. agent.state (auto-serialized from self.memory) ---
|
|
506
|
+
if has_state:
|
|
507
|
+
with open(os.path.join(temp_dir, 'agent.state'), 'w') as f:
|
|
508
|
+
json.dump(self.memory.to_dict(), f)
|
|
509
|
+
|
|
510
|
+
# --- 7. Build .uaf archive directly (tar.gz) ---
|
|
511
|
+
import tarfile
|
|
512
|
+
uaf_output = os.path.join(temp_dir, "temp_agent.uaf")
|
|
513
|
+
|
|
514
|
+
files_to_pack = [
|
|
515
|
+
"agent.yaml",
|
|
516
|
+
"agent.py",
|
|
517
|
+
"requirements.txt",
|
|
518
|
+
"sdk/agentcomet.json"
|
|
519
|
+
]
|
|
520
|
+
if custom_names:
|
|
521
|
+
files_to_pack.append("tools.py")
|
|
522
|
+
if has_state:
|
|
523
|
+
files_to_pack.append("agent.state")
|
|
524
|
+
|
|
525
|
+
with tarfile.open(uaf_output, "w:gz") as tar:
|
|
526
|
+
for fname in files_to_pack:
|
|
527
|
+
fpath = os.path.join(temp_dir, fname)
|
|
528
|
+
if os.path.exists(fpath):
|
|
529
|
+
tar.add(fpath, arcname=fname)
|
|
530
|
+
|
|
531
|
+
# Copy to destination path
|
|
532
|
+
shutil.copy2(uaf_output, path)
|
|
533
|
+
print(f"Exported AgentComet agent '{self.name}' to {path}")
|
|
534
|
+
|
|
535
|
+
def push_local(self, version: str = "auto", readme: Optional[str] = None) -> dict:
|
|
536
|
+
"""
|
|
537
|
+
Push the agent to a locally hosted AgentComet server.
|
|
538
|
+
"""
|
|
539
|
+
import requests
|
|
540
|
+
import tempfile
|
|
541
|
+
import os
|
|
542
|
+
from agentcomet.settings import Settings
|
|
543
|
+
|
|
544
|
+
url = Settings.get_url()
|
|
545
|
+
key = Settings.get_key()
|
|
546
|
+
|
|
547
|
+
if not url or not key:
|
|
548
|
+
raise ValueError("AGENTCOMET_LOCAL_URL and AGENTCOMET_LOCAL_KEY must be set to push locally.")
|
|
549
|
+
|
|
550
|
+
target_version = version
|
|
551
|
+
if version == "auto":
|
|
552
|
+
try:
|
|
553
|
+
resp = requests.get(f"{url}/api/sdk/agents/{self.name}", headers={"Authorization": f"Bearer {key}"}, timeout=10)
|
|
554
|
+
if resp.status_code == 200:
|
|
555
|
+
data = resp.json()
|
|
556
|
+
current_version = data.get("version", "0.1.0")
|
|
557
|
+
parts = current_version.split(".")
|
|
558
|
+
if len(parts) >= 3 and parts[-1].isdigit():
|
|
559
|
+
parts[-1] = str(int(parts[-1]) + 1)
|
|
560
|
+
target_version = ".".join(parts)
|
|
561
|
+
else:
|
|
562
|
+
target_version = "0.1.1"
|
|
563
|
+
else:
|
|
564
|
+
target_version = "0.1.0"
|
|
565
|
+
except Exception:
|
|
566
|
+
target_version = "0.1.0"
|
|
567
|
+
|
|
568
|
+
readme_text = readme if readme is not None else self.description
|
|
569
|
+
|
|
570
|
+
with tempfile.NamedTemporaryFile(suffix=".uaf", delete=False) as tmp:
|
|
571
|
+
tmp_path = tmp.name
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
self.export(tmp_path, version=target_version)
|
|
575
|
+
|
|
576
|
+
with open(tmp_path, "rb") as f:
|
|
577
|
+
files = {
|
|
578
|
+
"artifact": (f"{self.name}.uaf", f, "application/octet-stream")
|
|
579
|
+
}
|
|
580
|
+
data = {
|
|
581
|
+
"name": self.name,
|
|
582
|
+
"description": self.description,
|
|
583
|
+
"version": target_version,
|
|
584
|
+
"readme": readme_text
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
push_url = f"{url}/api/sdk/agents/push"
|
|
588
|
+
headers = {"Authorization": f"Bearer {key}"}
|
|
589
|
+
|
|
590
|
+
resp = requests.post(push_url, headers=headers, data=data, files=files, timeout=60)
|
|
591
|
+
resp.raise_for_status()
|
|
592
|
+
|
|
593
|
+
print(f"Successfully pushed '{self.name}' v{target_version} to {url}")
|
|
594
|
+
return resp.json()
|
|
595
|
+
finally:
|
|
596
|
+
if os.path.exists(tmp_path):
|
|
597
|
+
os.remove(tmp_path)
|
|
598
|
+
|
|
599
|
+
@classmethod
|
|
600
|
+
def pull_local(cls, agent_name: str, version: str = "latest"):
|
|
601
|
+
"""
|
|
602
|
+
Pull an agent from a locally hosted AgentComet server.
|
|
603
|
+
"""
|
|
604
|
+
import requests
|
|
605
|
+
import os
|
|
606
|
+
import tempfile
|
|
607
|
+
from agentcomet.settings import Settings
|
|
608
|
+
from agentcomet.agents.loader import load_agent
|
|
609
|
+
|
|
610
|
+
url = Settings.get_url()
|
|
611
|
+
key = Settings.get_key()
|
|
612
|
+
|
|
613
|
+
if not url or not key:
|
|
614
|
+
raise ValueError("AGENTCOMET_LOCAL_URL and AGENTCOMET_LOCAL_KEY must be set to pull locally.")
|
|
615
|
+
|
|
616
|
+
params = {"version": version} if version and version != "latest" else None
|
|
617
|
+
|
|
618
|
+
pull_url = f"{url}/api/sdk/agents/{agent_name}/pull"
|
|
619
|
+
headers = {"Authorization": f"Bearer {key}"}
|
|
620
|
+
|
|
621
|
+
resp = requests.get(pull_url, headers=headers, params=params, timeout=60)
|
|
622
|
+
resp.raise_for_status()
|
|
623
|
+
|
|
624
|
+
with tempfile.NamedTemporaryFile(suffix=".uaf", delete=False) as tmp:
|
|
625
|
+
tmp_path = tmp.name
|
|
626
|
+
|
|
627
|
+
try:
|
|
628
|
+
with open(tmp_path, "wb") as f:
|
|
629
|
+
f.write(resp.content)
|
|
630
|
+
|
|
631
|
+
agent = load_agent(tmp_path)
|
|
632
|
+
print(f"Successfully pulled and loaded '{agent_name}' v{version}")
|
|
633
|
+
return agent
|
|
634
|
+
finally:
|
|
635
|
+
if os.path.exists(tmp_path):
|
|
636
|
+
os.remove(tmp_path)
|
|
637
|
+
|
|
638
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
class BaseAgent(ABC):
|
|
5
|
+
"""
|
|
6
|
+
Abstract base class for all agents in the AgentComet framework.
|
|
7
|
+
"""
|
|
8
|
+
def __init__(self, name: str, config: Optional[Dict[str, Any]] = None, llm: Any = None):
|
|
9
|
+
self.name = name
|
|
10
|
+
self.config = config or {}
|
|
11
|
+
self.llm = llm
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def invoke(self, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Execute the agent logic given the current state.
|
|
17
|
+
Returns the updated state or a dictionary of updates.
|
|
18
|
+
"""
|
|
19
|
+
pass
|