traia-iatp 0.1.29__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.
Potentially problematic release.
This version of traia-iatp might be problematic. Click here for more details.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +54 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/d402_a2a_client.py +293 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/contracts/__init__.py +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +255 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +172 -0
- traia_iatp/d402/__init__.py +55 -0
- traia_iatp/d402/chains.py +102 -0
- traia_iatp/d402/client.py +150 -0
- traia_iatp/d402/clients/__init__.py +7 -0
- traia_iatp/d402/clients/base.py +218 -0
- traia_iatp/d402/clients/httpx.py +219 -0
- traia_iatp/d402/common.py +114 -0
- traia_iatp/d402/encoding.py +28 -0
- traia_iatp/d402/examples/client_example.py +197 -0
- traia_iatp/d402/examples/server_example.py +171 -0
- traia_iatp/d402/facilitator.py +453 -0
- traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
- traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
- traia_iatp/d402/fastmcp_middleware.py +147 -0
- traia_iatp/d402/mcp_middleware.py +434 -0
- traia_iatp/d402/middleware.py +193 -0
- traia_iatp/d402/models.py +116 -0
- traia_iatp/d402/networks.py +98 -0
- traia_iatp/d402/path.py +43 -0
- traia_iatp/d402/payment_introspection.py +104 -0
- traia_iatp/d402/payment_signing.py +178 -0
- traia_iatp/d402/paywall.py +119 -0
- traia_iatp/d402/starlette_middleware.py +326 -0
- traia_iatp/d402/template.py +1 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/__init__.py +18 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
- traia_iatp/mcp/mcp_agent_template.py +481 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
- traia_iatp/mcp/templates/README.md.j2 +310 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/env.example.j2 +57 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
- traia_iatp/mcp/templates/server.py.j2 +175 -0
- traia_iatp/mcp/traia_mcp_adapter.py +543 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +846 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +219 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +565 -0
- traia_iatp/server/templates/agent.py.j2 +94 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +279 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +84 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +78 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +243 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.29.dist-info/METADATA +423 -0
- traia_iatp-0.1.29.dist-info/RECORD +107 -0
- traia_iatp-0.1.29.dist-info/WHEEL +5 -0
- traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
IATP Search API
|
|
4
|
+
|
|
5
|
+
This module provides high-level search API functions for finding MCP servers and utility agents
|
|
6
|
+
in the IATP registry using text search, Atlas Search, and vector search capabilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import List, Dict, Any, Optional
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import os
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
from pymongo import MongoClient
|
|
16
|
+
from pymongo import server_api
|
|
17
|
+
|
|
18
|
+
# Import for embeddings
|
|
19
|
+
from .embeddings import get_embedding_service
|
|
20
|
+
|
|
21
|
+
# Get environment variables
|
|
22
|
+
CLUSTER_URI = "traia-iatp-cluster.yzwjvgd.mongodb.net/?retryWrites=true&w=majority&appName=Traia-IATP-Cluster"
|
|
23
|
+
DATABASE_NAME = "iatp"
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class MCPServerInfo:
|
|
30
|
+
"""Information about an MCP server from the registry."""
|
|
31
|
+
id: str
|
|
32
|
+
name: str
|
|
33
|
+
url: str
|
|
34
|
+
description: str
|
|
35
|
+
server_type: str
|
|
36
|
+
capabilities: List[str]
|
|
37
|
+
metadata: Dict[str, Any]
|
|
38
|
+
tags: List[str]
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_registry_doc(cls, doc: Dict[str, Any]) -> 'MCPServerInfo':
|
|
42
|
+
"""Create MCPServerInfo from MongoDB document."""
|
|
43
|
+
# Extract tags from metadata if present
|
|
44
|
+
metadata = doc.get('metadata', {})
|
|
45
|
+
tags = metadata.get('tags', [])
|
|
46
|
+
|
|
47
|
+
return cls(
|
|
48
|
+
id=str(doc.get('_id', '')),
|
|
49
|
+
name=doc.get('name', ''),
|
|
50
|
+
url=doc.get('url', ''),
|
|
51
|
+
description=doc.get('description', ''),
|
|
52
|
+
server_type=doc.get('server_type', 'streamable-http'),
|
|
53
|
+
capabilities=doc.get('capabilities', []),
|
|
54
|
+
metadata=metadata,
|
|
55
|
+
tags=tags
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class UtilityAgentInfo:
|
|
61
|
+
"""Information about a utility agent from the registry."""
|
|
62
|
+
agent_id: str
|
|
63
|
+
name: str
|
|
64
|
+
description: str
|
|
65
|
+
base_url: str
|
|
66
|
+
capabilities: List[str]
|
|
67
|
+
tags: List[str]
|
|
68
|
+
is_active: bool
|
|
69
|
+
metadata: Dict[str, Any]
|
|
70
|
+
skills: List[Dict[str, Any]]
|
|
71
|
+
endpoints: Optional[Dict[str, Any]] = None
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_registry_doc(cls, doc: Dict[str, Any]) -> 'UtilityAgentInfo':
|
|
75
|
+
"""Create UtilityAgentInfo from MongoDB document."""
|
|
76
|
+
return cls(
|
|
77
|
+
agent_id=doc.get('agent_id', ''),
|
|
78
|
+
name=doc.get('name', ''),
|
|
79
|
+
description=doc.get('description', ''),
|
|
80
|
+
base_url=doc.get('base_url', ''),
|
|
81
|
+
capabilities=doc.get('capabilities', []),
|
|
82
|
+
tags=doc.get('tags', []),
|
|
83
|
+
is_active=doc.get('is_active', True),
|
|
84
|
+
metadata=doc.get('metadata', {}),
|
|
85
|
+
skills=doc.get('skills', []),
|
|
86
|
+
endpoints=doc.get('endpoints') # Get endpoints from root level
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_readonly_connection_string() -> str:
|
|
91
|
+
"""Get read-only MongoDB connection string."""
|
|
92
|
+
|
|
93
|
+
# Try IAM access
|
|
94
|
+
cluster_host_name = os.environ.get('CLUSTER_HOST_NAME', None)
|
|
95
|
+
if cluster_host_name:
|
|
96
|
+
return f"mongodb+srv://{cluster_host_name}/?authMechanism=MONGODB-AWS&authSource=%24external&retryWrites=true&w=majority&appName=Traia-IATP-Cluster"
|
|
97
|
+
|
|
98
|
+
# Try X.509 certificate authentication next
|
|
99
|
+
cert_file = os.getenv("MONGODB_X509_CERT_FILE")
|
|
100
|
+
if cert_file and os.path.exists(cert_file):
|
|
101
|
+
# Extract just the cluster hostname without query parameters
|
|
102
|
+
cluster_host = CLUSTER_URI.split('?')[0]
|
|
103
|
+
return f"mongodb+srv://{cluster_host}?authSource=$external&authMechanism=MONGODB-X509&tls=true&tlsCertificateKeyFile={cert_file}"
|
|
104
|
+
|
|
105
|
+
# Fallback to username/password authentication
|
|
106
|
+
user = os.getenv("MONGODB_USER")
|
|
107
|
+
password = os.getenv("MONGODB_PASSWORD")
|
|
108
|
+
if user and password:
|
|
109
|
+
logger.info("Using username/password authentication for read-only access")
|
|
110
|
+
return f"mongodb+srv://{user}:{password}@{CLUSTER_URI}"
|
|
111
|
+
|
|
112
|
+
# Try connection string as last resort
|
|
113
|
+
conn_str = os.getenv("MONGODB_CONNECTION_STRING")
|
|
114
|
+
if conn_str:
|
|
115
|
+
return conn_str
|
|
116
|
+
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"MongoDB authentication required. Please provide either:\n"
|
|
119
|
+
"1. MONGODB_IAM_ACCESS - Cluster Host Required\n"
|
|
120
|
+
"2. MONGODB_X509_CERT_FILE - Path to X.509 certificate file\n"
|
|
121
|
+
"3. MONGODB_USER and MONGODB_PASSWORD - Username and password\n"
|
|
122
|
+
"4. MONGODB_CONNECTION_STRING - Full connection string"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_collection_names():
|
|
127
|
+
"""Get environment-specific collection names."""
|
|
128
|
+
env = os.getenv("ENV", "test").lower()
|
|
129
|
+
|
|
130
|
+
# Validate environment
|
|
131
|
+
valid_envs = ["test", "staging", "prod"]
|
|
132
|
+
if env not in valid_envs:
|
|
133
|
+
logger.warning(f"Invalid ENV '{env}', defaulting to 'test'. Valid values: {valid_envs}")
|
|
134
|
+
env = "test"
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"utility_agent": f"iatp-utility-agent-registry-{env}",
|
|
138
|
+
"mcp_server": f"iatp-mcp-server-registry-{env}"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class IATPSearchAPI:
|
|
143
|
+
"""
|
|
144
|
+
Search API for finding MCP servers and utility agents in the IATP registry.
|
|
145
|
+
Provides text search, Atlas Search, and vector search capabilities.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
# Class variable to cache readonly connections
|
|
149
|
+
_client = None
|
|
150
|
+
_db = None
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _get_connection(cls):
|
|
154
|
+
"""Get or create read-only MongoDB connection."""
|
|
155
|
+
if cls._client is None:
|
|
156
|
+
conn_str = get_readonly_connection_string()
|
|
157
|
+
cls._client = MongoClient(conn_str, server_api=server_api.ServerApi('1'))
|
|
158
|
+
cls._db = cls._client[DATABASE_NAME]
|
|
159
|
+
return cls._db
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def find_utility_agent(
|
|
163
|
+
cls,
|
|
164
|
+
name: Optional[str] = None,
|
|
165
|
+
agent_id: Optional[str] = None,
|
|
166
|
+
capability: Optional[str] = None,
|
|
167
|
+
tag: Optional[str] = None,
|
|
168
|
+
query: Optional[str] = None
|
|
169
|
+
) -> Optional[UtilityAgentInfo]:
|
|
170
|
+
"""
|
|
171
|
+
Find a utility agent in the registry.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
name: Exact name of the agent
|
|
175
|
+
agent_id: Exact agent_id of the agent
|
|
176
|
+
capability: Find agents with this capability
|
|
177
|
+
tag: Find agents with this tag
|
|
178
|
+
query: Text search query (uses Atlas Search)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
UtilityAgentInfo if found, None otherwise
|
|
182
|
+
"""
|
|
183
|
+
db = cls._get_connection()
|
|
184
|
+
collection_names = get_collection_names()
|
|
185
|
+
collection = db[collection_names["utility_agent"]]
|
|
186
|
+
|
|
187
|
+
if agent_id:
|
|
188
|
+
# Direct lookup by agent_id
|
|
189
|
+
doc = collection.find_one({"agent_id": agent_id, "is_active": True})
|
|
190
|
+
if doc:
|
|
191
|
+
return UtilityAgentInfo.from_registry_doc(doc)
|
|
192
|
+
|
|
193
|
+
if name:
|
|
194
|
+
# Direct lookup by name
|
|
195
|
+
doc = collection.find_one({"name": name, "is_active": True})
|
|
196
|
+
if doc:
|
|
197
|
+
return UtilityAgentInfo.from_registry_doc(doc)
|
|
198
|
+
|
|
199
|
+
# If query is provided, use Atlas Search
|
|
200
|
+
if query:
|
|
201
|
+
env = os.getenv("ENV", "test").lower()
|
|
202
|
+
atlas_index_name = f"utility_agent_atlas_search_{env}"
|
|
203
|
+
|
|
204
|
+
pipeline = [
|
|
205
|
+
{
|
|
206
|
+
"$search": {
|
|
207
|
+
"index": atlas_index_name,
|
|
208
|
+
"text": {
|
|
209
|
+
"query": query,
|
|
210
|
+
"path": ["name", "description", "tags", "capabilities", "skills.name", "skills.description", "skills.tags"]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{"$match": {"is_active": True}},
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
# Add additional filters if provided
|
|
218
|
+
match_filters = {}
|
|
219
|
+
if capability:
|
|
220
|
+
match_filters["capabilities"] = capability
|
|
221
|
+
if tag:
|
|
222
|
+
match_filters["tags"] = tag
|
|
223
|
+
|
|
224
|
+
if match_filters:
|
|
225
|
+
pipeline.append({"$match": match_filters})
|
|
226
|
+
|
|
227
|
+
pipeline.append({"$limit": 1})
|
|
228
|
+
|
|
229
|
+
results = list(collection.aggregate(pipeline))
|
|
230
|
+
if results:
|
|
231
|
+
return UtilityAgentInfo.from_registry_doc(results[0])
|
|
232
|
+
else:
|
|
233
|
+
# Build standard query filters
|
|
234
|
+
filters = {"is_active": True}
|
|
235
|
+
|
|
236
|
+
if capability:
|
|
237
|
+
filters["capabilities"] = capability
|
|
238
|
+
|
|
239
|
+
if tag:
|
|
240
|
+
filters["tags"] = tag
|
|
241
|
+
|
|
242
|
+
# Find first matching document
|
|
243
|
+
doc = collection.find_one(filters)
|
|
244
|
+
if doc:
|
|
245
|
+
return UtilityAgentInfo.from_registry_doc(doc)
|
|
246
|
+
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def list_utility_agents(
|
|
251
|
+
cls,
|
|
252
|
+
limit: int = 10,
|
|
253
|
+
tags: Optional[List[str]] = None,
|
|
254
|
+
capabilities: Optional[List[str]] = None,
|
|
255
|
+
active_only: bool = True
|
|
256
|
+
) -> List[UtilityAgentInfo]:
|
|
257
|
+
"""
|
|
258
|
+
List available utility agents from the registry.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
limit: Maximum number of agents to return
|
|
262
|
+
tags: Filter by tags
|
|
263
|
+
capabilities: Filter by capabilities
|
|
264
|
+
active_only: Only return active agents
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of UtilityAgentInfo objects
|
|
268
|
+
"""
|
|
269
|
+
db = cls._get_connection()
|
|
270
|
+
collection_names = get_collection_names()
|
|
271
|
+
collection = db[collection_names["utility_agent"]]
|
|
272
|
+
|
|
273
|
+
# Build query filters
|
|
274
|
+
filters = {}
|
|
275
|
+
if active_only:
|
|
276
|
+
filters["is_active"] = True
|
|
277
|
+
if tags:
|
|
278
|
+
filters["tags"] = {"$in": tags}
|
|
279
|
+
if capabilities:
|
|
280
|
+
filters["capabilities"] = {"$in": capabilities}
|
|
281
|
+
|
|
282
|
+
# Query with limit
|
|
283
|
+
cursor = collection.find(filters).limit(limit).sort("registered_at", -1)
|
|
284
|
+
|
|
285
|
+
return [UtilityAgentInfo.from_registry_doc(doc) for doc in cursor]
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
async def search_utility_agents(
|
|
289
|
+
cls,
|
|
290
|
+
query: str,
|
|
291
|
+
limit: int = 10,
|
|
292
|
+
active_only: bool = True,
|
|
293
|
+
embedding_fields: Optional[List[str]] = None
|
|
294
|
+
) -> List[UtilityAgentInfo]:
|
|
295
|
+
"""
|
|
296
|
+
Search utility agents using vector search.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
query: Search query
|
|
300
|
+
limit: Maximum number of results
|
|
301
|
+
active_only: Only return active agents
|
|
302
|
+
embedding_fields: Specific embedding fields to search on. If None, uses only search_text.
|
|
303
|
+
Options: ["description", "tags", "capabilities", "agent_card"]
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
List of UtilityAgentInfo objects
|
|
307
|
+
"""
|
|
308
|
+
# Generate query embedding
|
|
309
|
+
try:
|
|
310
|
+
embedding_service = get_embedding_service()
|
|
311
|
+
query_embedding = await embedding_service.generate_query_embedding(query)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Failed to generate query embedding: {e}")
|
|
314
|
+
# Fallback to Atlas text search
|
|
315
|
+
return await cls._fallback_atlas_search_agents(query, limit, active_only)
|
|
316
|
+
|
|
317
|
+
db = cls._get_connection()
|
|
318
|
+
collection_names = get_collection_names()
|
|
319
|
+
collection = db[collection_names["utility_agent"]]
|
|
320
|
+
|
|
321
|
+
env = os.getenv("ENV", "test").lower()
|
|
322
|
+
vector_index_name = f"utility_agent_vector_search_{env}"
|
|
323
|
+
|
|
324
|
+
# Default to search_text only, or use specified fields
|
|
325
|
+
if embedding_fields is None:
|
|
326
|
+
fields_to_search = ["embeddings.search_text"]
|
|
327
|
+
else:
|
|
328
|
+
# Map field names to embedding paths
|
|
329
|
+
field_mapping = {
|
|
330
|
+
"description": "embeddings.description",
|
|
331
|
+
"tags": "embeddings.tags",
|
|
332
|
+
"capabilities": "embeddings.capabilities",
|
|
333
|
+
"agent_card": "embeddings.agent_card"
|
|
334
|
+
}
|
|
335
|
+
fields_to_search = [field_mapping.get(f, f"embeddings.{f}") for f in embedding_fields]
|
|
336
|
+
|
|
337
|
+
# If only one field, do a simple search
|
|
338
|
+
if len(fields_to_search) == 1:
|
|
339
|
+
pipeline = [
|
|
340
|
+
{
|
|
341
|
+
"$vectorSearch": {
|
|
342
|
+
"index": vector_index_name,
|
|
343
|
+
"path": fields_to_search[0],
|
|
344
|
+
"queryVector": query_embedding,
|
|
345
|
+
"numCandidates": limit * 5,
|
|
346
|
+
"limit": limit,
|
|
347
|
+
"filter": {"is_active": True} if active_only else {}
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"$project": {
|
|
352
|
+
"_id": 0,
|
|
353
|
+
"agent_id": 1,
|
|
354
|
+
"name": 1,
|
|
355
|
+
"description": 1,
|
|
356
|
+
"base_url": 1,
|
|
357
|
+
"capabilities": 1,
|
|
358
|
+
"tags": 1,
|
|
359
|
+
"is_active": 1,
|
|
360
|
+
"metadata": 1,
|
|
361
|
+
"skills": 1,
|
|
362
|
+
"score": {"$meta": "vectorSearchScore"}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
results = list(collection.aggregate(pipeline))
|
|
368
|
+
return [UtilityAgentInfo.from_registry_doc(doc) for doc in results]
|
|
369
|
+
|
|
370
|
+
# Multiple fields - aggregate results
|
|
371
|
+
all_results = []
|
|
372
|
+
seen_ids = set()
|
|
373
|
+
|
|
374
|
+
for field in fields_to_search:
|
|
375
|
+
pipeline = [
|
|
376
|
+
{
|
|
377
|
+
"$vectorSearch": {
|
|
378
|
+
"index": vector_index_name,
|
|
379
|
+
"path": field,
|
|
380
|
+
"queryVector": query_embedding,
|
|
381
|
+
"numCandidates": limit * 5,
|
|
382
|
+
"limit": limit,
|
|
383
|
+
"filter": {"is_active": True} if active_only else {}
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"$project": {
|
|
388
|
+
"_id": 0,
|
|
389
|
+
"agent_id": 1,
|
|
390
|
+
"name": 1,
|
|
391
|
+
"description": 1,
|
|
392
|
+
"base_url": 1,
|
|
393
|
+
"capabilities": 1,
|
|
394
|
+
"tags": 1,
|
|
395
|
+
"is_active": 1,
|
|
396
|
+
"metadata": 1,
|
|
397
|
+
"skills": 1,
|
|
398
|
+
"score": {"$meta": "vectorSearchScore"}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
results = list(collection.aggregate(pipeline))
|
|
404
|
+
|
|
405
|
+
# Add results avoiding duplicates
|
|
406
|
+
for doc in results:
|
|
407
|
+
if doc.get("agent_id") not in seen_ids:
|
|
408
|
+
seen_ids.add(doc.get("agent_id"))
|
|
409
|
+
all_results.append(doc)
|
|
410
|
+
|
|
411
|
+
# Sort by score (highest first) and limit
|
|
412
|
+
all_results.sort(key=lambda x: x.get("score", 0), reverse=True)
|
|
413
|
+
all_results = all_results[:limit]
|
|
414
|
+
|
|
415
|
+
# Convert to UtilityAgentInfo objects
|
|
416
|
+
return [UtilityAgentInfo.from_registry_doc(doc) for doc in all_results]
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
async def _fallback_atlas_search_agents(
|
|
420
|
+
cls,
|
|
421
|
+
query: str,
|
|
422
|
+
limit: int,
|
|
423
|
+
active_only: bool
|
|
424
|
+
) -> List[UtilityAgentInfo]:
|
|
425
|
+
"""Fallback to Atlas text search if vector search fails."""
|
|
426
|
+
db = cls._get_connection()
|
|
427
|
+
collection_names = get_collection_names()
|
|
428
|
+
collection = db[collection_names["utility_agent"]]
|
|
429
|
+
|
|
430
|
+
env = os.getenv("ENV", "test").lower()
|
|
431
|
+
atlas_index_name = f"utility_agent_atlas_search_{env}"
|
|
432
|
+
|
|
433
|
+
pipeline = [
|
|
434
|
+
{
|
|
435
|
+
"$search": {
|
|
436
|
+
"index": atlas_index_name,
|
|
437
|
+
"text": {
|
|
438
|
+
"query": query,
|
|
439
|
+
"path": ["name", "description", "tags", "capabilities", "skills.name", "skills.description", "skills.tags"]
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
if active_only:
|
|
446
|
+
pipeline.append({"$match": {"is_active": True}})
|
|
447
|
+
|
|
448
|
+
pipeline.append({"$limit": limit})
|
|
449
|
+
|
|
450
|
+
results = list(collection.aggregate(pipeline))
|
|
451
|
+
return [UtilityAgentInfo.from_registry_doc(doc) for doc in results]
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
def find_mcp_server(
|
|
455
|
+
cls,
|
|
456
|
+
name: Optional[str] = None,
|
|
457
|
+
capability: Optional[str] = None,
|
|
458
|
+
tag: Optional[str] = None,
|
|
459
|
+
query: Optional[str] = None
|
|
460
|
+
) -> Optional[MCPServerInfo]:
|
|
461
|
+
"""
|
|
462
|
+
Find an MCP server in the registry.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
name: Exact name of the MCP server
|
|
466
|
+
capability: Find servers with this capability
|
|
467
|
+
tag: Find servers with this tag
|
|
468
|
+
query: Text search query (uses Atlas Search)
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
MCPServerInfo if found, None otherwise
|
|
472
|
+
"""
|
|
473
|
+
db = cls._get_connection()
|
|
474
|
+
collection_names = get_collection_names()
|
|
475
|
+
collection = db[collection_names["mcp_server"]]
|
|
476
|
+
|
|
477
|
+
if name:
|
|
478
|
+
# Direct lookup by name
|
|
479
|
+
doc = collection.find_one({"name": name, "is_active": True})
|
|
480
|
+
if doc:
|
|
481
|
+
return MCPServerInfo.from_registry_doc(doc)
|
|
482
|
+
|
|
483
|
+
# If query is provided, use Atlas Search
|
|
484
|
+
if query:
|
|
485
|
+
env = os.getenv("ENV", "test").lower()
|
|
486
|
+
atlas_index_name = f"mcp_server_atlas_search_{env}"
|
|
487
|
+
|
|
488
|
+
pipeline = [
|
|
489
|
+
{
|
|
490
|
+
"$search": {
|
|
491
|
+
"index": atlas_index_name,
|
|
492
|
+
"text": {
|
|
493
|
+
"query": query,
|
|
494
|
+
"path": ["name", "description", "capabilities"]
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{"$match": {"is_active": True}},
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
# Add additional filters if provided
|
|
502
|
+
match_filters = {}
|
|
503
|
+
if capability:
|
|
504
|
+
match_filters["capabilities"] = capability
|
|
505
|
+
if tag:
|
|
506
|
+
# Tags are stored in metadata
|
|
507
|
+
match_filters["metadata.tags"] = tag
|
|
508
|
+
|
|
509
|
+
if match_filters:
|
|
510
|
+
pipeline.append({"$match": match_filters})
|
|
511
|
+
|
|
512
|
+
pipeline.append({"$limit": 1})
|
|
513
|
+
|
|
514
|
+
results = list(collection.aggregate(pipeline))
|
|
515
|
+
if results:
|
|
516
|
+
return MCPServerInfo.from_registry_doc(results[0])
|
|
517
|
+
else:
|
|
518
|
+
# Build standard query filters
|
|
519
|
+
filters = {"is_active": True}
|
|
520
|
+
|
|
521
|
+
if capability:
|
|
522
|
+
filters["capabilities"] = capability
|
|
523
|
+
|
|
524
|
+
if tag:
|
|
525
|
+
# Tags are stored in metadata
|
|
526
|
+
filters["metadata.tags"] = tag
|
|
527
|
+
|
|
528
|
+
# Find first matching document
|
|
529
|
+
doc = collection.find_one(filters)
|
|
530
|
+
if doc:
|
|
531
|
+
return MCPServerInfo.from_registry_doc(doc)
|
|
532
|
+
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
@classmethod
|
|
536
|
+
def list_mcp_servers(
|
|
537
|
+
cls,
|
|
538
|
+
limit: int = 10,
|
|
539
|
+
tags: Optional[List[str]] = None,
|
|
540
|
+
capabilities: Optional[List[str]] = None
|
|
541
|
+
) -> List[MCPServerInfo]:
|
|
542
|
+
"""
|
|
543
|
+
List available MCP servers from the registry.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
limit: Maximum number of servers to return
|
|
547
|
+
tags: Filter by tags
|
|
548
|
+
capabilities: Filter by capabilities
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
List of MCPServerInfo objects
|
|
552
|
+
"""
|
|
553
|
+
db = cls._get_connection()
|
|
554
|
+
collection_names = get_collection_names()
|
|
555
|
+
collection = db[collection_names["mcp_server"]]
|
|
556
|
+
|
|
557
|
+
# Build query filters
|
|
558
|
+
filters = {"is_active": True}
|
|
559
|
+
if capabilities:
|
|
560
|
+
filters["capabilities"] = {"$in": capabilities}
|
|
561
|
+
|
|
562
|
+
# Query
|
|
563
|
+
cursor = collection.find(filters).limit(limit).sort("registered_at", -1)
|
|
564
|
+
|
|
565
|
+
# Manual filtering for tags if needed
|
|
566
|
+
servers = []
|
|
567
|
+
for doc in cursor:
|
|
568
|
+
if tags:
|
|
569
|
+
doc_tags = doc.get("metadata", {}).get("tags", [])
|
|
570
|
+
if not any(tag in doc_tags for tag in tags):
|
|
571
|
+
continue
|
|
572
|
+
servers.append(MCPServerInfo.from_registry_doc(doc))
|
|
573
|
+
|
|
574
|
+
return servers[:limit]
|
|
575
|
+
|
|
576
|
+
@classmethod
|
|
577
|
+
def get_mcp_server(cls, name: str) -> Optional[Dict[str, Any]]:
|
|
578
|
+
"""
|
|
579
|
+
Get an MCP server by name (returns raw document).
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
name: Name of the MCP server
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
MongoDB document if found, None otherwise
|
|
586
|
+
"""
|
|
587
|
+
db = cls._get_connection()
|
|
588
|
+
collection_names = get_collection_names()
|
|
589
|
+
collection = db[collection_names["mcp_server"]]
|
|
590
|
+
|
|
591
|
+
doc = collection.find_one({"name": name})
|
|
592
|
+
if doc:
|
|
593
|
+
doc["_id"] = str(doc["_id"])
|
|
594
|
+
return doc
|
|
595
|
+
|
|
596
|
+
@classmethod
|
|
597
|
+
def close_connections(cls):
|
|
598
|
+
"""Close all registry connections."""
|
|
599
|
+
if cls._client:
|
|
600
|
+
cls._client.close()
|
|
601
|
+
cls._client = None
|
|
602
|
+
cls._db = None
|
|
603
|
+
|
|
604
|
+
@classmethod
|
|
605
|
+
async def search_mcp_servers(
|
|
606
|
+
cls,
|
|
607
|
+
query: str,
|
|
608
|
+
limit: int = 10,
|
|
609
|
+
active_only: bool = True,
|
|
610
|
+
embedding_fields: Optional[List[str]] = None
|
|
611
|
+
) -> List[MCPServerInfo]:
|
|
612
|
+
"""
|
|
613
|
+
Search MCP servers using vector search.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
query: Search query
|
|
617
|
+
limit: Maximum number of results
|
|
618
|
+
active_only: Only return active servers
|
|
619
|
+
embedding_fields: Specific embedding fields to search on. If None, searches both description and capabilities.
|
|
620
|
+
Options: ["description", "capabilities"]
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
List of MCPServerInfo objects
|
|
624
|
+
"""
|
|
625
|
+
# Generate query embedding
|
|
626
|
+
try:
|
|
627
|
+
embedding_service = get_embedding_service()
|
|
628
|
+
query_embedding = await embedding_service.generate_query_embedding(query)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
logger.error(f"Failed to generate query embedding: {e}")
|
|
631
|
+
# Fallback to Atlas text search
|
|
632
|
+
return await cls._fallback_atlas_search_mcp_servers(query, limit, active_only)
|
|
633
|
+
|
|
634
|
+
db = cls._get_connection()
|
|
635
|
+
collection_names = get_collection_names()
|
|
636
|
+
collection = db[collection_names["mcp_server"]]
|
|
637
|
+
|
|
638
|
+
env = os.getenv("ENV", "test").lower()
|
|
639
|
+
vector_index_name = f"mcp_server_vector_search_{env}"
|
|
640
|
+
|
|
641
|
+
# Default to both fields, or use specified fields
|
|
642
|
+
if embedding_fields is None:
|
|
643
|
+
fields_to_search = ["description_embedding", "capabilities_embedding"]
|
|
644
|
+
else:
|
|
645
|
+
# Map field names to embedding paths
|
|
646
|
+
field_mapping = {
|
|
647
|
+
"description": "description_embedding",
|
|
648
|
+
"capabilities": "capabilities_embedding"
|
|
649
|
+
}
|
|
650
|
+
fields_to_search = [field_mapping.get(f, f"{f}_embedding") for f in embedding_fields]
|
|
651
|
+
|
|
652
|
+
# If only one field, do a simple search
|
|
653
|
+
if len(fields_to_search) == 1:
|
|
654
|
+
pipeline = [
|
|
655
|
+
{
|
|
656
|
+
"$vectorSearch": {
|
|
657
|
+
"index": vector_index_name,
|
|
658
|
+
"path": fields_to_search[0],
|
|
659
|
+
"queryVector": query_embedding,
|
|
660
|
+
"numCandidates": limit * 5,
|
|
661
|
+
"limit": limit,
|
|
662
|
+
"filter": {"is_active": True} if active_only else {}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"$project": {
|
|
667
|
+
"_id": 0,
|
|
668
|
+
"name": 1,
|
|
669
|
+
"url": 1,
|
|
670
|
+
"description": 1,
|
|
671
|
+
"server_type": 1,
|
|
672
|
+
"capabilities": 1,
|
|
673
|
+
"metadata": 1,
|
|
674
|
+
"registered_at": 1,
|
|
675
|
+
"is_active": 1,
|
|
676
|
+
"score": {"$meta": "vectorSearchScore"}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
results = list(collection.aggregate(pipeline))
|
|
682
|
+
return [MCPServerInfo.from_registry_doc(doc) for doc in results]
|
|
683
|
+
|
|
684
|
+
# Multiple fields - aggregate results
|
|
685
|
+
all_results = []
|
|
686
|
+
seen_names = set()
|
|
687
|
+
|
|
688
|
+
for field in fields_to_search:
|
|
689
|
+
pipeline = [
|
|
690
|
+
{
|
|
691
|
+
"$vectorSearch": {
|
|
692
|
+
"index": vector_index_name,
|
|
693
|
+
"path": field,
|
|
694
|
+
"queryVector": query_embedding,
|
|
695
|
+
"numCandidates": limit * 5,
|
|
696
|
+
"limit": limit,
|
|
697
|
+
"filter": {"is_active": True} if active_only else {}
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
"$project": {
|
|
702
|
+
"_id": 0,
|
|
703
|
+
"name": 1,
|
|
704
|
+
"url": 1,
|
|
705
|
+
"description": 1,
|
|
706
|
+
"server_type": 1,
|
|
707
|
+
"capabilities": 1,
|
|
708
|
+
"metadata": 1,
|
|
709
|
+
"registered_at": 1,
|
|
710
|
+
"is_active": 1,
|
|
711
|
+
"score": {"$meta": "vectorSearchScore"}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
results = list(collection.aggregate(pipeline))
|
|
717
|
+
|
|
718
|
+
# Add results avoiding duplicates
|
|
719
|
+
for doc in results:
|
|
720
|
+
if doc.get("name") not in seen_names:
|
|
721
|
+
seen_names.add(doc.get("name"))
|
|
722
|
+
all_results.append(doc)
|
|
723
|
+
|
|
724
|
+
# Sort by score (highest first) and limit
|
|
725
|
+
all_results.sort(key=lambda x: x.get("score", 0), reverse=True)
|
|
726
|
+
all_results = all_results[:limit]
|
|
727
|
+
|
|
728
|
+
# Convert to MCPServerInfo objects
|
|
729
|
+
return [MCPServerInfo.from_registry_doc(doc) for doc in all_results]
|
|
730
|
+
|
|
731
|
+
@classmethod
|
|
732
|
+
async def _fallback_atlas_search_mcp_servers(
|
|
733
|
+
cls,
|
|
734
|
+
query: str,
|
|
735
|
+
limit: int,
|
|
736
|
+
active_only: bool
|
|
737
|
+
) -> List[MCPServerInfo]:
|
|
738
|
+
"""Fallback to Atlas text search if vector search fails."""
|
|
739
|
+
db = cls._get_connection()
|
|
740
|
+
collection_names = get_collection_names()
|
|
741
|
+
collection = db[collection_names["mcp_server"]]
|
|
742
|
+
|
|
743
|
+
env = os.getenv("ENV", "test").lower()
|
|
744
|
+
atlas_index_name = f"mcp_server_atlas_search_{env}"
|
|
745
|
+
|
|
746
|
+
pipeline = [
|
|
747
|
+
{
|
|
748
|
+
"$search": {
|
|
749
|
+
"index": atlas_index_name,
|
|
750
|
+
"text": {
|
|
751
|
+
"query": query,
|
|
752
|
+
"path": ["name", "description", "capabilities"]
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
if active_only:
|
|
759
|
+
pipeline.append({"$match": {"is_active": True}})
|
|
760
|
+
|
|
761
|
+
pipeline.append({"$limit": limit})
|
|
762
|
+
|
|
763
|
+
results = list(collection.aggregate(pipeline))
|
|
764
|
+
return [MCPServerInfo.from_registry_doc(doc) for doc in results]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# Keep backward compatibility - use IATPSearchAPI
|
|
768
|
+
class RegistryAPI(IATPSearchAPI):
|
|
769
|
+
"""Backward compatibility alias for IATPSearchAPI."""
|
|
770
|
+
pass
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# Convenience functions that don't require class instantiation
|
|
774
|
+
def find_utility_agent(
|
|
775
|
+
name: Optional[str] = None,
|
|
776
|
+
agent_id: Optional[str] = None,
|
|
777
|
+
capability: Optional[str] = None,
|
|
778
|
+
tag: Optional[str] = None,
|
|
779
|
+
query: Optional[str] = None
|
|
780
|
+
) -> Optional[UtilityAgentInfo]:
|
|
781
|
+
"""Find a utility agent in the registry."""
|
|
782
|
+
return IATPSearchAPI.find_utility_agent(name, agent_id, capability, tag, query)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def list_utility_agents(
|
|
786
|
+
limit: int = 10,
|
|
787
|
+
tags: Optional[List[str]] = None,
|
|
788
|
+
capabilities: Optional[List[str]] = None,
|
|
789
|
+
active_only: bool = True
|
|
790
|
+
) -> List[UtilityAgentInfo]:
|
|
791
|
+
"""List available utility agents from the registry."""
|
|
792
|
+
return IATPSearchAPI.list_utility_agents(limit, tags, capabilities, active_only)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
async def search_utility_agents(
|
|
796
|
+
query: str,
|
|
797
|
+
limit: int = 10,
|
|
798
|
+
active_only: bool = True,
|
|
799
|
+
embedding_fields: Optional[List[str]] = None
|
|
800
|
+
) -> List[UtilityAgentInfo]:
|
|
801
|
+
"""Search utility agents using vector search."""
|
|
802
|
+
return await IATPSearchAPI.search_utility_agents(query, limit, active_only, embedding_fields)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def find_mcp_server(
|
|
806
|
+
name: Optional[str] = None,
|
|
807
|
+
capability: Optional[str] = None,
|
|
808
|
+
tag: Optional[str] = None,
|
|
809
|
+
query: Optional[str] = None
|
|
810
|
+
) -> Optional[MCPServerInfo]:
|
|
811
|
+
"""Find an MCP server in the registry."""
|
|
812
|
+
return IATPSearchAPI.find_mcp_server(name, capability, tag, query)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def list_mcp_servers(
|
|
816
|
+
limit: int = 10,
|
|
817
|
+
tags: Optional[List[str]] = None,
|
|
818
|
+
capabilities: Optional[List[str]] = None
|
|
819
|
+
) -> List[MCPServerInfo]:
|
|
820
|
+
"""List available MCP servers from the registry."""
|
|
821
|
+
return IATPSearchAPI.list_mcp_servers(limit, tags, capabilities)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
async def search_mcp_servers(
|
|
825
|
+
query: str,
|
|
826
|
+
limit: int = 10,
|
|
827
|
+
embedding_fields: Optional[List[str]] = None
|
|
828
|
+
) -> List[MCPServerInfo]:
|
|
829
|
+
"""
|
|
830
|
+
Search MCP servers using vector search.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
query: Search query
|
|
834
|
+
limit: Maximum number of results
|
|
835
|
+
embedding_fields: Specific embedding fields to search on. If None, searches both description and capabilities.
|
|
836
|
+
Options: ["description", "capabilities"]
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
List of MCPServerInfo objects
|
|
840
|
+
"""
|
|
841
|
+
return await IATPSearchAPI.search_mcp_servers(query, limit, active_only=True, embedding_fields=embedding_fields)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def get_mcp_server(name: str) -> Optional[Dict[str, Any]]:
|
|
845
|
+
"""Get an MCP server by name (returns raw document)."""
|
|
846
|
+
return IATPSearchAPI.get_mcp_server(name)
|