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,215 @@
1
+ """CrewAI integration for the CrewLayer SDK.
2
+
3
+ Provides two adapters:
4
+
5
+ - ``AgentLayerMemoryProvider`` — CrewAI ``Storage``-compatible memory backed by CrewLayer
6
+ - ``AgentLayerTaskLogger`` — task callback that logs each task completion as a CrewLayer action
7
+
8
+ Install::
9
+
10
+ pip install crewlayer[crewai]
11
+
12
+ Usage::
13
+
14
+ from crewlayer import CrewLayerClient
15
+ from crewlayer.integrations.crewai import (
16
+ AgentLayerMemoryProvider,
17
+ AgentLayerTaskLogger,
18
+ )
19
+ from crewai import Agent, Task, Crew
20
+
21
+ client = CrewLayerClient(api_key="crwl_...")
22
+
23
+ memory_provider = AgentLayerMemoryProvider(client=client, agent_id="agent-uuid")
24
+ task_logger = AgentLayerTaskLogger(client=client, agent_id="agent-uuid")
25
+
26
+ task = Task(description="Analyze sales data", callback=task_logger)
27
+ crew = Crew(agents=[...], tasks=[task], memory=True)
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Optional CrewAI imports — graceful fallback when not installed
35
+ # ---------------------------------------------------------------------------
36
+
37
+ try:
38
+ from crewai.memory.storage.interface import Storage as _CrewAIStorage # type: ignore[import]
39
+ _CREWAI = True
40
+ except ImportError:
41
+ try:
42
+ from crewai.memory.storage.base import BaseMemoryStorage as _CrewAIStorage # type: ignore[import, no-redef]
43
+ _CREWAI = True
44
+ except ImportError:
45
+ _CREWAI = False
46
+ _CrewAIStorage = object # type: ignore[assignment, misc]
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # AgentLayerMemoryProvider
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ class AgentLayerMemoryProvider(_CrewAIStorage): # type: ignore[misc]
55
+ """CrewAI-compatible ``Storage`` provider backed by CrewLayer.
56
+
57
+ Implements the minimal CrewAI storage interface (``save`` / ``search`` /
58
+ ``reset``) so it can be passed as the ``storage`` argument to any
59
+ CrewAI memory class (``LongTermMemory``, ``ShortTermMemory``, etc.).
60
+
61
+ Args:
62
+ client: A ``CrewLayerClient`` (sync) instance.
63
+ agent_id: Target agent UUID.
64
+ session_id: Session used for short-term memory writes (default ``"default"``).
65
+ recall_limit: Default number of results returned by ``search``
66
+ when no ``limit`` override is provided (default ``5``).
67
+ min_similarity: Minimum cosine-similarity score for recall
68
+ (default ``0.35``).
69
+
70
+ Example::
71
+
72
+ from crewai.memory import LongTermMemory
73
+ from crewlayer.integrations.crewai import AgentLayerMemoryProvider
74
+
75
+ ltm = LongTermMemory(
76
+ storage=AgentLayerMemoryProvider(client=client, agent_id="abc")
77
+ )
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ *,
83
+ client: Any,
84
+ agent_id: str,
85
+ session_id: str = "default",
86
+ recall_limit: int = 5,
87
+ min_similarity: float = 0.35,
88
+ ) -> None:
89
+ super().__init__()
90
+ # Bypass potential Pydantic __setattr__ if CrewAI uses Pydantic models
91
+ self.__dict__.update({
92
+ "_client": client,
93
+ "_agent_id": agent_id,
94
+ "_session_id": session_id,
95
+ "_recall_limit": recall_limit,
96
+ "_min_similarity": min_similarity,
97
+ })
98
+
99
+ # ------------------------------------------------------------------
100
+ # CrewAI Storage interface
101
+ # ------------------------------------------------------------------
102
+
103
+ def save(
104
+ self,
105
+ value: Any,
106
+ metadata: dict | None = None,
107
+ agent: Any = None,
108
+ ) -> None:
109
+ """Persist a memory value to CrewLayer short-term memory."""
110
+ self._client.memory.append(
111
+ self._agent_id,
112
+ role="assistant",
113
+ content=str(value),
114
+ session_id=self._session_id,
115
+ metadata=metadata or {},
116
+ )
117
+
118
+ def search(
119
+ self,
120
+ query: str,
121
+ *,
122
+ limit: int | None = None,
123
+ score_threshold: float | None = None,
124
+ ) -> list[dict]:
125
+ """Return memories semantically similar to *query*.
126
+
127
+ Returns a list of dicts compatible with CrewAI's expected format::
128
+
129
+ [{"id": ..., "memory": ..., "score": ..., "metadata": {...}}]
130
+ """
131
+ result = self._client.memory.recall(
132
+ self._agent_id,
133
+ query,
134
+ limit=limit if limit is not None else self._recall_limit,
135
+ min_similarity=score_threshold if score_threshold is not None else self._min_similarity,
136
+ )
137
+ return [
138
+ {
139
+ "id": item.id,
140
+ "memory": item.content,
141
+ "score": item.similarity or 0.0,
142
+ "metadata": {
143
+ "importance": item.importance,
144
+ "tags": item.tags,
145
+ },
146
+ }
147
+ for item in result.results
148
+ ]
149
+
150
+ def reset(self) -> None:
151
+ """No-op — use a fresh ``session_id`` or rely on TTL-based eviction."""
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # AgentLayerTaskLogger
156
+ # ---------------------------------------------------------------------------
157
+
158
+
159
+ class AgentLayerTaskLogger:
160
+ """Callable CrewAI task callback that logs completions as CrewLayer actions.
161
+
162
+ Pass an instance as ``callback=`` on any CrewAI ``Task``. Each time the
163
+ task finishes, the result is recorded as an action entry (status=success,
164
+ tool_name=task description) so it appears in the agent's action history
165
+ and can trigger alert rules.
166
+
167
+ Args:
168
+ client: A ``CrewLayerClient`` (sync) instance.
169
+ agent_id: Target agent UUID.
170
+ session_id: Optional session to associate the action with.
171
+
172
+ Example::
173
+
174
+ logger = AgentLayerTaskLogger(client=client, agent_id="agent-uuid")
175
+ task = Task(description="Summarize customer feedback", callback=logger)
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ *,
181
+ client: Any,
182
+ agent_id: str,
183
+ session_id: str | None = None,
184
+ ) -> None:
185
+ self._client = client
186
+ self._agent_id = agent_id
187
+ self._session_id = session_id
188
+
189
+ def __call__(self, task_output: Any) -> Any:
190
+ """Log a completed task and return ``task_output`` unchanged."""
191
+ # Prefer task name; fall back to description; truncate if needed
192
+ tool_name = (
193
+ getattr(task_output, "name", None)
194
+ or getattr(task_output, "description", None)
195
+ or "crewai_task"
196
+ )
197
+ if isinstance(tool_name, str) and len(tool_name) > 100:
198
+ tool_name = tool_name[:100]
199
+
200
+ raw_output = getattr(task_output, "raw", None) or str(task_output)
201
+
202
+ try:
203
+ self._client.actions.log(
204
+ self._agent_id,
205
+ tool_name=tool_name,
206
+ input_params={},
207
+ output_result={"output": str(raw_output)},
208
+ status="success",
209
+ session_id=self._session_id,
210
+ )
211
+ except Exception:
212
+ # Never block task execution if logging fails
213
+ pass
214
+
215
+ return task_output
@@ -0,0 +1,404 @@
1
+ """LangChain integration for the CrewLayer SDK.
2
+
3
+ Provides three adapters:
4
+
5
+ - ``AgentLayerMemory`` — BaseChatMemory backed by CrewLayer short-term memory
6
+ - ``AgentLayerVectorStore`` — VectorStore backed by CrewLayer semantic recall
7
+ - ``AgentLayerCallbackHandler`` — logs every tool call as a CrewLayer action
8
+
9
+ Install::
10
+
11
+ pip install crewlayer[langchain]
12
+
13
+ Usage::
14
+
15
+ from crewlayer import CrewLayerClient
16
+ from crewlayer.integrations.langchain import (
17
+ AgentLayerMemory,
18
+ AgentLayerVectorStore,
19
+ AgentLayerCallbackHandler,
20
+ )
21
+
22
+ client = CrewLayerClient(api_key="crwl_...")
23
+
24
+ memory = AgentLayerMemory(client=client, agent_id="agent-uuid")
25
+ handler = AgentLayerCallbackHandler(client=client, agent_id="agent-uuid")
26
+ chain = ConversationChain(llm=llm, memory=memory, callbacks=[handler])
27
+
28
+ store = AgentLayerVectorStore(client=client, agent_id="agent-uuid")
29
+ docs = store.similarity_search("user preferences", k=5)
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import time
34
+ from typing import Any, Sequence
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Optional LangChain imports — graceful fallback when not installed
38
+ # ---------------------------------------------------------------------------
39
+
40
+ try:
41
+ from langchain_core.callbacks import BaseCallbackHandler as _LCCallbackHandler
42
+ from langchain_core.documents import Document as _LCDocument
43
+ from langchain_core.memory import BaseMemory as _LCBaseMemory
44
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
45
+ from langchain_core.vectorstores import VectorStore as _LCVectorStore
46
+
47
+ _LANGCHAIN = True
48
+ except ImportError:
49
+ _LANGCHAIN = False
50
+ _LCBaseMemory = object # type: ignore[assignment, misc]
51
+ _LCVectorStore = object # type: ignore[assignment, misc]
52
+ _LCCallbackHandler = object # type: ignore[assignment, misc]
53
+
54
+ class HumanMessage: # type: ignore[no-redef]
55
+ """Stub — requires langchain-core."""
56
+ def __init__(self, content: str) -> None:
57
+ self.content = content
58
+
59
+ class AIMessage: # type: ignore[no-redef]
60
+ """Stub — requires langchain-core."""
61
+ def __init__(self, content: str) -> None:
62
+ self.content = content
63
+
64
+ class BaseMessage: # type: ignore[no-redef]
65
+ content: str
66
+
67
+ class _LCDocument: # type: ignore[no-redef]
68
+ def __init__(self, page_content: str, metadata: dict | None = None) -> None:
69
+ self.page_content = page_content
70
+ self.metadata = metadata or {}
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # AgentLayerMemory
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ class AgentLayerMemory(_LCBaseMemory): # type: ignore[misc]
79
+ """LangChain ``BaseMemory`` implementation backed by CrewLayer short-term memory.
80
+
81
+ Messages are stored in CrewLayer's Redis-backed session store and
82
+ rehydrated on every chain call via ``load_memory_variables``.
83
+
84
+ Args:
85
+ client: A ``CrewLayerClient`` (sync) instance.
86
+ agent_id: Target agent UUID.
87
+ session_id: Session key used for short-term memory (default ``"default"``).
88
+ memory_key: The key injected into the prompt template (default ``"history"``).
89
+ return_messages: If ``True`` (default) return ``HumanMessage``/``AIMessage``
90
+ objects; otherwise return a plain string transcript.
91
+ input_key: Override which input key is treated as the human turn.
92
+ output_key: Override which output key is treated as the AI turn.
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ *,
98
+ client: Any,
99
+ agent_id: str,
100
+ session_id: str = "default",
101
+ memory_key: str = "history",
102
+ return_messages: bool = True,
103
+ input_key: str | None = None,
104
+ output_key: str | None = None,
105
+ ) -> None:
106
+ super().__init__()
107
+ # Use __dict__ directly to bypass Pydantic's __setattr__ when
108
+ # langchain-core's BaseMemory is a Pydantic model.
109
+ self.__dict__.update({
110
+ "_client": client,
111
+ "_agent_id": agent_id,
112
+ "_session_id": session_id,
113
+ "_memory_key": memory_key,
114
+ "_return_messages": return_messages,
115
+ "_input_key": input_key,
116
+ "_output_key": output_key,
117
+ })
118
+
119
+ # ------------------------------------------------------------------
120
+ # BaseMemory interface
121
+ # ------------------------------------------------------------------
122
+
123
+ @property
124
+ def memory_variables(self) -> list[str]:
125
+ return [self._memory_key]
126
+
127
+ def load_memory_variables(self, inputs: dict[str, Any]) -> dict[str, Any]:
128
+ """Load session messages from CrewLayer and return them as the memory variable."""
129
+ sm = self._client.memory.messages(
130
+ self._agent_id, session_id=self._session_id
131
+ )
132
+ if self._return_messages:
133
+ messages: list[Any] = []
134
+ for m in sm.messages:
135
+ if m.role in ("user", "human"):
136
+ messages.append(HumanMessage(content=m.content))
137
+ else:
138
+ messages.append(AIMessage(content=m.content))
139
+ return {self._memory_key: messages}
140
+
141
+ lines = [f"{m.role}: {m.content}" for m in sm.messages]
142
+ return {self._memory_key: "\n".join(lines)}
143
+
144
+ def save_context(
145
+ self, inputs: dict[str, Any], outputs: dict[str, str]
146
+ ) -> None:
147
+ """Append the latest human/AI turn to CrewLayer short-term memory."""
148
+ input_key = self._input_key or next(iter(inputs), None)
149
+ output_key = self._output_key or next(iter(outputs), None)
150
+
151
+ if input_key and input_key in inputs:
152
+ self._client.memory.append(
153
+ self._agent_id,
154
+ "user",
155
+ str(inputs[input_key]),
156
+ session_id=self._session_id,
157
+ )
158
+ if output_key and output_key in outputs:
159
+ self._client.memory.append(
160
+ self._agent_id,
161
+ "assistant",
162
+ str(outputs[output_key]),
163
+ session_id=self._session_id,
164
+ )
165
+
166
+ def clear(self) -> None:
167
+ """No-op — use a fresh ``session_id`` to start a new conversation."""
168
+
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # AgentLayerVectorStore
172
+ # ---------------------------------------------------------------------------
173
+
174
+
175
+ class AgentLayerVectorStore(_LCVectorStore): # type: ignore[misc]
176
+ """LangChain ``VectorStore`` backed by CrewLayer's pgvector semantic recall.
177
+
178
+ ``similarity_search`` maps directly to ``client.memory.recall`` — queries
179
+ are embedded and scored by cosine similarity on the server.
180
+ ``add_texts`` uses the memory-extract endpoint so that Claude processes
181
+ and embeds each text as a long-term memory.
182
+
183
+ Args:
184
+ client: A ``CrewLayerClient`` (sync) instance.
185
+ agent_id: Target agent UUID.
186
+ k: Default number of results (default ``4``).
187
+ min_similarity: Minimum cosine similarity threshold (default ``0.0``).
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ *,
193
+ client: Any,
194
+ agent_id: str,
195
+ k: int = 4,
196
+ min_similarity: float = 0.0,
197
+ ) -> None:
198
+ super().__init__()
199
+ self.__dict__.update({
200
+ "_client": client,
201
+ "_agent_id": agent_id,
202
+ "_k": k,
203
+ "_min_similarity": min_similarity,
204
+ })
205
+
206
+ # ------------------------------------------------------------------
207
+ # VectorStore interface
208
+ # ------------------------------------------------------------------
209
+
210
+ def add_texts(
211
+ self,
212
+ texts: Sequence[str],
213
+ metadatas: list[dict] | None = None,
214
+ **kwargs: Any,
215
+ ) -> list[str]:
216
+ """Persist texts to CrewLayer as long-term memories via extraction."""
217
+ ids: list[str] = []
218
+ for text in texts:
219
+ result = self._client.memory.extract(
220
+ self._agent_id,
221
+ conversation=f"Remember the following:\n{text}",
222
+ )
223
+ ids.extend(result.memory_ids)
224
+ return ids
225
+
226
+ def similarity_search(
227
+ self,
228
+ query: str,
229
+ k: int | None = None,
230
+ **kwargs: Any,
231
+ ) -> list[Any]:
232
+ """Return the top-k memories most semantically similar to *query*."""
233
+ limit = k if k is not None else self._k
234
+ results = self._client.memory.recall(
235
+ self._agent_id,
236
+ query,
237
+ limit=limit,
238
+ min_similarity=self._min_similarity,
239
+ )
240
+ return [_to_document(item) for item in results.results]
241
+
242
+ def similarity_search_with_score(
243
+ self,
244
+ query: str,
245
+ k: int | None = None,
246
+ **kwargs: Any,
247
+ ) -> list[tuple[Any, float]]:
248
+ """Return ``(Document, similarity_score)`` pairs."""
249
+ limit = k if k is not None else self._k
250
+ results = self._client.memory.recall(
251
+ self._agent_id,
252
+ query,
253
+ limit=limit,
254
+ min_similarity=self._min_similarity,
255
+ )
256
+ return [(_to_document(item), item.similarity or 0.0) for item in results.results]
257
+
258
+ @classmethod
259
+ def from_texts(
260
+ cls,
261
+ texts: list[str],
262
+ embedding: Any,
263
+ metadatas: list[dict] | None = None,
264
+ *,
265
+ client: Any,
266
+ agent_id: str,
267
+ **kwargs: Any,
268
+ ) -> "AgentLayerVectorStore":
269
+ """Factory: create a store, add *texts*, and return the instance."""
270
+ store = cls(client=client, agent_id=agent_id, **kwargs)
271
+ store.add_texts(texts, metadatas=metadatas)
272
+ return store
273
+
274
+
275
+ def _to_document(item: Any) -> Any:
276
+ return _LCDocument(
277
+ page_content=item.content,
278
+ metadata={
279
+ "memory_id": item.id,
280
+ "importance": item.importance,
281
+ "tags": item.tags,
282
+ "similarity": item.similarity,
283
+ },
284
+ )
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # AgentLayerCallbackHandler
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ class AgentLayerCallbackHandler(_LCCallbackHandler): # type: ignore[misc]
293
+ """LangChain ``BaseCallbackHandler`` that logs tool calls to CrewLayer actions.
294
+
295
+ Attach to any chain or agent executor to automatically record every tool
296
+ invocation — name, input, output, and wall-clock duration — as an
297
+ immutable action entry in CrewLayer.
298
+
299
+ Args:
300
+ client: A ``CrewLayerClient`` (sync) instance.
301
+ agent_id: Target agent UUID.
302
+ session_id: Optional session to associate actions with.
303
+
304
+ Usage::
305
+
306
+ handler = AgentLayerCallbackHandler(client=client, agent_id="agent-uuid")
307
+ agent_executor.invoke({"input": "..."}, config={"callbacks": [handler]})
308
+ """
309
+
310
+ def __init__(
311
+ self,
312
+ *,
313
+ client: Any,
314
+ agent_id: str,
315
+ session_id: str | None = None,
316
+ ) -> None:
317
+ super().__init__()
318
+ self.__dict__.update({
319
+ "_client": client,
320
+ "_agent_id": agent_id,
321
+ "_session_id": session_id,
322
+ "_start_times": {}, # run_id → monotonic start
323
+ "_tool_names": {}, # run_id → tool name
324
+ "_tool_inputs": {}, # run_id → input string
325
+ })
326
+
327
+ # ------------------------------------------------------------------
328
+ # BaseCallbackHandler interface
329
+ # ------------------------------------------------------------------
330
+
331
+ def on_tool_start(
332
+ self,
333
+ serialized: dict[str, Any],
334
+ input_str: str,
335
+ *,
336
+ run_id: Any = None,
337
+ **kwargs: Any,
338
+ ) -> None:
339
+ """Record start time and tool name keyed by run_id."""
340
+ if run_id is not None:
341
+ key = str(run_id)
342
+ self._start_times[key] = time.monotonic()
343
+ name = (serialized or {}).get("name") or kwargs.get("name") or "tool"
344
+ self._tool_names[key] = str(name)
345
+ self._tool_inputs[key] = input_str
346
+
347
+ def on_tool_end(
348
+ self,
349
+ output: Any,
350
+ *,
351
+ run_id: Any = None,
352
+ **kwargs: Any,
353
+ ) -> None:
354
+ """Log a successful tool call as a CrewLayer action."""
355
+ key = str(run_id) if run_id is not None else ""
356
+ tool_name = self._tool_names.pop(key, None) or kwargs.get("name") or "tool"
357
+ input_str = self._tool_inputs.pop(key, "")
358
+ duration_ms = self._pop_duration(run_id)
359
+
360
+ self._client.actions.log(
361
+ self._agent_id,
362
+ tool_name=tool_name,
363
+ input_params={"input": input_str},
364
+ output_result={"output": str(output)},
365
+ status="success",
366
+ session_id=self._session_id,
367
+ duration_ms=duration_ms,
368
+ )
369
+
370
+ def on_tool_error(
371
+ self,
372
+ error: Any,
373
+ *,
374
+ run_id: Any = None,
375
+ **kwargs: Any,
376
+ ) -> None:
377
+ """Log a failed tool call as a CrewLayer action with status=error."""
378
+ key = str(run_id) if run_id is not None else ""
379
+ tool_name = self._tool_names.pop(key, None) or kwargs.get("name") or "tool"
380
+ input_str = self._tool_inputs.pop(key, "")
381
+ duration_ms = self._pop_duration(run_id)
382
+
383
+ self._client.actions.log(
384
+ self._agent_id,
385
+ tool_name=tool_name,
386
+ input_params={"input": input_str},
387
+ output_result={},
388
+ status="error",
389
+ error_msg=str(error),
390
+ session_id=self._session_id,
391
+ duration_ms=duration_ms,
392
+ )
393
+
394
+ # ------------------------------------------------------------------
395
+ # Internal helpers
396
+ # ------------------------------------------------------------------
397
+
398
+ def _pop_duration(self, run_id: Any) -> int | None:
399
+ if run_id is None:
400
+ return None
401
+ start = self._start_times.pop(str(run_id), None)
402
+ if start is None:
403
+ return None
404
+ return max(0, int((time.monotonic() - start) * 1000))