crewlayer 0.1.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,538 @@
1
+ """Microsoft AutoGen integration for the CrewLayer SDK.
2
+
3
+ Provides four adapters purpose-built for multi-agent workflows:
4
+
5
+ - ``CrewLayerConversableAgent`` — ConversableAgent that persists every
6
+ message to CrewLayer memory and logs
7
+ each send/receive as an action
8
+ - ``CrewLayerGroupChatManager`` — GroupChatManager that keeps the shared
9
+ CrewLayer blackboard in sync after every
10
+ group chat turn (the killer feature for
11
+ multi-agent workflows)
12
+ - ``CrewLayerAgentMemory`` — helper that loads an agent's long-term
13
+ memories as initial system context
14
+ - ``sync_agent_status`` — maps AutoGen thinking/idle states to
15
+ CrewLayer working/idle
16
+
17
+ Install::
18
+
19
+ pip install crewlayer[autogen]
20
+
21
+ Usage::
22
+
23
+ from crewlayer import CrewLayerClient
24
+ from crewlayer.integrations.autogen import (
25
+ CrewLayerConversableAgent,
26
+ CrewLayerGroupChatManager,
27
+ CrewLayerAgentMemory,
28
+ sync_agent_status,
29
+ )
30
+ import autogen
31
+
32
+ client = CrewLayerClient(api_key="crwl_...")
33
+
34
+ agent_a = CrewLayerConversableAgent(
35
+ name="researcher",
36
+ client=client,
37
+ agent_id="<uuid-a>",
38
+ system_message="You are a research assistant.",
39
+ llm_config={"model": "gpt-4"},
40
+ )
41
+ agent_b = CrewLayerConversableAgent(
42
+ name="writer",
43
+ client=client,
44
+ agent_id="<uuid-b>",
45
+ system_message="You are a technical writer.",
46
+ llm_config={"model": "gpt-4"},
47
+ )
48
+
49
+ groupchat = autogen.GroupChat(agents=[agent_a, agent_b], messages=[], max_round=10)
50
+ manager = CrewLayerGroupChatManager(
51
+ client=client,
52
+ group_id="project-alpha",
53
+ groupchat=groupchat,
54
+ )
55
+
56
+ # Load long-term memories as initial context
57
+ CrewLayerAgentMemory(client=client, agent_id="<uuid-a>").apply(agent_a)
58
+
59
+ # Every turn → blackboard updated automatically
60
+ agent_a.initiate_chat(manager, message="Let's start researching CrewLayer.")
61
+ """
62
+ from __future__ import annotations
63
+
64
+ import time
65
+ from typing import Any
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Optional AutoGen imports — graceful fallback when not installed
69
+ # ---------------------------------------------------------------------------
70
+
71
+ try:
72
+ from autogen import ConversableAgent as _AGConversableAgent # type: ignore[import]
73
+ from autogen import GroupChatManager as _AGGroupChatManager # type: ignore[import]
74
+ _AUTOGEN = True
75
+ except ImportError:
76
+ _AUTOGEN = False
77
+
78
+ class _AutoGenStub: # type: ignore[no-redef]
79
+ """Minimal stub for testing without AutoGen installed."""
80
+
81
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
82
+ self.name: str = str(kwargs.get("name", ""))
83
+ self.system_message: str = str(kwargs.get("system_message", ""))
84
+
85
+ def send(self, message: Any, recipient: Any, **kwargs: Any) -> None:
86
+ pass
87
+
88
+ def receive(self, message: Any, sender: Any, **kwargs: Any) -> None:
89
+ pass
90
+
91
+ def update_system_message(self, system_message: str) -> None:
92
+ self.system_message = system_message
93
+
94
+ _AGConversableAgent = _AutoGenStub # type: ignore[assignment, misc]
95
+ _AGGroupChatManager = _AutoGenStub # type: ignore[assignment, misc]
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Internal helpers
100
+ # ---------------------------------------------------------------------------
101
+
102
+ # AutoGen agent-status → CrewLayer status
103
+ _STATUS_MAP: dict[str, str] = {
104
+ "thinking": "working",
105
+ "replying": "working",
106
+ "generating": "working",
107
+ "processing": "working",
108
+ "waiting": "idle",
109
+ "idle": "idle",
110
+ "error": "error",
111
+ }
112
+
113
+
114
+ def _extract_content(message: Any) -> str:
115
+ """Return plain-text content from an AutoGen message (str or dict)."""
116
+ if message is None:
117
+ return ""
118
+ if isinstance(message, str):
119
+ return message
120
+ if isinstance(message, dict):
121
+ content = message.get("content")
122
+ if content is not None:
123
+ return str(content)
124
+ if "tool_calls" in message:
125
+ return str(message["tool_calls"])
126
+ return str(message)
127
+ return str(message)
128
+
129
+
130
+ def _agent_name(agent: Any) -> str:
131
+ return getattr(agent, "name", None) or str(agent)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # CrewLayerConversableAgent
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ class CrewLayerConversableAgent(_AGConversableAgent): # type: ignore[misc]
140
+ """AutoGen ``ConversableAgent`` with automatic CrewLayer persistence.
141
+
142
+ Every message sent or received is:
143
+ - appended to CrewLayer short-term memory (builds conversation history)
144
+ - logged as an immutable action entry (full audit trail with duration)
145
+
146
+ CrewLayer-specific parameters are keyword-only and separated from the
147
+ AutoGen parameters, so all existing AutoGen kwargs pass through unchanged.
148
+
149
+ Args:
150
+ name: Agent name (forwarded to AutoGen).
151
+ client: A ``CrewLayerClient`` (sync) instance.
152
+ agent_id: Target agent UUID in CrewLayer.
153
+ session_id: Session key for short-term memory (default ``"default"``).
154
+ **kwargs: All other kwargs forwarded to ``ConversableAgent.__init__``.
155
+
156
+ Example::
157
+
158
+ agent = CrewLayerConversableAgent(
159
+ name="assistant",
160
+ client=client,
161
+ agent_id="agent-uuid",
162
+ system_message="You are helpful.",
163
+ llm_config={"model": "gpt-4"},
164
+ )
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ name: str,
170
+ *,
171
+ client: Any,
172
+ agent_id: str,
173
+ session_id: str = "default",
174
+ **kwargs: Any,
175
+ ) -> None:
176
+ super().__init__(name=name, **kwargs)
177
+ # __dict__ assignment bypasses Pydantic __setattr__ if AutoGen ever
178
+ # switches to Pydantic models.
179
+ self.__dict__.update({
180
+ "_cl_client": client,
181
+ "_cl_agent_id": agent_id,
182
+ "_cl_session": session_id,
183
+ })
184
+
185
+ # ------------------------------------------------------------------
186
+ # AutoGen ConversableAgent overrides
187
+ # ------------------------------------------------------------------
188
+
189
+ def send(
190
+ self,
191
+ message: Any,
192
+ recipient: Any,
193
+ request_reply: bool | None = None,
194
+ silent: bool | None = False,
195
+ ) -> None:
196
+ """Send a message; log the action and persist to memory afterwards."""
197
+ start = time.monotonic()
198
+ result = super().send(
199
+ message, recipient,
200
+ request_reply=request_reply,
201
+ silent=silent,
202
+ )
203
+ duration_ms = max(0, int((time.monotonic() - start) * 1000))
204
+ content = _extract_content(message)
205
+ recipient_name = _agent_name(recipient)
206
+ self._cl_save(
207
+ role="assistant",
208
+ content=content,
209
+ metadata={"direction": "send", "to": recipient_name},
210
+ )
211
+ self._cl_log(
212
+ tool_name="autogen.send",
213
+ input_params={"message": content[:500], "to": recipient_name},
214
+ output_result={},
215
+ duration_ms=duration_ms,
216
+ )
217
+ return result
218
+
219
+ def receive(
220
+ self,
221
+ message: Any,
222
+ sender: Any,
223
+ request_reply: bool | None = None,
224
+ silent: bool | None = False,
225
+ ) -> None:
226
+ """Receive a message; persist to memory first, then log the action."""
227
+ content = _extract_content(message)
228
+ sender_name = _agent_name(sender)
229
+
230
+ # Save the incoming message before processing so it's preserved even
231
+ # if processing later raises.
232
+ self._cl_save(
233
+ role="user",
234
+ content=content,
235
+ metadata={"direction": "receive", "from": sender_name},
236
+ )
237
+
238
+ start = time.monotonic()
239
+ result = super().receive(
240
+ message, sender,
241
+ request_reply=request_reply,
242
+ silent=silent,
243
+ )
244
+ duration_ms = max(0, int((time.monotonic() - start) * 1000))
245
+
246
+ self._cl_log(
247
+ tool_name="autogen.receive",
248
+ input_params={"message": content[:500], "from": sender_name},
249
+ output_result={},
250
+ duration_ms=duration_ms,
251
+ )
252
+ return result
253
+
254
+ # ------------------------------------------------------------------
255
+ # Internal helpers
256
+ # ------------------------------------------------------------------
257
+
258
+ def _cl_save(self, role: str, content: str, metadata: dict | None = None) -> None:
259
+ """Append a message to CrewLayer short-term memory — never raises."""
260
+ try:
261
+ self._cl_client.memory.append(
262
+ self._cl_agent_id,
263
+ role,
264
+ content,
265
+ session_id=self._cl_session,
266
+ metadata=metadata or {},
267
+ )
268
+ except Exception:
269
+ pass
270
+
271
+ def _cl_log(
272
+ self,
273
+ tool_name: str,
274
+ input_params: dict,
275
+ output_result: dict,
276
+ duration_ms: int | None = None,
277
+ ) -> None:
278
+ """Log an action to CrewLayer — never raises."""
279
+ try:
280
+ self._cl_client.actions.log(
281
+ self._cl_agent_id,
282
+ tool_name=tool_name,
283
+ input_params=input_params,
284
+ output_result=output_result,
285
+ status="success",
286
+ session_id=self._cl_session,
287
+ duration_ms=duration_ms,
288
+ )
289
+ except Exception:
290
+ pass
291
+
292
+
293
+ # ---------------------------------------------------------------------------
294
+ # CrewLayerGroupChatManager
295
+ # ---------------------------------------------------------------------------
296
+
297
+
298
+ class CrewLayerGroupChatManager(_AGGroupChatManager): # type: ignore[misc]
299
+ """AutoGen ``GroupChatManager`` that syncs every turn to the CrewLayer blackboard.
300
+
301
+ After each message in the group chat, two blackboard entries are written:
302
+
303
+ * ``latest_turn`` — the most recent speaker, content, and turn counter
304
+ * ``agent:{name}`` — each speaker's most recent message
305
+
306
+ This gives every agent (and external observers) a shared, persistent view
307
+ of the conversation state at ``context.read(group_id, "latest_turn")``.
308
+
309
+ Args:
310
+ client: A ``CrewLayerClient`` (sync) instance.
311
+ group_id: Blackboard namespace (default: ``"groupchat:{manager_name}"``).
312
+ groupchat: The ``autogen.GroupChat`` instance (forwarded to AutoGen).
313
+ name: Manager agent name (default ``"group_chat_manager"``).
314
+ **kwargs: All other kwargs forwarded to ``GroupChatManager.__init__``.
315
+
316
+ Example::
317
+
318
+ groupchat = autogen.GroupChat(agents=[a, b], messages=[], max_round=10)
319
+ manager = CrewLayerGroupChatManager(
320
+ client=client,
321
+ group_id="project-alpha",
322
+ groupchat=groupchat,
323
+ )
324
+ # After each turn: client.context.read("project-alpha", "latest_turn")
325
+ """
326
+
327
+ def __init__(
328
+ self,
329
+ *,
330
+ client: Any,
331
+ group_id: str | None = None,
332
+ groupchat: Any = None,
333
+ name: str = "group_chat_manager",
334
+ **kwargs: Any,
335
+ ) -> None:
336
+ if groupchat is not None:
337
+ super().__init__(groupchat=groupchat, name=name, **kwargs)
338
+ else:
339
+ super().__init__(name=name, **kwargs)
340
+
341
+ effective_group_id = group_id or f"groupchat:{name}"
342
+ self.__dict__.update({
343
+ "_cl_client": client,
344
+ "_cl_group_id": effective_group_id,
345
+ "_cl_turn": 0,
346
+ })
347
+
348
+ # ------------------------------------------------------------------
349
+ # AutoGen GroupChatManager override
350
+ # ------------------------------------------------------------------
351
+
352
+ def receive(
353
+ self,
354
+ message: Any,
355
+ sender: Any,
356
+ request_reply: bool | None = None,
357
+ silent: bool | None = False,
358
+ ) -> None:
359
+ """Process a group chat message and sync state to the blackboard."""
360
+ result = super().receive(
361
+ message, sender,
362
+ request_reply=request_reply,
363
+ silent=silent,
364
+ )
365
+ try:
366
+ self._cl_write_blackboard(message, sender)
367
+ except Exception:
368
+ pass # Never block group chat coordination
369
+ return result
370
+
371
+ # ------------------------------------------------------------------
372
+ # Blackboard helpers
373
+ # ------------------------------------------------------------------
374
+
375
+ def _cl_write_blackboard(self, message: Any, sender: Any) -> None:
376
+ """Write sender's message to the shared blackboard namespace."""
377
+ self._cl_turn += 1
378
+ content = _extract_content(message)
379
+ sender_name = _agent_name(sender)
380
+
381
+ self._cl_client.context.write(
382
+ self._cl_group_id,
383
+ "latest_turn",
384
+ {
385
+ "agent": sender_name,
386
+ "content": content[:1000],
387
+ "turn": self._cl_turn,
388
+ },
389
+ written_by=sender_name,
390
+ )
391
+ self._cl_client.context.write(
392
+ self._cl_group_id,
393
+ f"agent:{sender_name}",
394
+ {
395
+ "last_message": content[:1000],
396
+ "turn": self._cl_turn,
397
+ },
398
+ written_by=sender_name,
399
+ )
400
+
401
+ def get_shared_context(self) -> Any:
402
+ """Return all blackboard entries for this group's namespace.
403
+
404
+ Returns a ``ContextNamespace`` with a list of ``ContextEntry`` objects,
405
+ one per agent and the ``latest_turn`` entry.
406
+
407
+ Example::
408
+
409
+ ctx = manager.get_shared_context()
410
+ for entry in ctx.entries:
411
+ print(f"{entry.key}: {entry.value}")
412
+ """
413
+ return self._cl_client.context.list_namespace(self._cl_group_id)
414
+
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # CrewLayerAgentMemory
418
+ # ---------------------------------------------------------------------------
419
+
420
+
421
+ class CrewLayerAgentMemory:
422
+ """Loads an agent's long-term memories as initial system context.
423
+
424
+ Call ``apply(agent)`` before starting a conversation to pre-load relevant
425
+ memories into the agent's system message. Useful for giving agents
426
+ continuity across sessions without manually managing context.
427
+
428
+ Args:
429
+ client: A ``CrewLayerClient`` (sync) instance.
430
+ agent_id: Target agent UUID.
431
+ query: Semantic query used to select relevant memories
432
+ (default: ``"agent background context and history"``).
433
+ limit: Maximum number of memories to load (default ``5``).
434
+
435
+ Example::
436
+
437
+ mem = CrewLayerAgentMemory(client=client, agent_id="agent-uuid")
438
+ mem.apply(agent) # enriches agent.system_message
439
+ agent.initiate_chat(other, message="Hello")
440
+ """
441
+
442
+ def __init__(
443
+ self,
444
+ *,
445
+ client: Any,
446
+ agent_id: str,
447
+ query: str = "agent background context and history",
448
+ limit: int = 5,
449
+ ) -> None:
450
+ self._client = client
451
+ self._agent_id = agent_id
452
+ self._query = query
453
+ self._limit = limit
454
+
455
+ def apply(self, agent: Any) -> None:
456
+ """Prepend relevant long-term memories to *agent*'s system message.
457
+
458
+ If no memories are found the agent is left unchanged.
459
+ Respects AutoGen's ``update_system_message()`` API; falls back to
460
+ direct attribute assignment for custom or stub agents.
461
+ """
462
+ results = self._client.memory.recall(
463
+ self._agent_id, self._query, limit=self._limit
464
+ )
465
+ if not results.results:
466
+ return
467
+
468
+ memory_lines = "\n".join(f"- {item.content}" for item in results.results)
469
+ memory_block = (
470
+ "Relevant memories from previous sessions:\n" + memory_lines
471
+ )
472
+
473
+ current = getattr(agent, "system_message", "") or ""
474
+ new_message = f"{memory_block}\n\n{current}" if current else memory_block
475
+
476
+ if callable(getattr(agent, "update_system_message", None)):
477
+ agent.update_system_message(new_message)
478
+ else:
479
+ try:
480
+ agent.system_message = new_message
481
+ except (AttributeError, TypeError):
482
+ pass
483
+
484
+
485
+ # ---------------------------------------------------------------------------
486
+ # sync_agent_status
487
+ # ---------------------------------------------------------------------------
488
+
489
+
490
+ def sync_agent_status(
491
+ client: Any,
492
+ agent_id: str,
493
+ autogen_status: str,
494
+ *,
495
+ session_id: str | None = None,
496
+ ) -> None:
497
+ """Synchronise an AutoGen agent state with CrewLayer's agent status.
498
+
499
+ AutoGen status strings are mapped to CrewLayer's status enum:
500
+
501
+ +--------------+------------------+
502
+ | AutoGen | CrewLayer |
503
+ +==============+==================+
504
+ | thinking | working |
505
+ | replying | working |
506
+ | generating | working |
507
+ | processing | working |
508
+ | waiting | idle |
509
+ | idle | idle |
510
+ | error | error |
511
+ +--------------+------------------+
512
+
513
+ Unknown strings default to ``idle``.
514
+
515
+ The call is best-effort — it silently swallows all exceptions so it never
516
+ blocks the agent's execution.
517
+
518
+ Args:
519
+ client: A ``CrewLayerClient`` (sync) instance.
520
+ agent_id: Target agent UUID.
521
+ autogen_status: AutoGen status string (case-insensitive).
522
+ session_id: Optional current session to associate with the status.
523
+
524
+ Example::
525
+
526
+ sync_agent_status(client, agent_id, "thinking")
527
+ response = agent.generate_reply(messages)
528
+ sync_agent_status(client, agent_id, "idle")
529
+ """
530
+ crewlayer_status = _STATUS_MAP.get(autogen_status.lower(), "idle")
531
+ try:
532
+ client._http.request(
533
+ "PATCH",
534
+ f"/v1/agents/{agent_id}/status",
535
+ json={"status": crewlayer_status, "session_id": session_id},
536
+ )
537
+ except Exception:
538
+ pass