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.
Files changed (40) hide show
  1. agentcomet/__init__.py +10 -0
  2. agentcomet/agents/__init__.py +10 -0
  3. agentcomet/agents/agent.py +638 -0
  4. agentcomet/agents/base_agent.py +19 -0
  5. agentcomet/agents/factory.py +21 -0
  6. agentcomet/agents/loader.py +96 -0
  7. agentcomet/agents/registry.py +34 -0
  8. agentcomet/cli.py +95 -0
  9. agentcomet/communication/__init__.py +5 -0
  10. agentcomet/communication/message_broker.py +45 -0
  11. agentcomet/communication/protocols.py +17 -0
  12. agentcomet/communication/state_manager.py +20 -0
  13. agentcomet/memory.py +66 -0
  14. agentcomet/models/__init__.py +4 -0
  15. agentcomet/models/base_model.py +17 -0
  16. agentcomet/models/providers.py +271 -0
  17. agentcomet/orchestrators/__init__.py +4 -0
  18. agentcomet/orchestrators/agent_orchestrator.py +70 -0
  19. agentcomet/orchestrators/execution_engine.py +83 -0
  20. agentcomet/settings.py +22 -0
  21. agentcomet/tools/__init__.py +10 -0
  22. agentcomet/tools/core.py +81 -0
  23. agentcomet/tools/registry.py +41 -0
  24. agentcomet/utils/__init__.py +4 -0
  25. agentcomet/utils/serialization.py +24 -0
  26. agentcomet/utils/validation.py +15 -0
  27. agentcomet/vcs/__init__.py +3 -0
  28. agentcomet/vcs/repository.py +156 -0
  29. agentcomet/workflows/__init__.py +7 -0
  30. agentcomet/workflows/conditional_router.py +29 -0
  31. agentcomet/workflows/dag_builder.py +47 -0
  32. agentcomet/workflows/patterns.py +11 -0
  33. agentcomet/workflows/templates.py +74 -0
  34. agentcomet/workflows/workflow_builder.py +58 -0
  35. agentcomet-0.1.0b1.dist-info/METADATA +238 -0
  36. agentcomet-0.1.0b1.dist-info/RECORD +40 -0
  37. agentcomet-0.1.0b1.dist-info/WHEEL +5 -0
  38. agentcomet-0.1.0b1.dist-info/licenses/LICENSE +201 -0
  39. agentcomet-0.1.0b1.dist-info/licenses/NOTICE +6 -0
  40. 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,10 @@
1
+ from .base_agent import BaseAgent
2
+ from .agent import Agent
3
+ from .factory import create_agent
4
+ from .loader import load_agent
5
+ from .registry import AgentRegistry
6
+
7
+
8
+
9
+
10
+ __all__ = ["BaseAgent", "Agent", "create_agent", "AgentRegistry", "load_agent"]
@@ -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