PyStreamMCP 3.2.0__py3-none-any.whl → 3.3.1__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.
- pystreammcp/__init__.py +4 -2
- pystreammcp/api.py +47 -4
- pystreammcp/cli.py +11 -160
- pystreammcp/discovery.py +139 -5
- pystreammcp/integrations/langchain.py +20 -12
- pystreammcp/integrations/llamaindex.py +4 -1
- pystreammcp/integrations/semantic_kernel.py +4 -1
- pystreammcp/mcp_server.py +157 -110
- pystreammcp/multi_agent.py +2 -1
- pystreammcp/orchestration/airflow.py +21 -12
- pystreammcp/orchestration/nocode_rpa.py +15 -11
- pystreammcp/orchestration/temporal.py +25 -15
- pystreammcp/quality.py +6 -6
- pystreammcp/server.py +3 -3
- pystreammcp/testing.py +123 -0
- pystreammcp/webhook_handlers.py +30 -30
- pystreammcp/webhook_router.py +8 -8
- pystreammcp-3.3.1.dist-info/METADATA +404 -0
- {pystreammcp-3.2.0.dist-info → pystreammcp-3.3.1.dist-info}/RECORD +23 -21
- {pystreammcp-3.2.0.dist-info → pystreammcp-3.3.1.dist-info}/WHEEL +1 -1
- pystreammcp-3.3.1.dist-info/entry_points.txt +2 -0
- pystreammcp-3.3.1.dist-info/licenses/LICENSE +202 -0
- pystreammcp-3.2.0.dist-info/METADATA +0 -231
- pystreammcp-3.2.0.dist-info/licenses/LICENSE +0 -22
- {pystreammcp-3.2.0.dist-info → pystreammcp-3.3.1.dist-info}/top_level.txt +0 -0
pystreammcp/__init__.py
CHANGED
|
@@ -7,12 +7,12 @@ Reduces token usage by 60-75% while maintaining quality.
|
|
|
7
7
|
Includes StatGuardian integration for data quality validation.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "3.
|
|
10
|
+
__version__ = "3.3.1"
|
|
11
11
|
|
|
12
12
|
from .agent import Agent, QueryResult
|
|
13
13
|
from .query import Query, QueryIntent, QueryConstraints
|
|
14
14
|
from .context import Context, ContextType, ContextWindow
|
|
15
|
-
from .discovery import Discovery, DiscoveredSource, SourceType
|
|
15
|
+
from .discovery import Discovery, DiscoveredSource, SourceType, SourceRegistry, DataSource
|
|
16
16
|
|
|
17
17
|
# Optimization imports
|
|
18
18
|
try:
|
|
@@ -79,6 +79,8 @@ __all__ = [
|
|
|
79
79
|
"Discovery",
|
|
80
80
|
"DiscoveredSource",
|
|
81
81
|
"SourceType",
|
|
82
|
+
"SourceRegistry",
|
|
83
|
+
"DataSource",
|
|
82
84
|
# Legacy optimization (optional, phase 1-2)
|
|
83
85
|
*((["OptimizationStrategy"]) if OptimizationStrategy else []),
|
|
84
86
|
*((["StrategyType"]) if StrategyType else []),
|
pystreammcp/api.py
CHANGED
|
@@ -13,6 +13,7 @@ import logging
|
|
|
13
13
|
|
|
14
14
|
from .adapters import AdapterRegistry, AdapterConfig, FrameworkType, QueryResult
|
|
15
15
|
from .agent import Agent
|
|
16
|
+
from .discovery import SourceRegistry
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
@@ -59,6 +60,15 @@ class DiscoveryResponse(BaseModel):
|
|
|
59
60
|
total_sources: int
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
class SourceRegistration(BaseModel):
|
|
64
|
+
"""Register a data source for discovery."""
|
|
65
|
+
|
|
66
|
+
name: str = Field(..., description="Unique source name")
|
|
67
|
+
description: str = Field(..., description="What this source contains, in plain text")
|
|
68
|
+
type: str = Field("unknown", description="Source type, e.g. database, api, docs")
|
|
69
|
+
tags: List[str] = Field(default_factory=list, description="Optional keyword tags")
|
|
70
|
+
|
|
71
|
+
|
|
62
72
|
class OptimizationRequest(BaseModel):
|
|
63
73
|
"""Query optimization request."""
|
|
64
74
|
|
|
@@ -117,6 +127,7 @@ class PyStreamMCPAPI:
|
|
|
117
127
|
self.title = title
|
|
118
128
|
self.version = version
|
|
119
129
|
self.agents: Dict[str, Agent] = {}
|
|
130
|
+
self.registry = SourceRegistry()
|
|
120
131
|
self.app = self._create_app()
|
|
121
132
|
|
|
122
133
|
def _create_app(self) -> FastAPI:
|
|
@@ -260,13 +271,45 @@ class PyStreamMCPAPI:
|
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
# Discovery Endpoints
|
|
274
|
+
@app.post("/sources", response_model=Dict[str, Any])
|
|
275
|
+
async def register_source(source: SourceRegistration):
|
|
276
|
+
"""Register a data source so /discover can find it."""
|
|
277
|
+
self.registry.register(
|
|
278
|
+
name=source.name,
|
|
279
|
+
description=source.description,
|
|
280
|
+
type=source.type,
|
|
281
|
+
tags=source.tags,
|
|
282
|
+
)
|
|
283
|
+
return {"status": "success", "name": source.name}
|
|
284
|
+
|
|
285
|
+
@app.delete("/sources/{name}")
|
|
286
|
+
async def unregister_source(name: str):
|
|
287
|
+
"""Remove a registered data source."""
|
|
288
|
+
removed = self.registry.unregister(name)
|
|
289
|
+
if not removed:
|
|
290
|
+
raise HTTPException(status_code=404, detail=f"Source {name} not found")
|
|
291
|
+
return {"status": "success", "name": name}
|
|
292
|
+
|
|
293
|
+
@app.get("/sources")
|
|
294
|
+
async def list_sources():
|
|
295
|
+
"""List all registered data sources."""
|
|
296
|
+
return {
|
|
297
|
+
"status": "success",
|
|
298
|
+
"sources": [
|
|
299
|
+
{"name": s.name, "type": s.type, "tags": s.tags}
|
|
300
|
+
for s in self.registry.list_sources()
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
|
|
263
304
|
@app.post("/discover", response_model=DiscoveryResponse)
|
|
264
305
|
async def discover(request: DiscoveryRequest):
|
|
265
|
-
"""Discover relevant
|
|
266
|
-
agent = self._get_agent(request.agent_id)
|
|
306
|
+
"""Discover data sources relevant to `context`.
|
|
267
307
|
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
Ranks *registered* sources (see POST /sources) by real token
|
|
309
|
+
overlap against `context`. Returns an empty list if nothing is
|
|
310
|
+
registered or nothing overlaps — never a fabricated result.
|
|
311
|
+
"""
|
|
312
|
+
sources = self.registry.discover(request.context)
|
|
270
313
|
|
|
271
314
|
return {
|
|
272
315
|
"status": "success",
|
pystreammcp/cli.py
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
5
|
-
from typing import Optional
|
|
6
5
|
import click
|
|
7
6
|
|
|
8
7
|
from pystreammcp import Agent, __version__
|
|
9
|
-
from pystreammcp.api import PyStreamMCPAPI
|
|
10
8
|
from pystreammcp.cli_dashboard import PyStreamMCPDashboard
|
|
11
9
|
|
|
12
10
|
|
|
@@ -83,6 +81,16 @@ def query(
|
|
|
83
81
|
@click.option("--reload", is_flag=True)
|
|
84
82
|
def server(host: str, port: int, reload: bool):
|
|
85
83
|
"""Start PyStreamMCP API server."""
|
|
84
|
+
try:
|
|
85
|
+
from pystreammcp.api import PyStreamMCPAPI
|
|
86
|
+
except ImportError:
|
|
87
|
+
click.echo(
|
|
88
|
+
"Error: the 'server' command requires the 'api' extra. "
|
|
89
|
+
"Install with: pip install 'pystreammcp[api]'",
|
|
90
|
+
err=True,
|
|
91
|
+
)
|
|
92
|
+
raise SystemExit(1)
|
|
93
|
+
|
|
86
94
|
click.echo(f"Starting server on {host}:{port}...")
|
|
87
95
|
api = PyStreamMCPAPI()
|
|
88
96
|
api.run(host=host, port=port, reload=reload)
|
|
@@ -124,162 +132,5 @@ def dashboard(
|
|
|
124
132
|
sys.exit(1)
|
|
125
133
|
|
|
126
134
|
|
|
127
|
-
# Legacy CLI interface for compatibility
|
|
128
|
-
class CLIInterface:
|
|
129
|
-
"""Command-line interface for PyStreamMCP (legacy)."""
|
|
130
|
-
|
|
131
|
-
def __init__(self):
|
|
132
|
-
self.agent = None
|
|
133
|
-
|
|
134
|
-
def create_agent(
|
|
135
|
-
self, agent_id: str, strategy: str = "balanced", max_tokens: int = 2000
|
|
136
|
-
):
|
|
137
|
-
"""Create an agent."""
|
|
138
|
-
self.agent = Agent(
|
|
139
|
-
agent_id=agent_id,
|
|
140
|
-
optimization_strategy=strategy,
|
|
141
|
-
max_tokens=max_tokens,
|
|
142
|
-
)
|
|
143
|
-
return {"status": "success", "agent_id": agent_id}
|
|
144
|
-
|
|
145
|
-
def query(
|
|
146
|
-
self, text: str, intent: str = "retrieve", agent_id: Optional[str] = None
|
|
147
|
-
) -> dict:
|
|
148
|
-
"""Execute a query."""
|
|
149
|
-
if agent_id:
|
|
150
|
-
self.create_agent(agent_id)
|
|
151
|
-
|
|
152
|
-
if not self.agent:
|
|
153
|
-
return {"status": "error", "message": "No agent created"}
|
|
154
|
-
|
|
155
|
-
result = self.agent.query(text)
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
"status": "success",
|
|
159
|
-
"query_id": result.query_id,
|
|
160
|
-
"query_text": text,
|
|
161
|
-
"baseline_tokens": result.baseline_tokens,
|
|
162
|
-
"optimized_tokens": result.optimized_tokens,
|
|
163
|
-
"cost_reduction_percent": result.cost_reduction_percent,
|
|
164
|
-
"cost_saved": (result.baseline_tokens - result.optimized_tokens) * 0.00001,
|
|
165
|
-
"execution_time_ms": result.execution_time_ms,
|
|
166
|
-
"meets_target": 60 <= result.cost_reduction_percent <= 75,
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
def get_metrics(self) -> dict:
|
|
170
|
-
"""Get agent metrics."""
|
|
171
|
-
if not self.agent:
|
|
172
|
-
return {"status": "error", "message": "No agent created"}
|
|
173
|
-
|
|
174
|
-
metrics = self.agent.get_metrics()
|
|
175
|
-
return {
|
|
176
|
-
"status": "success",
|
|
177
|
-
"metrics": metrics,
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def main():
|
|
182
|
-
"""Main CLI entry point."""
|
|
183
|
-
cli = CLIInterface()
|
|
184
|
-
|
|
185
|
-
if len(sys.argv) < 2:
|
|
186
|
-
print_help()
|
|
187
|
-
sys.exit(1)
|
|
188
|
-
|
|
189
|
-
command = sys.argv[1]
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
if command == "query":
|
|
193
|
-
if len(sys.argv) < 3:
|
|
194
|
-
print(json.dumps({"error": "Missing query text"}))
|
|
195
|
-
sys.exit(1)
|
|
196
|
-
|
|
197
|
-
text = sys.argv[2]
|
|
198
|
-
intent = sys.argv[3] if len(sys.argv) > 3 else "retrieve"
|
|
199
|
-
agent_id = sys.argv[4] if len(sys.argv) > 4 else "cli_agent"
|
|
200
|
-
|
|
201
|
-
result = cli.query(text, intent=intent, agent_id=agent_id)
|
|
202
|
-
print(json.dumps(result))
|
|
203
|
-
|
|
204
|
-
elif command == "create-agent":
|
|
205
|
-
if len(sys.argv) < 3:
|
|
206
|
-
print(json.dumps({"error": "Missing agent_id"}))
|
|
207
|
-
sys.exit(1)
|
|
208
|
-
|
|
209
|
-
agent_id = sys.argv[2]
|
|
210
|
-
strategy = sys.argv[3] if len(sys.argv) > 3 else "balanced"
|
|
211
|
-
max_tokens = int(sys.argv[4]) if len(sys.argv) > 4 else 2000
|
|
212
|
-
|
|
213
|
-
result = cli.create_agent(agent_id, strategy, max_tokens)
|
|
214
|
-
print(json.dumps(result))
|
|
215
|
-
|
|
216
|
-
elif command == "metrics":
|
|
217
|
-
result = cli.get_metrics()
|
|
218
|
-
print(json.dumps(result))
|
|
219
|
-
|
|
220
|
-
elif command == "help":
|
|
221
|
-
print_help()
|
|
222
|
-
|
|
223
|
-
else:
|
|
224
|
-
print(json.dumps({"error": f"Unknown command: {command}"}))
|
|
225
|
-
sys.exit(1)
|
|
226
|
-
|
|
227
|
-
except Exception as e:
|
|
228
|
-
print(json.dumps({"error": str(e), "status": "error"}))
|
|
229
|
-
sys.exit(1)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def print_help():
|
|
233
|
-
"""Print help message."""
|
|
234
|
-
help_text = """
|
|
235
|
-
PyStreamMCP CLI - Intelligence Layer for Workflows
|
|
236
|
-
|
|
237
|
-
USAGE:
|
|
238
|
-
pystreammcp <command> [options]
|
|
239
|
-
|
|
240
|
-
COMMANDS:
|
|
241
|
-
query <text> [intent] [agent_id]
|
|
242
|
-
Execute a query with optimization
|
|
243
|
-
- text: The query text (required)
|
|
244
|
-
- intent: Query type (retrieve|discover|aggregate) (default: retrieve)
|
|
245
|
-
- agent_id: Agent identifier (default: cli_agent)
|
|
246
|
-
|
|
247
|
-
Example:
|
|
248
|
-
pystreammcp query "Top 10 customers by LTV" retrieve my_agent
|
|
249
|
-
|
|
250
|
-
create-agent <agent_id> [strategy] [max_tokens]
|
|
251
|
-
Create an agent with specific configuration
|
|
252
|
-
- agent_id: Agent identifier (required)
|
|
253
|
-
- strategy: Optimization strategy (balanced|token_efficient|quality_first) (default: balanced)
|
|
254
|
-
- max_tokens: Token budget (default: 2000)
|
|
255
|
-
|
|
256
|
-
Example:
|
|
257
|
-
pystreammcp create-agent workflow_agent token_efficient 1500
|
|
258
|
-
|
|
259
|
-
metrics
|
|
260
|
-
Get current agent metrics
|
|
261
|
-
|
|
262
|
-
Example:
|
|
263
|
-
pystreammcp metrics
|
|
264
|
-
|
|
265
|
-
help
|
|
266
|
-
Show this help message
|
|
267
|
-
|
|
268
|
-
OUTPUT FORMAT:
|
|
269
|
-
All commands return JSON output for easy parsing in workflows
|
|
270
|
-
|
|
271
|
-
EXAMPLES:
|
|
272
|
-
# Query with cost tracking
|
|
273
|
-
pystreammcp query "Find churn risk customers"
|
|
274
|
-
|
|
275
|
-
# Efficient query for token-constrained environments
|
|
276
|
-
pystreammcp query "List segments" discover automation_agent
|
|
277
|
-
|
|
278
|
-
# Get metrics for reporting
|
|
279
|
-
pystreammcp metrics | jq '.metrics.total_cost_saved'
|
|
280
|
-
"""
|
|
281
|
-
print(help_text)
|
|
282
|
-
|
|
283
|
-
|
|
284
135
|
if __name__ == "__main__":
|
|
285
|
-
|
|
136
|
+
cli()
|
pystreammcp/discovery.py
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
"""Discovery types for PyStreamMCP.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Discovery types and logic for PyStreamMCP.
|
|
2
|
+
|
|
3
|
+
`SourceRegistry` is real discovery logic: register data sources (name,
|
|
4
|
+
description, type, tags), then find the ones most relevant to a query
|
|
5
|
+
context using word-overlap relevance scoring. This is deliberately a
|
|
6
|
+
simple, honest heuristic — Jaccard token overlap between the query context
|
|
7
|
+
and each source's registered description/tags/name — not a semantic or
|
|
8
|
+
embedding-based search. Sources with zero token overlap are excluded from
|
|
9
|
+
results rather than padded in with a fabricated score, so an empty
|
|
10
|
+
registry or a context that matches nothing returns no sources, not
|
|
11
|
+
placeholder data.
|
|
12
|
+
|
|
13
|
+
`Discovery` / `DiscoveredSource` / `SourceType` are the result types
|
|
14
|
+
`SourceRegistry.discover()` returns results as, so callers get real,
|
|
15
|
+
structured discovery results rather than a bare dict.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
5
21
|
from dataclasses import dataclass, field
|
|
6
22
|
from datetime import datetime
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import Any, Dict, List, Optional, Set
|
|
25
|
+
|
|
26
|
+
_WORD_RE = re.compile(r"[a-z0-9]+")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _tokenize(text: str) -> Set[str]:
|
|
30
|
+
return set(_WORD_RE.findall(text.lower()))
|
|
7
31
|
|
|
8
32
|
|
|
9
33
|
class SourceType(str, Enum):
|
|
@@ -14,11 +38,12 @@ class SourceType(str, Enum):
|
|
|
14
38
|
CACHE = "cache"
|
|
15
39
|
EXTERNAL = "external"
|
|
16
40
|
COMPUTED = "computed"
|
|
41
|
+
UNKNOWN = "unknown"
|
|
17
42
|
|
|
18
43
|
|
|
19
44
|
@dataclass
|
|
20
45
|
class DiscoveredSource:
|
|
21
|
-
"""A discovered data source."""
|
|
46
|
+
"""A discovered data source, returned by SourceRegistry.discover()."""
|
|
22
47
|
|
|
23
48
|
name: str
|
|
24
49
|
source_type: SourceType
|
|
@@ -80,3 +105,112 @@ class Discovery:
|
|
|
80
105
|
def high_relevance_sources(self, threshold: float = 0.8) -> List[DiscoveredSource]:
|
|
81
106
|
"""Get high-relevance sources."""
|
|
82
107
|
return [s for s in self.discovered_sources if s.relevance_score >= threshold]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class DataSource:
|
|
112
|
+
"""A data source registered with a SourceRegistry."""
|
|
113
|
+
|
|
114
|
+
name: str
|
|
115
|
+
description: str
|
|
116
|
+
type: str = "unknown"
|
|
117
|
+
tags: List[str] = field(default_factory=list)
|
|
118
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
119
|
+
estimated_tokens: int = 0
|
|
120
|
+
|
|
121
|
+
def _text(self) -> str:
|
|
122
|
+
return " ".join([self.name, self.description, *self.tags])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SourceRegistry:
|
|
126
|
+
"""In-memory registry of data sources, with real relevance scoring."""
|
|
127
|
+
|
|
128
|
+
def __init__(self) -> None:
|
|
129
|
+
self._sources: Dict[str, DataSource] = {}
|
|
130
|
+
|
|
131
|
+
def register(
|
|
132
|
+
self,
|
|
133
|
+
name: str,
|
|
134
|
+
description: str,
|
|
135
|
+
type: str = "unknown",
|
|
136
|
+
tags: Optional[List[str]] = None,
|
|
137
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
138
|
+
estimated_tokens: int = 0,
|
|
139
|
+
) -> DataSource:
|
|
140
|
+
"""Register (or replace) a data source by name."""
|
|
141
|
+
source = DataSource(
|
|
142
|
+
name=name,
|
|
143
|
+
description=description,
|
|
144
|
+
type=type,
|
|
145
|
+
tags=list(tags) if tags else [],
|
|
146
|
+
metadata=dict(metadata) if metadata else {},
|
|
147
|
+
estimated_tokens=estimated_tokens,
|
|
148
|
+
)
|
|
149
|
+
self._sources[name] = source
|
|
150
|
+
return source
|
|
151
|
+
|
|
152
|
+
def unregister(self, name: str) -> bool:
|
|
153
|
+
"""Remove a registered source. Returns True if it existed."""
|
|
154
|
+
return self._sources.pop(name, None) is not None
|
|
155
|
+
|
|
156
|
+
def list_sources(self) -> List[DataSource]:
|
|
157
|
+
return list(self._sources.values())
|
|
158
|
+
|
|
159
|
+
def discover(self, context: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
160
|
+
"""Return registered sources ranked by real relevance to `context`.
|
|
161
|
+
|
|
162
|
+
Relevance is Jaccard token overlap: |context tokens ∩ source
|
|
163
|
+
tokens| / |context tokens ∪ source tokens|. Sources that share no
|
|
164
|
+
tokens with `context` are excluded, not returned with a made-up
|
|
165
|
+
low score. Returns plain dicts (the wire-format shape used by the
|
|
166
|
+
REST/MCP surfaces) — use `discover_typed` for `DiscoveredSource`
|
|
167
|
+
objects.
|
|
168
|
+
"""
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
"name": s.name,
|
|
172
|
+
"type": s.type,
|
|
173
|
+
"relevance": round(score, 4),
|
|
174
|
+
"matched_terms": matched,
|
|
175
|
+
}
|
|
176
|
+
for score, matched, s in self._score(context, limit)
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
def discover_typed(self, query_id: str, context: str, limit: int = 10) -> Discovery:
|
|
180
|
+
"""Same ranking as `discover`, returned as a `Discovery` result of
|
|
181
|
+
real `DiscoveredSource` objects instead of plain dicts."""
|
|
182
|
+
result = Discovery(query_id=query_id)
|
|
183
|
+
for score, _matched, s in self._score(context, limit):
|
|
184
|
+
try:
|
|
185
|
+
source_type = SourceType(s.type)
|
|
186
|
+
except ValueError:
|
|
187
|
+
source_type = SourceType.UNKNOWN
|
|
188
|
+
result.add_source(
|
|
189
|
+
DiscoveredSource(
|
|
190
|
+
name=s.name,
|
|
191
|
+
source_type=source_type,
|
|
192
|
+
relevance_score=score,
|
|
193
|
+
estimated_tokens=s.estimated_tokens,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def _score(self, context: str, limit: int):
|
|
199
|
+
context_tokens = _tokenize(context)
|
|
200
|
+
if not context_tokens or not self._sources:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
scored = []
|
|
204
|
+
for source in self._sources.values():
|
|
205
|
+
source_tokens = _tokenize(source._text())
|
|
206
|
+
if not source_tokens:
|
|
207
|
+
continue
|
|
208
|
+
overlap = context_tokens & source_tokens
|
|
209
|
+
if not overlap:
|
|
210
|
+
continue
|
|
211
|
+
union = context_tokens | source_tokens
|
|
212
|
+
relevance = len(overlap) / len(union)
|
|
213
|
+
scored.append((relevance, sorted(overlap), source))
|
|
214
|
+
|
|
215
|
+
scored.sort(key=lambda item: item[0], reverse=True)
|
|
216
|
+
return scored[:limit]
|
|
@@ -8,7 +8,7 @@ Provides both:
|
|
|
8
8
|
2. Legacy PyStreamMCPTool & PyStreamMCPRetriever (backward compatible)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Optional, Dict, Any, List, Type
|
|
11
|
+
from typing import Optional, Dict, Any, List, Type, TYPE_CHECKING
|
|
12
12
|
import asyncio
|
|
13
13
|
from pystreammcp import (
|
|
14
14
|
Agent,
|
|
@@ -21,6 +21,11 @@ from pystreammcp import (
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
from pystreammcp.adapters import QueryResult as AdapterQueryResult
|
|
24
|
+
from pystreammcp.discovery import SourceRegistry
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from langchain_core.tools import Tool as LangchainTool
|
|
28
|
+
from langchain_core.retrievers import BaseRetriever
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
class LangchainAdapter(AgentFrameworkAdapter):
|
|
@@ -30,11 +35,15 @@ class LangchainAdapter(AgentFrameworkAdapter):
|
|
|
30
35
|
Supports both sync and async execution.
|
|
31
36
|
"""
|
|
32
37
|
|
|
33
|
-
def __init__(self, config: AdapterConfig):
|
|
38
|
+
def __init__(self, config: AdapterConfig, registry: Optional[SourceRegistry] = None):
|
|
34
39
|
"""Initialize Langchain adapter.
|
|
35
40
|
|
|
36
41
|
Args:
|
|
37
42
|
config: Adapter configuration
|
|
43
|
+
registry: Optional shared SourceRegistry (e.g. the same one used
|
|
44
|
+
by a PyStreamMCPAPI or PyStreamMCPServer instance, so sources
|
|
45
|
+
registered on one surface are discoverable from another). A
|
|
46
|
+
new, empty registry is created if not given.
|
|
38
47
|
"""
|
|
39
48
|
super().__init__(config)
|
|
40
49
|
self.agent = Agent(
|
|
@@ -43,6 +52,7 @@ class LangchainAdapter(AgentFrameworkAdapter):
|
|
|
43
52
|
optimization_strategy=config.optimization_strategy,
|
|
44
53
|
max_tokens=config.max_tokens,
|
|
45
54
|
)
|
|
55
|
+
self.registry = registry if registry is not None else SourceRegistry()
|
|
46
56
|
|
|
47
57
|
def query(
|
|
48
58
|
self, text: str, intent: str = "retrieve", **kwargs
|
|
@@ -92,6 +102,11 @@ class LangchainAdapter(AgentFrameworkAdapter):
|
|
|
92
102
|
def discover(self, context: str, **kwargs) -> Dict[str, Any]:
|
|
93
103
|
"""Discover relevant data sources for context.
|
|
94
104
|
|
|
105
|
+
Ranks sources registered on `self.registry` (see
|
|
106
|
+
`SourceRegistry.register` / pass a shared registry to `__init__`)
|
|
107
|
+
by real token overlap against `context`. Returns an empty list if
|
|
108
|
+
nothing is registered or nothing overlaps — never fabricated data.
|
|
109
|
+
|
|
95
110
|
Args:
|
|
96
111
|
context: Context for discovery
|
|
97
112
|
**kwargs: Additional arguments
|
|
@@ -99,17 +114,10 @@ class LangchainAdapter(AgentFrameworkAdapter):
|
|
|
99
114
|
Returns:
|
|
100
115
|
Dictionary with discovered sources
|
|
101
116
|
"""
|
|
102
|
-
|
|
117
|
+
sources = self.registry.discover(context, limit=kwargs.get("limit", 10))
|
|
103
118
|
return {
|
|
104
|
-
"sources":
|
|
105
|
-
|
|
106
|
-
"name": "data_warehouse",
|
|
107
|
-
"relevance": 0.95,
|
|
108
|
-
"type": "database",
|
|
109
|
-
"estimated_tokens": 500,
|
|
110
|
-
}
|
|
111
|
-
],
|
|
112
|
-
"total_sources": 1,
|
|
119
|
+
"sources": sources,
|
|
120
|
+
"total_sources": len(sources),
|
|
113
121
|
"search_context": context,
|
|
114
122
|
}
|
|
115
123
|
|
|
@@ -8,7 +8,7 @@ Provides both:
|
|
|
8
8
|
2. Legacy StreamMCPRetriever (backward compatible)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Any, List, Optional, Dict
|
|
11
|
+
from typing import Any, List, Optional, Dict, TYPE_CHECKING
|
|
12
12
|
import asyncio
|
|
13
13
|
from pystreammcp import (
|
|
14
14
|
Agent,
|
|
@@ -22,6 +22,9 @@ from pystreammcp import (
|
|
|
22
22
|
|
|
23
23
|
from pystreammcp.adapters import QueryResult as AdapterQueryResult
|
|
24
24
|
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from llama_index.core.retrievers import BaseRetriever
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
class LlamaIndexAdapter(AgentFrameworkAdapter):
|
|
27
30
|
"""LlamaIndex adapter using new AgentFrameworkAdapter base class.
|
|
@@ -8,7 +8,7 @@ Provides both:
|
|
|
8
8
|
2. Legacy PyStreamMCPPlugin (backward compatible)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Optional, Dict, Any
|
|
11
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
12
12
|
import asyncio
|
|
13
13
|
from pystreammcp import (
|
|
14
14
|
Agent,
|
|
@@ -22,6 +22,9 @@ from pystreammcp import (
|
|
|
22
22
|
|
|
23
23
|
from pystreammcp.adapters import QueryResult as AdapterQueryResult
|
|
24
24
|
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from semantic_kernel.functions import KernelPlugin
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
class SemanticKernelAdapter(AgentFrameworkAdapter):
|
|
27
30
|
"""Semantic Kernel adapter using new AdapterFrameworkAdapter base class.
|