clap-agents 0.1.1__py3-none-any.whl → 0.2.2__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.
- clap/__init__.py +13 -42
- clap/embedding/__init__.py +21 -0
- clap/embedding/base_embedding.py +28 -0
- clap/embedding/fastembed_embedding.py +75 -0
- clap/embedding/ollama_embedding.py +76 -0
- clap/embedding/sentence_transformer_embedding.py +44 -0
- clap/llm_services/__init__.py +15 -0
- clap/llm_services/base.py +3 -6
- clap/llm_services/google_openai_compat_service.py +1 -5
- clap/llm_services/groq_service.py +5 -13
- clap/llm_services/ollama_service.py +101 -0
- clap/mcp_client/client.py +13 -25
- clap/multiagent_pattern/agent.py +107 -34
- clap/multiagent_pattern/team.py +54 -29
- clap/react_pattern/react_agent.py +339 -126
- clap/tool_pattern/tool.py +94 -165
- clap/tool_pattern/tool_agent.py +171 -171
- clap/tools/__init__.py +1 -1
- clap/tools/email_tools.py +16 -19
- clap/tools/web_crawler.py +26 -18
- clap/utils/completions.py +35 -37
- clap/utils/extraction.py +3 -3
- clap/utils/rag_utils.py +183 -0
- clap/vector_stores/__init__.py +16 -0
- clap/vector_stores/base.py +85 -0
- clap/vector_stores/chroma_store.py +142 -0
- clap/vector_stores/qdrant_store.py +155 -0
- {clap_agents-0.1.1.dist-info → clap_agents-0.2.2.dist-info}/METADATA +201 -23
- clap_agents-0.2.2.dist-info/RECORD +38 -0
- clap_agents-0.1.1.dist-info/RECORD +0 -27
- {clap_agents-0.1.1.dist-info → clap_agents-0.2.2.dist-info}/WHEEL +0 -0
- {clap_agents-0.1.1.dist-info → clap_agents-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
import json
|
2
|
+
import functools
|
3
|
+
from typing import Any, Dict, List, Optional, cast, Callable, Coroutine
|
4
|
+
import asyncio
|
5
|
+
import anyio
|
6
|
+
|
7
|
+
_CHROMADB_LIB_AVAILABLE = False
|
8
|
+
_ChromaDB_Client_Placeholder_Type = Any
|
9
|
+
_ChromaDB_Collection_Placeholder_Type = Any
|
10
|
+
_ChromaDB_Settings_Placeholder_Type = Any
|
11
|
+
_ChromaDB_EmbeddingFunction_Placeholder_Type = Any
|
12
|
+
_ChromaDB_DefaultEmbeddingFunction_Placeholder_Type = Any
|
13
|
+
|
14
|
+
try:
|
15
|
+
import chromadb
|
16
|
+
from chromadb import Collection as ImportedChromaCollection
|
17
|
+
from chromadb.config import Settings as ImportedChromaSettings
|
18
|
+
from chromadb.utils.embedding_functions import (
|
19
|
+
EmbeddingFunction as ImportedChromaEF,
|
20
|
+
DefaultEmbeddingFunction as ImportedChromaDefaultEF
|
21
|
+
)
|
22
|
+
from chromadb.api.types import Documents, Embeddings
|
23
|
+
_CHROMADB_LIB_AVAILABLE = True
|
24
|
+
_ChromaDB_Client_Placeholder_Type = chromadb.Client
|
25
|
+
_ChromaDB_Collection_Placeholder_Type = ImportedChromaCollection
|
26
|
+
_ChromaDB_Settings_Placeholder_Type = ImportedChromaSettings
|
27
|
+
_ChromaDB_EmbeddingFunction_Placeholder_Type = ImportedChromaEF
|
28
|
+
_ChromaDB_DefaultEmbeddingFunction_Placeholder_Type = ImportedChromaDefaultEF
|
29
|
+
except ImportError:
|
30
|
+
class Documents: pass
|
31
|
+
class Embeddings: pass
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
from .base import ( Document as ClapDocument, Embedding as ClapEmbedding, ID, Metadata, QueryResult, VectorStoreInterface,) # Aliased Document
|
36
|
+
from clap.embedding.base_embedding import EmbeddingFunctionInterface as ClapEFInterface
|
37
|
+
|
38
|
+
|
39
|
+
class _AsyncEFWrapperForChroma:
|
40
|
+
"""Wraps an async CLAP EmbeddingFunctionInterface to be callable synchronously by Chroma."""
|
41
|
+
def __init__(self, async_ef_call: Callable[..., Coroutine[Any, Any, Embeddings]], loop: asyncio.AbstractEventLoop):
|
42
|
+
self._async_ef_call = async_ef_call
|
43
|
+
self._loop = loop
|
44
|
+
|
45
|
+
def __call__(self, input: Documents) -> Embeddings:
|
46
|
+
|
47
|
+
if self._loop.is_running():
|
48
|
+
|
49
|
+
future = asyncio.run_coroutine_threadsafe(self._async_ef_call(input), self._loop)
|
50
|
+
return future.result()
|
51
|
+
else:
|
52
|
+
return self._loop.run_until_complete(self._async_ef_call(input))
|
53
|
+
|
54
|
+
|
55
|
+
class ChromaStore(VectorStoreInterface):
|
56
|
+
_client: _ChromaDB_Client_Placeholder_Type
|
57
|
+
_collection: _ChromaDB_Collection_Placeholder_Type
|
58
|
+
_clap_ef_is_async: bool = False
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self,
|
62
|
+
collection_name: str,
|
63
|
+
embedding_function: Optional[ClapEFInterface] = None, # CLAP's interface
|
64
|
+
path: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None,
|
65
|
+
client_settings: Optional[_ChromaDB_Settings_Placeholder_Type] = None,
|
66
|
+
):
|
67
|
+
if not _CHROMADB_LIB_AVAILABLE:
|
68
|
+
raise ImportError("The 'chromadb' library is required to use ChromaStore.")
|
69
|
+
|
70
|
+
self.collection_name = collection_name
|
71
|
+
|
72
|
+
if path: self._client = chromadb.PersistentClient(path=path, settings=client_settings)
|
73
|
+
elif host and port: self._client = chromadb.HttpClient(host=host, port=port, settings=client_settings)
|
74
|
+
else: self._client = chromadb.EphemeralClient(settings=client_settings)
|
75
|
+
|
76
|
+
|
77
|
+
chroma_ef_to_pass_to_chroma: Optional[_ChromaDB_EmbeddingFunction_Placeholder_Type]
|
78
|
+
|
79
|
+
if embedding_function is None:
|
80
|
+
chroma_ef_to_pass_to_chroma = _ChromaDB_DefaultEmbeddingFunction_Placeholder_Type()
|
81
|
+
self._clap_ef_is_async = False
|
82
|
+
print(f"ChromaStore: Using ChromaDB's DefaultEmbeddingFunction for '{self.collection_name}'.")
|
83
|
+
|
84
|
+
elif isinstance(embedding_function, _ChromaDB_DefaultEmbeddingFunction_Placeholder_Type):
|
85
|
+
chroma_ef_to_pass_to_chroma = embedding_function
|
86
|
+
self._clap_ef_is_async = False
|
87
|
+
print(f"ChromaStore: Using provided native Chroma EmbeddingFunction for '{self.collection_name}'.")
|
88
|
+
else:
|
89
|
+
|
90
|
+
ef_call_method = getattr(embedding_function, "__call__", None)
|
91
|
+
if ef_call_method and asyncio.iscoroutinefunction(ef_call_method):
|
92
|
+
self._clap_ef_is_async = True
|
93
|
+
print(f"ChromaStore: Wrapping async CLAP EmbeddingFunction for Chroma compatibility for '{self.collection_name}'.")
|
94
|
+
|
95
|
+
try:
|
96
|
+
loop = asyncio.get_running_loop()
|
97
|
+
except RuntimeError:
|
98
|
+
|
99
|
+
print("ChromaStore WARNING: No running asyncio loop to wrap async EF for Chroma. This might fail.")
|
100
|
+
|
101
|
+
loop = asyncio.new_event_loop()
|
102
|
+
|
103
|
+
|
104
|
+
chroma_ef_to_pass_to_chroma = _AsyncEFWrapperForChroma(ef_call_method, loop)
|
105
|
+
else:
|
106
|
+
self._clap_ef_is_async = False
|
107
|
+
print(f"ChromaStore: Using synchronous CLAP EmbeddingFunction for '{self.collection_name}'.")
|
108
|
+
chroma_ef_to_pass_to_chroma = embedding_function # type: ignore
|
109
|
+
|
110
|
+
self._collection = self._client.get_or_create_collection(
|
111
|
+
name=self.collection_name,
|
112
|
+
embedding_function=chroma_ef_to_pass_to_chroma
|
113
|
+
)
|
114
|
+
# print(f"ChromaStore: Collection '{self.collection_name}' ready.")
|
115
|
+
|
116
|
+
|
117
|
+
async def _run_sync(self, func, *args, **kwargs):
|
118
|
+
bound_func = functools.partial(func, *args, **kwargs)
|
119
|
+
return await anyio.to_thread.run_sync(bound_func)
|
120
|
+
|
121
|
+
async def add_documents(self, documents: List[ClapDocument], ids: List[ID], metadatas: Optional[List[Metadata]] = None, embeddings: Optional[List[ClapEmbedding]] = None) -> None:
|
122
|
+
|
123
|
+
# print(f"ChromaStore: Adding {len(ids)} documents to '{self.collection_name}'...")
|
124
|
+
await self._run_sync(self._collection.add, ids=ids, embeddings=embeddings, metadatas=metadatas, documents=documents)
|
125
|
+
# print(f"ChromaStore: Add/Update completed for {len(ids)} documents.")
|
126
|
+
|
127
|
+
async def aquery(self, query_texts: Optional[List[ClapDocument]] = None, query_embeddings: Optional[List[ClapEmbedding]] = None, n_results: int = 5, where: Optional[Dict[str, Any]] = None, where_document: Optional[Dict[str, Any]] = None, include: List[str] = ["metadatas", "documents", "distances"]) -> QueryResult:
|
128
|
+
if not query_texts and not query_embeddings: raise ValueError("Requires query_texts or query_embeddings.")
|
129
|
+
if query_texts and query_embeddings: query_texts = None
|
130
|
+
|
131
|
+
|
132
|
+
# print(f"ChromaStore: Querying collection '{self.collection_name}'...")
|
133
|
+
results = await self._run_sync(self._collection.query, query_embeddings=query_embeddings, query_texts=query_texts, n_results=n_results, where=where, where_document=where_document, include=include)
|
134
|
+
|
135
|
+
num_queries = len(query_texts or query_embeddings or [])
|
136
|
+
|
137
|
+
return QueryResult(
|
138
|
+
ids=results.get("ids") or ([[]] * num_queries), embeddings=results.get("embeddings"),
|
139
|
+
documents=results.get("documents"), metadatas=results.get("metadatas"), distances=results.get("distances") )
|
140
|
+
|
141
|
+
async def adelete(self, ids: Optional[List[ID]] = None, where: Optional[Dict[str, Any]] = None, where_document: Optional[Dict[str, Any]] = None) -> None:
|
142
|
+
await self._run_sync(self._collection.delete, ids=ids, where=where, where_document=where_document)
|
@@ -0,0 +1,155 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import functools
|
4
|
+
import os
|
5
|
+
from typing import Any, Dict, List, Optional, cast, Type
|
6
|
+
|
7
|
+
import anyio
|
8
|
+
|
9
|
+
_QDRANT_LIB_AVAILABLE = False
|
10
|
+
_AsyncQdrantClient_Placeholder_Type = Any
|
11
|
+
_QdrantClient_Placeholder_Type = Any
|
12
|
+
_qdrant_models_Placeholder_Type = Any
|
13
|
+
_QdrantUnexpectedResponse_Placeholder_Type = type(Exception)
|
14
|
+
|
15
|
+
try:
|
16
|
+
from qdrant_client import AsyncQdrantClient as ImportedAsyncQC, QdrantClient as ImportedSyncQC, models as ImportedModels
|
17
|
+
from qdrant_client.http.exceptions import UnexpectedResponse as ImportedQdrantUnexpectedResponse
|
18
|
+
_QDRANT_LIB_AVAILABLE = True
|
19
|
+
_AsyncQdrantClient_Placeholder_Type = ImportedAsyncQC
|
20
|
+
_QdrantClient_Placeholder_Type = ImportedSyncQC
|
21
|
+
_qdrant_models_Placeholder_Type = ImportedModels
|
22
|
+
_QdrantUnexpectedResponse_Placeholder_Type = ImportedQdrantUnexpectedResponse
|
23
|
+
except ImportError:
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
27
|
+
from .base import ( Document, Embedding, ID, Metadata, QueryResult, VectorStoreInterface,)
|
28
|
+
from clap.embedding.base_embedding import EmbeddingFunctionInterface
|
29
|
+
|
30
|
+
|
31
|
+
class QdrantStore(VectorStoreInterface):
|
32
|
+
_async_client: _AsyncQdrantClient_Placeholder_Type
|
33
|
+
models: Any # For qdrant_client.models
|
34
|
+
|
35
|
+
def __init__(self):
|
36
|
+
if not hasattr(self, "_initialized_via_factory"):
|
37
|
+
raise RuntimeError("Use QdrantStore.create(...) async factory method.")
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
async def create(
|
41
|
+
cls: Type['QdrantStore'],
|
42
|
+
collection_name: str, embedding_function: EmbeddingFunctionInterface,
|
43
|
+
path: Optional[str] = None, distance_metric: Any = None,
|
44
|
+
recreate_collection_if_exists: bool = False, **qdrant_client_kwargs: Any
|
45
|
+
) -> 'QdrantStore':
|
46
|
+
if not _QDRANT_LIB_AVAILABLE:
|
47
|
+
raise ImportError("The 'qdrant-client' library is required. Install with 'pip install \"clap-agents[qdrant]\"'")
|
48
|
+
if not embedding_function: raise ValueError("embedding_function is required.")
|
49
|
+
|
50
|
+
instance = cls.__new__(cls)
|
51
|
+
instance._initialized_via_factory = True
|
52
|
+
instance.collection_name = collection_name
|
53
|
+
instance.models = _qdrant_models_Placeholder_Type
|
54
|
+
instance._embedding_function = embedding_function
|
55
|
+
instance.distance_metric = distance_metric if distance_metric else instance.models.Distance.COSINE
|
56
|
+
instance.vector_size = instance._embedding_function.get_embedding_dimension()
|
57
|
+
|
58
|
+
client_location_for_log = path if path else ":memory:"
|
59
|
+
# print(f"QdrantStore (Async): Initializing client for '{instance.collection_name}' at '{client_location_for_log}'.")
|
60
|
+
if path: instance._async_client = _AsyncQdrantClient_Placeholder_Type(path=path, **qdrant_client_kwargs)
|
61
|
+
else: instance._async_client = _AsyncQdrantClient_Placeholder_Type(location=":memory:", **qdrant_client_kwargs)
|
62
|
+
|
63
|
+
await instance._setup_collection_async(recreate_collection_if_exists)
|
64
|
+
return instance
|
65
|
+
|
66
|
+
async def _setup_collection_async(self, recreate_if_exists: bool):
|
67
|
+
try:
|
68
|
+
if recreate_if_exists:
|
69
|
+
await self._async_client.delete_collection(collection_name=self.collection_name)
|
70
|
+
await self._async_client.create_collection(
|
71
|
+
collection_name=self.collection_name,
|
72
|
+
vectors_config=self.models.VectorParams(size=self.vector_size, distance=self.distance_metric))
|
73
|
+
return
|
74
|
+
try:
|
75
|
+
info = await self._async_client.get_collection(collection_name=self.collection_name)
|
76
|
+
if info.config.params.size != self.vector_size or info.config.params.distance.lower() != self.distance_metric.lower(): # type: ignore
|
77
|
+
raise ValueError("Existing Qdrant collection has incompatible config.")
|
78
|
+
except (_QdrantUnexpectedResponse_Placeholder_Type, ValueError) as e: # type: ignore
|
79
|
+
if isinstance(e, _QdrantUnexpectedResponse_Placeholder_Type) and e.status_code == 404 or "not found" in str(e).lower(): # type: ignore
|
80
|
+
await self._async_client.create_collection(
|
81
|
+
collection_name=self.collection_name,
|
82
|
+
vectors_config=self.models.VectorParams(size=self.vector_size, distance=self.distance_metric))
|
83
|
+
else: raise
|
84
|
+
except Exception as e: print(f"QdrantStore: Error during collection setup: {e}"); raise
|
85
|
+
|
86
|
+
|
87
|
+
async def _embed_texts_via_interface(self, texts: List[Document]) -> List[Embedding]:
|
88
|
+
if not self._embedding_function: raise RuntimeError("EF missing.")
|
89
|
+
ef_call = self._embedding_function.__call__
|
90
|
+
if asyncio.iscoroutinefunction(ef_call): return await ef_call(texts)
|
91
|
+
return await anyio.to_thread.run_sync(functools.partial(ef_call, texts))
|
92
|
+
|
93
|
+
async def add_documents(self, documents: List[Document], ids: List[ID], metadatas: Optional[List[Metadata]] = None, embeddings: Optional[List[Embedding]] = None) -> None:
|
94
|
+
if not documents and not embeddings: raise ValueError("Requires docs or embeddings.")
|
95
|
+
num_items = len(documents) if documents else (len(embeddings) if embeddings else 0)
|
96
|
+
if num_items == 0: return
|
97
|
+
if len(ids) != num_items: raise ValueError("'ids' length mismatch.")
|
98
|
+
if metadatas and len(metadatas) != num_items: raise ValueError("'metadatas' length mismatch.")
|
99
|
+
if not embeddings and documents: embeddings = await self._embed_texts_via_interface(documents)
|
100
|
+
if not embeddings: return
|
101
|
+
points: List[Any] = [] # Use Any for models.PointStruct if models is Any
|
102
|
+
for i, item_id in enumerate(ids):
|
103
|
+
payload = metadatas[i].copy() if metadatas and i < len(metadatas) else {}
|
104
|
+
if documents and i < len(documents): payload["_clap_document_content_"] = documents[i]
|
105
|
+
points.append(self.models.PointStruct(id=str(item_id), vector=embeddings[i], payload=payload))
|
106
|
+
if points: await self._async_client.upsert(collection_name=self.collection_name, points=points, wait=True)
|
107
|
+
|
108
|
+
async def aquery(self, query_texts: Optional[List[Document]] = None, query_embeddings: Optional[List[Embedding]] = None, n_results: int = 5, where: Optional[Dict[str, Any]] = None, include: List[str] = ["metadatas", "documents", "distances"], **kwargs) -> QueryResult:
|
109
|
+
if not query_texts and not query_embeddings: raise ValueError("Requires query_texts or query_embeddings.")
|
110
|
+
if query_texts and query_embeddings: query_texts = None
|
111
|
+
q_filter = self._translate_clap_filter(where)
|
112
|
+
if query_texts: q_vectors = await self._embed_texts_via_interface(query_texts)
|
113
|
+
elif query_embeddings: q_vectors = query_embeddings
|
114
|
+
else: return QueryResult(ids=[[]], embeddings=None, documents=None, metadatas=None, distances=None)
|
115
|
+
raw_results: List[List[Any]] = []
|
116
|
+
for qv in q_vectors:
|
117
|
+
hits = await self._async_client.search(collection_name=self.collection_name, query_vector=qv, query_filter=q_filter, limit=n_results, with_payload=True, with_vectors="embeddings" in include, **kwargs)
|
118
|
+
raw_results.append(hits)
|
119
|
+
return self._format_qdrant_results(raw_results, include) # Keep using self.models
|
120
|
+
|
121
|
+
def _translate_clap_filter(self, clap_where_filter: Optional[Dict[str, Any]]) -> Optional[Any]: # Return Any
|
122
|
+
if not clap_where_filter or not _QDRANT_LIB_AVAILABLE: return None
|
123
|
+
must = []
|
124
|
+
for k, v in clap_where_filter.items():
|
125
|
+
if isinstance(v, dict) and "$eq" in v: must.append(self.models.FieldCondition(key=k, match=self.models.MatchValue(value=v["$eq"])))
|
126
|
+
elif isinstance(v, (str,int,float,bool)): must.append(self.models.FieldCondition(key=k, match=self.models.MatchValue(value=v)))
|
127
|
+
return self.models.Filter(must=must) if must else None
|
128
|
+
|
129
|
+
def _format_qdrant_results(self, raw_results: List[List[Any]], include: List[str]) -> QueryResult:
|
130
|
+
ids, embs, docs, metas, dists_final = [], [], [], [], []
|
131
|
+
for hits_list in raw_results:
|
132
|
+
current_ids, current_embs, current_docs, current_metas, current_dists = [],[],[],[],[]
|
133
|
+
for hit in hits_list:
|
134
|
+
current_ids.append(str(hit.id))
|
135
|
+
payload = hit.payload if hit.payload else {}
|
136
|
+
if "distances" in include and hit.score is not None: current_dists.append(hit.score)
|
137
|
+
if "embeddings" in include and hit.vector : current_embs.append(cast(List[float], hit.vector))
|
138
|
+
if "documents" in include : current_docs.append(payload.get("_clap_document_content_", ""))
|
139
|
+
if "metadatas" in include : current_metas.append({k:v for k,v in payload.items() if k != "_clap_document_content_"})
|
140
|
+
ids.append(current_ids); embs.append(current_embs); docs.append(current_docs); metas.append(current_metas); dists_final.append(current_dists)
|
141
|
+
return QueryResult(ids=ids, embeddings=embs if "embeddings" in include else None, documents=docs if "documents" in include else None, metadatas=metas if "metadatas" in include else None, distances=dists_final if "distances" in include else None)
|
142
|
+
|
143
|
+
|
144
|
+
async def adelete(self, ids: Optional[List[ID]] = None, where: Optional[Dict[str, Any]] = None, **kwargs ) -> None:
|
145
|
+
if not ids and not where: return
|
146
|
+
q_filter = self._translate_clap_filter(where)
|
147
|
+
selector: Any = None
|
148
|
+
if ids and q_filter: selector = self.models.FilterSelector(filter=self.models.Filter(must=[q_filter, self.models.HasIdCondition(has_id=[str(i) for i in ids])]))
|
149
|
+
elif ids: selector = self.models.PointIdsList(points=[str(i) for i in ids])
|
150
|
+
elif q_filter: selector = self.models.FilterSelector(filter=q_filter)
|
151
|
+
if selector: await self._async_client.delete(collection_name=self.collection_name, points_selector=selector, wait=True)
|
152
|
+
|
153
|
+
async def close(self):
|
154
|
+
if hasattr(self, '_async_client') and self._async_client:
|
155
|
+
await self._async_client.close(timeout=5)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: clap-agents
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: A Python framework for building cognitive agentic patterns including ReAct agents, Multi-Agent Teams, native tool calling, and MCP client integration.
|
5
5
|
Project-URL: Homepage, https://github.com/MaitreyaM/CLAP-AGENTS.git
|
6
6
|
Project-URL: Repository, https://github.com/MaitreyaM/CLAP-AGENTS.git
|
@@ -233,44 +233,93 @@ Requires-Dist: openai>=1.0.0
|
|
233
233
|
Requires-Dist: pydantic<3.0.0,>=2.7.2
|
234
234
|
Requires-Dist: requests
|
235
235
|
Provides-Extra: all
|
236
|
-
Requires-Dist:
|
236
|
+
Requires-Dist: chromadb>=0.5.0; extra == 'all'
|
237
|
+
Requires-Dist: crawl4ai; extra == 'all'
|
238
|
+
Requires-Dist: fastembed>=0.2.0; extra == 'all'
|
239
|
+
Requires-Dist: graphviz; extra == 'all'
|
240
|
+
Requires-Dist: ollama>=0.2.0; extra == 'all'
|
241
|
+
Requires-Dist: pandas; extra == 'all'
|
242
|
+
Requires-Dist: pypdf; extra == 'all'
|
243
|
+
Requires-Dist: qdrant-client[fastembed]>=1.7.0; extra == 'all'
|
244
|
+
Requires-Dist: sentence-transformers; extra == 'all'
|
245
|
+
Provides-Extra: chromadb
|
246
|
+
Requires-Dist: chromadb>=0.5.0; extra == 'chromadb'
|
247
|
+
Provides-Extra: fastembed
|
248
|
+
Requires-Dist: fastembed>=0.2.0; extra == 'fastembed'
|
249
|
+
Provides-Extra: ollama
|
250
|
+
Requires-Dist: ollama>=0.2.0; extra == 'ollama'
|
251
|
+
Provides-Extra: pandas
|
252
|
+
Requires-Dist: pandas; extra == 'pandas'
|
253
|
+
Provides-Extra: pdf
|
254
|
+
Requires-Dist: pypdf; extra == 'pdf'
|
255
|
+
Provides-Extra: qdrant
|
256
|
+
Requires-Dist: qdrant-client[fastembed]>=1.7.0; extra == 'qdrant'
|
257
|
+
Provides-Extra: rag
|
258
|
+
Requires-Dist: chromadb>=0.5.0; extra == 'rag'
|
259
|
+
Requires-Dist: fastembed>=0.2.0; extra == 'rag'
|
260
|
+
Requires-Dist: ollama>=0.2.0; extra == 'rag'
|
261
|
+
Requires-Dist: pypdf; extra == 'rag'
|
262
|
+
Requires-Dist: qdrant-client[fastembed]>=1.7.0; extra == 'rag'
|
263
|
+
Requires-Dist: sentence-transformers; extra == 'rag'
|
264
|
+
Provides-Extra: sentence-transformers
|
265
|
+
Requires-Dist: sentence-transformers; extra == 'sentence-transformers'
|
237
266
|
Provides-Extra: standard-tools
|
238
267
|
Requires-Dist: crawl4ai; extra == 'standard-tools'
|
239
268
|
Provides-Extra: viz
|
240
269
|
Requires-Dist: graphviz; extra == 'viz'
|
241
270
|
Description-Content-Type: text/markdown
|
242
271
|
|
243
|
-
|
272
|
+
<p align="center">
|
273
|
+
<img src="GITCLAP.png" alt="CLAP Logo" width="700" height="200"/>
|
274
|
+
</p>
|
244
275
|
|
245
|
-
|
276
|
+
# CLAP - Cognitive Layer Agent Package
|
277
|
+
|
278
|
+
[](https://pypi.org/project/clap-agents/)
|
246
279
|
[](https://opensource.org/licenses/Apache-2.0)
|
247
|
-
[](https://pypi.org/project/clap-agents/)
|
249
281
|
|
250
|
-
**CLAP (Cognitive Layer Agent Package)** is a Python framework providing building blocks for creating sophisticated AI agents based on modern agentic patterns. It enables developers to easily construct agents capable of reasoning, planning, and interacting with external tools and
|
282
|
+
**CLAP (Cognitive Layer Agent Package)** is a Python framework providing building blocks for creating sophisticated AI agents based on modern agentic patterns. It enables developers to easily construct agents capable of reasoning, planning, and interacting with external tools, systems, and knowledge bases.
|
251
283
|
|
252
284
|
Built with an asynchronous core (`asyncio`), CLAP offers flexibility and performance for complex agentic workflows.
|
253
285
|
|
286
|
+
<p align="center">
|
287
|
+
<img src="PIP CLAP.png" alt="CLAP Pip Install" width="700" height="200"/> <!-- Updated alt text -->
|
288
|
+
</p>
|
289
|
+
|
254
290
|
## Key Features
|
255
291
|
|
256
292
|
* **Modular Agent Patterns:**
|
257
|
-
* **ReAct Agent:** Implements the Reason-Act loop with robust thought-prompting and native tool calling.
|
293
|
+
* **ReAct Agent:** Implements the Reason-Act loop with robust thought-prompting and native tool calling. Ideal for complex reasoning and RAG.
|
294
|
+
* **Tool Agent:** A straightforward agent for single-step tool usage, including simple RAG.
|
258
295
|
* **Multi-Agent Teams:** Define teams of specialized agents with dependencies, enabling collaborative task execution (sequential or parallel).
|
259
|
-
* **Simple Tool Agent:** A straightforward agent for single-step tool usage.
|
260
296
|
* **Advanced Tool Integration:**
|
261
297
|
* **Native LLM Tool Calling:** Leverages modern LLM APIs for reliable tool execution.
|
262
298
|
* **Local Tools:** Easily define and use local Python functions (both synchronous and asynchronous) as tools using the `@tool` decorator.
|
263
299
|
* **Remote Tools (MCP):** Integrates with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers via the included `MCPClientManager`, allowing agents to discover and use tools exposed by external systems (currently supports SSE transport).
|
264
|
-
* **Robust Validation:** Uses `jsonschema` for strict validation of tool arguments
|
300
|
+
* **Robust Validation & Coercion:** Uses `jsonschema` for strict validation of tool arguments and attempts type coercion for common LLM outputs (e.g., string numbers to integers).
|
301
|
+
* **Retrieval Augmented Generation (RAG) Capabilities:**
|
302
|
+
* **`VectorStoreInterface`:** An abstraction for interacting with various vector databases.
|
303
|
+
* **Supported Vector Stores:**
|
304
|
+
* **ChromaDB:** (`ChromaStore`) For local or self-hosted vector storage.
|
305
|
+
* **Qdrant:** (`QdrantStore`) For local (in-memory or file-based) vector storage.
|
306
|
+
* **`EmbeddingFunctionInterface`:** A protocol for consistent interaction with different embedding models.
|
307
|
+
* **Supported Embedding Function Wrappers:**
|
308
|
+
* `SentenceTransformerEmbeddings`: Uses models from the `sentence-transformers` library.
|
309
|
+
* `OllamaEmbeddings`: Generates embeddings using models running locally via Ollama.
|
310
|
+
* `FastEmbedEmbeddings`: Utilizes the `fastembed` library for CPU-optimized embeddings. (Note: Performance for very large batch ingestions via the async wrapper might vary based on CPU and may be slower than SentenceTransformers for initial bulk loads.)
|
311
|
+
* **RAG-Aware Agents:** Both `Agent` (via `ReactAgent`) and `ToolAgent` can be equipped with a `vector_store` to perform `vector_query` tool calls, enabling them to retrieve context before responding.
|
312
|
+
* **Utilities:** Includes basic PDF and CSV text loaders and chunking strategies in `clap.utils.rag_utils`.
|
265
313
|
* **Pluggable LLM Backends:**
|
266
314
|
* Uses a **Strategy Pattern** (`LLMServiceInterface`) to abstract LLM interactions.
|
267
315
|
* Includes ready-to-use service implementations for:
|
268
316
|
* **Groq:** (`GroqService`)
|
269
317
|
* **Google Generative AI (Gemini):** (`GoogleOpenAICompatService` via OpenAI compatibility layer)
|
318
|
+
* **Ollama (Local LLMs):** (`OllamaOpenAICompatService` also known as `OllamaService` via OpenAI compatibility layer, allowing use of locally run models like Llama 3, Mistral, etc.)
|
270
319
|
* Easily extensible to support other LLM providers.
|
271
320
|
* **Asynchronous Core:** Built entirely on `asyncio` for efficient I/O operations and potential concurrency.
|
272
|
-
* **Structured Context Passing:** Enables clear and organized information flow between agents in a team
|
273
|
-
* **Built-in Tools:** Includes helpers for web search
|
321
|
+
* **Structured Context Passing:** Enables clear and organized information flow between agents in a team.
|
322
|
+
* **Built-in Tools:** Includes helpers for web search (`duckduckgo_search`). More available via optional dependencies.
|
274
323
|
|
275
324
|
## Installation
|
276
325
|
|
@@ -278,13 +327,40 @@ Ensure you have Python 3.10 or later installed.
|
|
278
327
|
|
279
328
|
```bash
|
280
329
|
pip install clap-agents
|
330
|
+
```
|
331
|
+
|
332
|
+
Ensure you have Python 3.10 or later installed.
|
333
|
+
|
334
|
+
```bash
|
335
|
+
pip install clap-agents
|
336
|
+
|
281
337
|
|
338
|
+
To use specific features, you might need to install optional dependencies:
|
339
|
+
# For Qdrant support (includes fastembed)
|
340
|
+
pip install "clap-agents[qdrant]"
|
341
|
+
|
342
|
+
# For ChromaDB support
|
343
|
+
pip install "clap-agents[chromadb]"
|
344
|
+
|
345
|
+
# For Ollama (LLM and/or Embeddings)
|
346
|
+
pip install "clap-agents[ollama]"
|
347
|
+
|
348
|
+
# For other tools like web crawling or visualization
|
349
|
+
pip install "clap-agents[standard_tools,viz]"
|
350
|
+
|
351
|
+
# To install all major optional dependencies
|
352
|
+
pip install "clap-agents[all]"
|
353
|
+
```
|
354
|
+
|
355
|
+
|
356
|
+
Check the pyproject.toml for the full list of [project.optional-dependencies]. You will also need to have external services like Ollama or Qdrant (if used locally) running.
|
282
357
|
Depending on the tools or LLM backends you intend to use, you might need additional dependencies listed in the pyproject.toml (e.g., groq, openai, mcp, jsonschema, requests, duckduckgo-search, graphviz). Check the [project.dependencies] and [project.optional-dependencies] sections.
|
283
358
|
|
284
359
|
|
285
|
-
Quick Start: Simple Tool calling Agent with a Local Tool
|
360
|
+
## Quick Start: Simple Tool calling Agent with a Local Tool
|
286
361
|
This example demonstrates creating a Tool calling agent using the Groq backend and a local tool
|
287
362
|
|
363
|
+
```
|
288
364
|
from dotenv import load_dotenv
|
289
365
|
from clap import ToolAgent
|
290
366
|
from clap import duckduckgo_search
|
@@ -292,25 +368,24 @@ from clap import duckduckgo_search
|
|
292
368
|
load_dotenv()
|
293
369
|
|
294
370
|
async def main():
|
295
|
-
agent = ToolAgent(tools=duckduckgo_search, model="llama-
|
371
|
+
agent = ToolAgent(tools=duckduckgo_search, model="meta-llama/llama-4-scout-17b-16e-instruct")
|
296
372
|
user_query = "Search the web for recent news about AI advancements."
|
297
373
|
response = await agent.run(user_msg=user_query)
|
298
374
|
print(f"Response:\n{response}")
|
299
375
|
|
300
376
|
asyncio.run(main())
|
377
|
+
```
|
301
378
|
|
302
379
|
|
303
|
-
|
304
|
-
Quick Start: Simple ReAct Agent with a Local Tool
|
380
|
+
## Quick Start: Simple ReAct Agent with a Local Tool
|
305
381
|
This example demonstrates creating a ReAct agent using the Groq backend and a local tool.
|
306
382
|
|
307
|
-
|
383
|
+
```
|
308
384
|
import asyncio
|
309
385
|
import os
|
310
386
|
from dotenv import load_dotenv
|
311
387
|
from clap import ReactAgent, tool, GroqService
|
312
388
|
|
313
|
-
# --- Setup ---
|
314
389
|
load_dotenv()
|
315
390
|
@tool
|
316
391
|
def get_word_length(word: str) -> int:
|
@@ -333,14 +408,117 @@ async def main():
|
|
333
408
|
print(response)
|
334
409
|
|
335
410
|
asyncio.run(main())
|
411
|
+
```
|
412
|
+
|
413
|
+
## Quick Start: Simple Tool-Calling Agent with Ollama
|
414
|
+
This example demonstrates a ToolAgent using a local Ollama model and a local tool.
|
415
|
+
Ensure Ollama is running and you have pulled the model (e.g., ollama pull llama3).
|
416
|
+
|
417
|
+
```
|
418
|
+
import asyncio
|
419
|
+
from dotenv import load_dotenv
|
420
|
+
from clap import ToolAgent, tool, OllamaService # Assuming OllamaService is your OllamaOpenAICompatService
|
421
|
+
|
422
|
+
load_dotenv()
|
423
|
+
|
424
|
+
@tool
|
425
|
+
def get_capital(country: str) -> str:
|
426
|
+
"""Returns the capital of a country."""
|
427
|
+
if country.lower() == "france": return "Paris"
|
428
|
+
return f"I don't know the capital of {country}."
|
429
|
+
|
430
|
+
async def main():
|
431
|
+
# Initialize the Ollama service
|
432
|
+
ollama_llm_service = OllamaService(default_model="llama3") # Specify your Ollama model
|
433
|
+
|
434
|
+
agent = ToolAgent(
|
435
|
+
llm_service=ollama_llm_service,
|
436
|
+
model="llama3", # Model name for this agent
|
437
|
+
tools=[get_capital]
|
438
|
+
)
|
439
|
+
user_query = "What is the capital of France?"
|
440
|
+
response = await agent.run(user_msg=user_query)
|
441
|
+
print(f"Query: {user_query}\nResponse:\n{response}")
|
336
442
|
|
443
|
+
await ollama_llm_service.close() # Important for OllamaService
|
337
444
|
|
338
|
-
|
339
|
-
|
340
|
-
MCP Integration: Check examples/minimal_react_mcp_test.py and examples/test_tool_agent_mcp.py (ensure the corresponding MCP server like examples/minimal_mcp_server.py is running).
|
341
|
-
Google GenAI: Modify the Quick Start to use GoogleOpenAICompatService instead of GroqService (ensure GOOGLE_API_KEY is set and openai library is installed).
|
342
|
-
Built-in Tools: Explore the tools provided in clap.tools (like duckduckgo_search, scrape_url, etc.).
|
445
|
+
if __name__ == "__main__":
|
446
|
+
asyncio.run(main())
|
343
447
|
|
448
|
+
```
|
449
|
+
|
450
|
+
## Quick Start: RAG Agent with Qdrant and Ollama Embeddings
|
451
|
+
This example shows an Agent performing RAG using Ollama for embeddings and Qdrant as the vector store.
|
452
|
+
Ensure Ollama is running (with nomic-embed-text and llama3 pulled) and Qdrant is running (e.g., via Docker).
|
453
|
+
```
|
454
|
+
import asyncio
|
455
|
+
import os
|
456
|
+
import shutil
|
457
|
+
from dotenv import load_dotenv
|
458
|
+
from clap import Agent, QdrantStore, OllamaEmbeddings, OllamaService
|
459
|
+
from clap.utils.rag_utils import chunk_text_by_fixed_size
|
460
|
+
from qdrant_client import models as qdrant_models # If needed for distance
|
461
|
+
|
462
|
+
load_dotenv()
|
463
|
+
|
464
|
+
OLLAMA_HOST = "http://localhost:11434"
|
465
|
+
EMBED_MODEL = "nomic-embed-text"
|
466
|
+
LLM_MODEL = "llama3"
|
467
|
+
DB_PATH = "./temp_rag_db_ollama_qdrant"
|
468
|
+
COLLECTION = "my_rag_docs"
|
469
|
+
|
470
|
+
async def main():
|
471
|
+
if os.path.exists(DB_PATH): shutil.rmtree(DB_PATH)
|
472
|
+
|
473
|
+
ollama_ef = OllamaEmbeddings(model_name=EMBED_MODEL, ollama_host=OLLAMA_HOST)
|
474
|
+
vector_store = await QdrantStore.create(
|
475
|
+
collection_name=COLLECTION,
|
476
|
+
embedding_function=ollama_ef,
|
477
|
+
path=DB_PATH, # For local file-based Qdrant
|
478
|
+
recreate_collection_if_exists=True
|
479
|
+
)
|
480
|
+
|
481
|
+
sample_texts = ["The sky is blue due to Rayleigh scattering.", "Large language models are powerful."]
|
482
|
+
chunks = [chunk for text in sample_texts for chunk in chunk_text_by_fixed_size(text, 100, 10)]
|
483
|
+
ids = [str(i) for i in range(len(chunks))] # Qdrant needs UUIDs; QdrantStore handles this
|
484
|
+
|
485
|
+
if chunks:
|
486
|
+
await vector_store.add_documents(documents=chunks, ids=ids)
|
487
|
+
print(f"Ingested {len(chunks)} chunks.")
|
488
|
+
|
489
|
+
ollama_llm_service = OllamaService(default_model=LLM_MODEL, base_url=f"{OLLAMA_HOST}/v1")
|
490
|
+
rag_agent = Agent(
|
491
|
+
name="RAGMaster",
|
492
|
+
backstory="I answer questions using provided documents.",
|
493
|
+
task_description="Why is the sky blue according to the documents?", # This becomes the User Query
|
494
|
+
llm_service=ollama_llm_service,
|
495
|
+
model=LLM_MODEL,
|
496
|
+
vector_store=vector_store
|
497
|
+
)
|
498
|
+
|
499
|
+
response = await rag_agent.run()
|
500
|
+
print(f"Query: {rag_agent.task_description}\nResponse:\n{response.get('output')}")
|
501
|
+
|
502
|
+
await vector_store.close()
|
503
|
+
await ollama_llm_service.close()
|
504
|
+
if os.path.exists(DB_PATH): shutil.rmtree(DB_PATH)
|
505
|
+
|
506
|
+
asyncio.run(main())
|
507
|
+
```
|
508
|
+
|
509
|
+
## Exploring Further
|
510
|
+
|
511
|
+
|
512
|
+
# Multi-Agent Teams: See examples/test_clap_comprehensive_suite.py and other team examples for setting up sequential or parallel agent workflows.
|
513
|
+
|
514
|
+
# MCP Integration: Check examples/test_clap_comprehensive_suite.py (ensure corresponding MCP servers from examples/simple_mcp.py etc. are running).
|
515
|
+
|
516
|
+
# Other LLM Services (Groq, Google Gemini , Ollama): Modify the Quick Starts to use GroqService or GoogleOpenAICompatService (ensure API keys are set).
|
517
|
+
|
518
|
+
# Different Vector Stores & Embedding Functions: Experiment with ChromaStore, QdrantStore, SentenceTransformerEmbeddings, FastEmbedEmbeddings, and OllamaEmbeddings as shown in the comprehensive test suite.
|
344
519
|
|
345
520
|
License
|
346
521
|
This project is licensed under the terms of the Apache License 2.0. See the LICENSE file for details.
|
522
|
+
|
523
|
+
|
524
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
clap/__init__.py,sha256=rxxESl-xpSZpM4ZIh-GvHYF74CkQdbe-dSLvhMC_2dQ,1069
|
2
|
+
clap/embedding/__init__.py,sha256=PqnqcSiA_JwEvn69g2DCQHdsffL2l4GNEKo0fAtCqbs,520
|
3
|
+
clap/embedding/base_embedding.py,sha256=0SYicQ-A-rSDqHoFK0IOrRQe0cisOl8OBnis6V43Chs,696
|
4
|
+
clap/embedding/fastembed_embedding.py,sha256=Xo2H7r45wNqN89WzI8InXtjahOXAcKDyHS0fYt_jtsc,2959
|
5
|
+
clap/embedding/ollama_embedding.py,sha256=s7IYFs4BuM114Md1cqxim5WzCwCjbEJ48wAZZOgR7KQ,3702
|
6
|
+
clap/embedding/sentence_transformer_embedding.py,sha256=0RAqGxDpjZVwerOLmVirqqnCwC07kHdfAPiy2fgOSCk,1798
|
7
|
+
clap/llm_services/__init__.py,sha256=js__xIlkbXBLd2q1trYKjATYWa4xLNgPwwJz2Flwi0s,532
|
8
|
+
clap/llm_services/base.py,sha256=BjSBVOtPMvPZJjI3HwSSI3OvVFyxd5HeOKLI9bN6P7M,2204
|
9
|
+
clap/llm_services/google_openai_compat_service.py,sha256=X5ehwQEv4DobXCBjhtuo2v365DUl-thDa9Gdc16L67k,4522
|
10
|
+
clap/llm_services/groq_service.py,sha256=HQoY3IpNHiOe0h6Gy0EMa1YsCUex0CvZ_MxApNMo8Ig,3206
|
11
|
+
clap/llm_services/ollama_service.py,sha256=LsoJhCw8TZVbfuJeHATo2uPpkXsOWOuuE78HAxTQ9RY,5326
|
12
|
+
clap/mcp_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
clap/mcp_client/client.py,sha256=skIWhwkHnRXjlHwBvKXNZD5yQDe1iRIjrikn7WwczGM,8713
|
14
|
+
clap/multiagent_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
clap/multiagent_pattern/agent.py,sha256=6Og1ZujhAIzQ_JLzuhc4oh9TwJ_mTXl2q2ca302PE8g,8345
|
16
|
+
clap/multiagent_pattern/team.py,sha256=t8Xru3fVPblw75pyuPT1wmI3jlsrZHD_GKAW5APbpFg,7966
|
17
|
+
clap/react_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
+
clap/react_pattern/react_agent.py,sha256=Ron5zz-yn5EHmnXiL5G1sDYF0-3DCiTVujZ2RJI27JQ,23478
|
19
|
+
clap/tool_pattern/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
clap/tool_pattern/tool.py,sha256=7fHkLTPafsAWr_ECXZKpQz0zzqEAxSrQ56ns5oWcBSU,5964
|
21
|
+
clap/tool_pattern/tool_agent.py,sha256=VTQv9DNU16zgINZKVcX5oDw1lPfw5Y_8bUnW6wad2vE,14439
|
22
|
+
clap/tools/__init__.py,sha256=veMtEmjaY6aj_aUXNnPT_ijI2XM1DJU-y8dLqwx_t2s,293
|
23
|
+
clap/tools/email_tools.py,sha256=18aAlbjcSaOzpf9R3H-EGeRsqL5gdzmcJJcW619xOHU,9729
|
24
|
+
clap/tools/web_crawler.py,sha256=WdFbAKhUsUVveJimmLbzQ6k1BhOdMsg87FjL628HEKM,3542
|
25
|
+
clap/tools/web_search.py,sha256=YT0I1kPrdxMUst-dpsGqqF6aqxMgj3ACwiW_jN9Pu9s,985
|
26
|
+
clap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
|
+
clap/utils/completions.py,sha256=xgHhXSJfPEOjQmeB2j6TpXEswMaDC87uCpIWXztSFBw,5613
|
28
|
+
clap/utils/extraction.py,sha256=x0wK66E37aB7UIq6ms4Juf8Ga2Zdm12kYXCzL3AiwTI,1295
|
29
|
+
clap/utils/logging.py,sha256=FfrOHXnVJYCgc3mepIMKcIcGSdSWkMpV7ccKPGFxFiM,727
|
30
|
+
clap/utils/rag_utils.py,sha256=Zy-C9l39tHrxYSluaDWcEl1g__uozu4jx5hEGC7h370,6455
|
31
|
+
clap/vector_stores/__init__.py,sha256=H3w5jLdQFbXArVgiidy4RlAalM8a6LAiMlAX0Z-2v7U,404
|
32
|
+
clap/vector_stores/base.py,sha256=nvk8J1oNG3OKFhJfxBGFyVeh9YxoDs9RkB_iOzPBm1w,2853
|
33
|
+
clap/vector_stores/chroma_store.py,sha256=GWshlx0ZhiCLAhuZOCY_NjJT69r1wM8GSUVCY4CgLgo,7243
|
34
|
+
clap/vector_stores/qdrant_store.py,sha256=-SwMTb0yaGngpQ9AddDzDIt3x8GZevlFT-0FMkWD28I,9923
|
35
|
+
clap_agents-0.2.2.dist-info/METADATA,sha256=9qa7O_D0uNxkMTb9oqR5hsjDEmet-g_RIPPgaWCvsqc,27199
|
36
|
+
clap_agents-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
37
|
+
clap_agents-0.2.2.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
38
|
+
clap_agents-0.2.2.dist-info/RECORD,,
|