agmem 0.2.0__py3-none-any.whl → 0.3.0__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.
@@ -0,0 +1,435 @@
1
+ """
2
+ Multi-Agent Collaboration - Trust management and agent registry.
3
+
4
+ This module provides:
5
+ - Agent identity and key management
6
+ - Trust relationships between agents
7
+ - Contribution tracking and attribution
8
+ - Conflict detection and resolution helpers
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import uuid
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional, Set, Tuple
19
+
20
+
21
+ @dataclass
22
+ class Agent:
23
+ """Represents an agent identity."""
24
+
25
+ agent_id: str
26
+ name: str
27
+ public_key: Optional[str] = None
28
+ created_at: Optional[str] = None
29
+ metadata: Dict[str, Any] = field(default_factory=dict)
30
+
31
+ def to_dict(self) -> Dict[str, Any]:
32
+ return {
33
+ "agent_id": self.agent_id,
34
+ "name": self.name,
35
+ "public_key": self.public_key,
36
+ "created_at": self.created_at,
37
+ "metadata": self.metadata,
38
+ }
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: Dict[str, Any]) -> "Agent":
42
+ return cls(
43
+ agent_id=data["agent_id"],
44
+ name=data["name"],
45
+ public_key=data.get("public_key"),
46
+ created_at=data.get("created_at"),
47
+ metadata=data.get("metadata", {}),
48
+ )
49
+
50
+
51
+ @dataclass
52
+ class TrustRelation:
53
+ """A trust relationship between two agents."""
54
+
55
+ from_agent: str
56
+ to_agent: str
57
+ trust_level: str # "full", "partial", "read-only", "none"
58
+ created_at: str
59
+ reason: Optional[str] = None
60
+ expires_at: Optional[str] = None
61
+
62
+ def to_dict(self) -> Dict[str, Any]:
63
+ return {
64
+ "from_agent": self.from_agent,
65
+ "to_agent": self.to_agent,
66
+ "trust_level": self.trust_level,
67
+ "created_at": self.created_at,
68
+ "reason": self.reason,
69
+ "expires_at": self.expires_at,
70
+ }
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: Dict[str, Any]) -> "TrustRelation":
74
+ return cls(
75
+ from_agent=data["from_agent"],
76
+ to_agent=data["to_agent"],
77
+ trust_level=data["trust_level"],
78
+ created_at=data["created_at"],
79
+ reason=data.get("reason"),
80
+ expires_at=data.get("expires_at"),
81
+ )
82
+
83
+
84
+ @dataclass
85
+ class Contribution:
86
+ """A contribution by an agent to memory."""
87
+
88
+ agent_id: str
89
+ commit_hash: str
90
+ timestamp: str
91
+ files_changed: int
92
+ additions: int
93
+ deletions: int
94
+ message: Optional[str] = None
95
+
96
+ def to_dict(self) -> Dict[str, Any]:
97
+ return {
98
+ "agent_id": self.agent_id,
99
+ "commit_hash": self.commit_hash,
100
+ "timestamp": self.timestamp,
101
+ "files_changed": self.files_changed,
102
+ "additions": self.additions,
103
+ "deletions": self.deletions,
104
+ "message": self.message,
105
+ }
106
+
107
+
108
+ class AgentRegistry:
109
+ """Registry for managing agent identities."""
110
+
111
+ def __init__(self, mem_dir: Path):
112
+ self.mem_dir = Path(mem_dir)
113
+ self.agents_file = self.mem_dir / "agents.json"
114
+ self._agents: Dict[str, Agent] = {}
115
+ self._load()
116
+
117
+ def _load(self) -> None:
118
+ """Load agents from disk."""
119
+ if self.agents_file.exists():
120
+ try:
121
+ data = json.loads(self.agents_file.read_text())
122
+ self._agents = {
123
+ aid: Agent.from_dict(a) for aid, a in data.get("agents", {}).items()
124
+ }
125
+ except Exception:
126
+ self._agents = {}
127
+
128
+ def _save(self) -> None:
129
+ """Save agents to disk."""
130
+ self.mem_dir.mkdir(parents=True, exist_ok=True)
131
+ data = {"agents": {aid: a.to_dict() for aid, a in self._agents.items()}}
132
+ self.agents_file.write_text(json.dumps(data, indent=2))
133
+
134
+ def register_agent(
135
+ self, name: str, public_key: Optional[str] = None, metadata: Optional[Dict] = None
136
+ ) -> Agent:
137
+ """Register a new agent."""
138
+ agent_id = hashlib.sha256(
139
+ f"{name}{datetime.now(timezone.utc).isoformat()}".encode()
140
+ ).hexdigest()[:16]
141
+
142
+ agent = Agent(
143
+ agent_id=agent_id,
144
+ name=name,
145
+ public_key=public_key,
146
+ created_at=datetime.now(timezone.utc).isoformat(),
147
+ metadata=metadata or {},
148
+ )
149
+
150
+ self._agents[agent_id] = agent
151
+ self._save()
152
+ return agent
153
+
154
+ def get_agent(self, agent_id: str) -> Optional[Agent]:
155
+ """Get an agent by ID."""
156
+ return self._agents.get(agent_id)
157
+
158
+ def list_agents(self) -> List[Agent]:
159
+ """List all registered agents."""
160
+ return list(self._agents.values())
161
+
162
+ def remove_agent(self, agent_id: str) -> bool:
163
+ """Remove an agent."""
164
+ if agent_id in self._agents:
165
+ del self._agents[agent_id]
166
+ self._save()
167
+ return True
168
+ return False
169
+
170
+
171
+ class TrustManager:
172
+ """Manages trust relationships between agents."""
173
+
174
+ TRUST_LEVELS = ["full", "partial", "read-only", "none"]
175
+
176
+ def __init__(self, mem_dir: Path):
177
+ self.mem_dir = Path(mem_dir)
178
+ self.trust_file = self.mem_dir / "trust.json"
179
+ self._relations: List[TrustRelation] = []
180
+ self._load()
181
+
182
+ def _load(self) -> None:
183
+ """Load trust relations from disk."""
184
+ if self.trust_file.exists():
185
+ try:
186
+ data = json.loads(self.trust_file.read_text())
187
+ self._relations = [TrustRelation.from_dict(r) for r in data.get("relations", [])]
188
+ except Exception:
189
+ self._relations = []
190
+
191
+ def _save(self) -> None:
192
+ """Save trust relations to disk."""
193
+ self.mem_dir.mkdir(parents=True, exist_ok=True)
194
+ data = {"relations": [r.to_dict() for r in self._relations]}
195
+ self.trust_file.write_text(json.dumps(data, indent=2))
196
+
197
+ def grant_trust(
198
+ self,
199
+ from_agent: str,
200
+ to_agent: str,
201
+ trust_level: str,
202
+ reason: Optional[str] = None,
203
+ expires_at: Optional[str] = None,
204
+ ) -> TrustRelation:
205
+ """Grant trust from one agent to another."""
206
+ if trust_level not in self.TRUST_LEVELS:
207
+ raise ValueError(f"Invalid trust level: {trust_level}")
208
+
209
+ # Remove existing relation
210
+ self._relations = [
211
+ r
212
+ for r in self._relations
213
+ if not (r.from_agent == from_agent and r.to_agent == to_agent)
214
+ ]
215
+
216
+ relation = TrustRelation(
217
+ from_agent=from_agent,
218
+ to_agent=to_agent,
219
+ trust_level=trust_level,
220
+ created_at=datetime.now(timezone.utc).isoformat(),
221
+ reason=reason,
222
+ expires_at=expires_at,
223
+ )
224
+
225
+ self._relations.append(relation)
226
+ self._save()
227
+ return relation
228
+
229
+ def revoke_trust(self, from_agent: str, to_agent: str) -> bool:
230
+ """Revoke trust between agents."""
231
+ original_count = len(self._relations)
232
+ self._relations = [
233
+ r
234
+ for r in self._relations
235
+ if not (r.from_agent == from_agent and r.to_agent == to_agent)
236
+ ]
237
+ if len(self._relations) < original_count:
238
+ self._save()
239
+ return True
240
+ return False
241
+
242
+ def get_trust_level(self, from_agent: str, to_agent: str) -> str:
243
+ """Get trust level from one agent to another."""
244
+ for r in self._relations:
245
+ if r.from_agent == from_agent and r.to_agent == to_agent:
246
+ return r.trust_level
247
+ return "none"
248
+
249
+ def get_trusted_by(self, agent_id: str) -> List[TrustRelation]:
250
+ """Get agents that trust this agent."""
251
+ return [r for r in self._relations if r.to_agent == agent_id]
252
+
253
+ def get_trusts(self, agent_id: str) -> List[TrustRelation]:
254
+ """Get agents this agent trusts."""
255
+ return [r for r in self._relations if r.from_agent == agent_id]
256
+
257
+ def get_trust_graph(self) -> Dict[str, Any]:
258
+ """Get trust graph data for visualization."""
259
+ agents: Set[str] = set()
260
+ for r in self._relations:
261
+ agents.add(r.from_agent)
262
+ agents.add(r.to_agent)
263
+
264
+ nodes = [{"id": a, "name": a[:8]} for a in agents]
265
+ links = [
266
+ {
267
+ "source": r.from_agent,
268
+ "target": r.to_agent,
269
+ "trust_level": r.trust_level,
270
+ "value": {"full": 3, "partial": 2, "read-only": 1, "none": 0}.get(r.trust_level, 0),
271
+ }
272
+ for r in self._relations
273
+ ]
274
+
275
+ return {"nodes": nodes, "links": links}
276
+
277
+
278
+ class ContributionTracker:
279
+ """Tracks contributions by agents to memory."""
280
+
281
+ def __init__(self, mem_dir: Path):
282
+ self.mem_dir = Path(mem_dir)
283
+ self.contributions_file = self.mem_dir / "contributions.json"
284
+ self._contributions: List[Contribution] = []
285
+ self._load()
286
+
287
+ def _load(self) -> None:
288
+ """Load contributions from disk."""
289
+ if self.contributions_file.exists():
290
+ try:
291
+ data = json.loads(self.contributions_file.read_text())
292
+ self._contributions = [Contribution(**c) for c in data.get("contributions", [])]
293
+ except Exception:
294
+ self._contributions = []
295
+
296
+ def _save(self) -> None:
297
+ """Save contributions to disk."""
298
+ self.mem_dir.mkdir(parents=True, exist_ok=True)
299
+ data = {"contributions": [c.to_dict() for c in self._contributions]}
300
+ self.contributions_file.write_text(json.dumps(data, indent=2))
301
+
302
+ def record_contribution(
303
+ self,
304
+ agent_id: str,
305
+ commit_hash: str,
306
+ files_changed: int,
307
+ additions: int,
308
+ deletions: int,
309
+ message: Optional[str] = None,
310
+ ) -> Contribution:
311
+ """Record a contribution by an agent."""
312
+ contribution = Contribution(
313
+ agent_id=agent_id,
314
+ commit_hash=commit_hash,
315
+ timestamp=datetime.now(timezone.utc).isoformat(),
316
+ files_changed=files_changed,
317
+ additions=additions,
318
+ deletions=deletions,
319
+ message=message,
320
+ )
321
+
322
+ self._contributions.append(contribution)
323
+ self._save()
324
+ return contribution
325
+
326
+ def get_contributions(self, agent_id: str) -> List[Contribution]:
327
+ """Get all contributions by an agent."""
328
+ return [c for c in self._contributions if c.agent_id == agent_id]
329
+
330
+ def get_leaderboard(self, limit: int = 10) -> List[Dict[str, Any]]:
331
+ """Get leaderboard of top contributors."""
332
+ stats: Dict[str, Dict[str, int]] = {}
333
+
334
+ for c in self._contributions:
335
+ if c.agent_id not in stats:
336
+ stats[c.agent_id] = {"commits": 0, "additions": 0, "deletions": 0}
337
+ stats[c.agent_id]["commits"] += 1
338
+ stats[c.agent_id]["additions"] += c.additions
339
+ stats[c.agent_id]["deletions"] += c.deletions
340
+
341
+ sorted_agents = sorted(
342
+ stats.items(),
343
+ key=lambda x: x[1]["commits"],
344
+ reverse=True,
345
+ )
346
+
347
+ return [
348
+ {"agent_id": aid, "rank": i + 1, **s}
349
+ for i, (aid, s) in enumerate(sorted_agents[:limit])
350
+ ]
351
+
352
+ def get_timeline(self, limit: int = 20) -> List[Contribution]:
353
+ """Get recent contributions timeline."""
354
+ sorted_contributions = sorted(
355
+ self._contributions,
356
+ key=lambda c: c.timestamp,
357
+ reverse=True,
358
+ )
359
+ return sorted_contributions[:limit]
360
+
361
+
362
+ class ConflictDetector:
363
+ """Detects and helps resolve conflicts between agents."""
364
+
365
+ def __init__(self, repo_root: Path):
366
+ self.repo_root = Path(repo_root)
367
+
368
+ def detect_conflicts(self, base_commit: str, head_commits: List[str]) -> List[Dict[str, Any]]:
369
+ """Detect conflicts between commits from different agents."""
370
+ conflicts = []
371
+
372
+ try:
373
+ from memvcs.core.repository import Repository
374
+ from memvcs.core.diff import DiffEngine
375
+
376
+ repo = Repository(self.repo_root)
377
+ engine = DiffEngine(repo.object_store)
378
+
379
+ # Get files changed in each head commit
380
+ head_files: Dict[str, Set[str]] = {}
381
+ for head in head_commits:
382
+ diff = engine.diff_commits(base_commit, head)
383
+ head_files[head] = set(f.path for f in diff.files)
384
+
385
+ # Find overlapping files
386
+ all_heads = list(head_files.keys())
387
+ for i, head1 in enumerate(all_heads):
388
+ for head2 in all_heads[i + 1 :]:
389
+ overlapping = head_files[head1] & head_files[head2]
390
+ for path in overlapping:
391
+ conflicts.append(
392
+ {
393
+ "path": path,
394
+ "commits": [head1, head2],
395
+ "type": "concurrent_modification",
396
+ }
397
+ )
398
+
399
+ except Exception:
400
+ pass
401
+
402
+ return conflicts
403
+
404
+ def suggest_resolution(self, conflict: Dict[str, Any]) -> Dict[str, Any]:
405
+ """Suggest a resolution for a conflict."""
406
+ suggestions = {
407
+ "concurrent_modification": [
408
+ "Use the most recent version",
409
+ "Merge changes manually",
410
+ "Ask the agents to resolve",
411
+ ],
412
+ }
413
+
414
+ conflict_type = conflict.get("type", "unknown")
415
+ return {
416
+ "conflict": conflict,
417
+ "suggestions": suggestions.get(conflict_type, ["Manual review required"]),
418
+ }
419
+
420
+
421
+ # --- Web UI Helpers ---
422
+
423
+
424
+ def get_collaboration_dashboard(mem_dir: Path) -> Dict[str, Any]:
425
+ """Get data for the collaboration dashboard."""
426
+ registry = AgentRegistry(mem_dir)
427
+ trust_mgr = TrustManager(mem_dir)
428
+ contrib_tracker = ContributionTracker(mem_dir)
429
+
430
+ return {
431
+ "agents": [a.to_dict() for a in registry.list_agents()],
432
+ "trust_graph": trust_mgr.get_trust_graph(),
433
+ "leaderboard": contrib_tracker.get_leaderboard(),
434
+ "recent_activity": [c.to_dict() for c in contrib_tracker.get_timeline(10)],
435
+ }