PyStreamMCP 3.2.0__tar.gz → 3.3.0__tar.gz
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-3.2.0/python/PyStreamMCP.egg-info → pystreammcp-3.3.0}/PKG-INFO +76 -2
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/README.md +75 -1
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/pyproject.toml +1 -1
- {pystreammcp-3.2.0 → pystreammcp-3.3.0/python/PyStreamMCP.egg-info}/PKG-INFO +76 -2
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/SOURCES.txt +3 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/__init__.py +4 -2
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/api.py +47 -4
- pystreammcp-3.3.0/python/pystreammcp/discovery.py +216 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/langchain.py +15 -11
- pystreammcp-3.3.0/python/pystreammcp/mcp_server.py +330 -0
- pystreammcp-3.3.0/python/pystreammcp/testing.py +123 -0
- pystreammcp-3.3.0/tests/test_discovery.py +118 -0
- pystreammcp-3.3.0/tests/test_mcp_server_resilience.py +172 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint1_foundation.py +3 -2
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint2_langchain.py +12 -1
- pystreammcp-3.2.0/python/pystreammcp/discovery.py +0 -82
- pystreammcp-3.2.0/python/pystreammcp/mcp_server.py +0 -283
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/LICENSE +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/MANIFEST.in +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/dependency_links.txt +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/requires.txt +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/top_level.txt +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/_mcp_connector.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/_mcp_tools.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/adapters.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/agent.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/cli.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/cli_daemon.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/cli_dashboard.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/context.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/decomposition.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/crewai.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/haystack.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/langsmith.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/llamaindex.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/pydantic_ai.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/semantic_kernel.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml/relevance.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml/tagging.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/multi_agent.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/observability/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/observability/metrics.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/okf_core.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/okf_discovery.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/okf_query_planner.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization/advanced.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization/decomposition.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/airflow.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/nocode_rpa.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/temporal.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/qa/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/qa/validator.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/quality.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/query.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/scripts/__init__.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/scripts/post_install.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/server.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/streaming.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/webhook_handlers.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/webhook_router.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/setup.cfg +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/setup.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_integration_phase2.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_langchain_integration.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_llamaindex_integration.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_mcp_connector_discovery.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_mcp_tools_handler.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_okf_core.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_okf_discovery.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_okf_query_planner.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_phase3_advanced.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_phase3_items12.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_phase3_items345.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint3_rag_frameworks.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint4_agent_frameworks.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint5_orchestration.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_statguardian_integration.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_webhook_hmac_auth.py +0 -0
- {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_webhook_router.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyStreamMCP
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Intelligent MCP orchestration hub - Intent understanding, capability matching, tool ranking with cohesive foundation
|
|
5
5
|
Author-email: Georgi Mammen Mullassery <mullassery@gmail.com>
|
|
6
6
|
License: Proprietary License — Free to use with explicit attribution
|
|
@@ -91,7 +91,11 @@ pip install "PyStreamMCP[semantic-kernel]" # Semantic Kernel adapter
|
|
|
91
91
|
pip install "PyStreamMCP[all-integrations]" # every framework adapter
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Requires Python 3.9
|
|
94
|
+
Requires Python 3.9+ for the core SDK (`Agent`, `SourceRegistry`,
|
|
95
|
+
`Orchestrator`). The `mcp` and `semantic-kernel` extras require Python
|
|
96
|
+
3.10+ — their upstream packages don't publish Python 3.9-compatible
|
|
97
|
+
releases, so `pip install` for those extras will fail on 3.9. CI tests
|
|
98
|
+
3.10, 3.11, and 3.12 only; 3.9 is not covered by CI.
|
|
95
99
|
|
|
96
100
|
## Quick start
|
|
97
101
|
|
|
@@ -120,6 +124,33 @@ for a full runnable walkthrough, including framework adapters for
|
|
|
120
124
|
LangChain, LlamaIndex, CrewAI, Semantic Kernel, PydanticAI, and Haystack
|
|
121
125
|
under [`pystreammcp.integrations`](python/pystreammcp/integrations/).
|
|
122
126
|
|
|
127
|
+
## Data source discovery
|
|
128
|
+
|
|
129
|
+
`SourceRegistry` (`pystreammcp.SourceRegistry`) ranks *registered* data
|
|
130
|
+
sources by real word-overlap relevance to a query — not a fabricated or
|
|
131
|
+
hardcoded result:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pystreammcp import SourceRegistry
|
|
135
|
+
|
|
136
|
+
registry = SourceRegistry()
|
|
137
|
+
registry.register(
|
|
138
|
+
"orders_db",
|
|
139
|
+
description="customer order history and revenue by quarter",
|
|
140
|
+
type="database",
|
|
141
|
+
tags=["orders", "revenue"],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
results = registry.discover("customer revenue by quarter")
|
|
145
|
+
# [{"name": "orders_db", "type": "database", "relevance": 0.4444, "matched_terms": [...]}]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The same registry backs `POST /discover` (REST), the `pystreammcp_discover`
|
|
149
|
+
MCP tool, and `LangchainAdapter.discover()` — register sources once via
|
|
150
|
+
`POST /sources` / `pystreammcp_register_source` / `registry.register()`,
|
|
151
|
+
then discover against them from any of the three surfaces. This is a
|
|
152
|
+
simple Jaccard token-overlap heuristic, not semantic/embedding search.
|
|
153
|
+
|
|
123
154
|
## MCP orchestration & federation
|
|
124
155
|
|
|
125
156
|
`Orchestrator` (`pystreammcp.Orchestrator`) discovers and routes across
|
|
@@ -217,6 +248,49 @@ pip install -e ".[dev,api,mcp,langchain,llamaindex,semantic-kernel]"
|
|
|
217
248
|
pytest tests/ -v
|
|
218
249
|
```
|
|
219
250
|
|
|
251
|
+
Note: this exact command needs Python 3.10+ (see [Install](#install)); on
|
|
252
|
+
3.9 the `mcp` and `semantic-kernel` extras fail to resolve, so drop them
|
|
253
|
+
from the extras list if you're on 3.9.
|
|
254
|
+
|
|
255
|
+
## Known Issues
|
|
256
|
+
|
|
257
|
+
- ~~No end-to-end tests for LLM failure states~~ **Fixed.** PyStreamMCP's
|
|
258
|
+
own code never calls a model provider directly (no code path here to add
|
|
259
|
+
`except RateLimitError`/`APIError` handling *to* — see `AgentFrameworkAdapter`
|
|
260
|
+
below), so what's now tested is the part this repo actually owns: that the
|
|
261
|
+
MCP tool-call path doesn't crash when whatever it's orchestrating fails.
|
|
262
|
+
`pystreammcp.testing` exports `RateLimitError`, `ContextWindowExceededError`,
|
|
263
|
+
`MalformedModelResponseError`, and `FailingAgent`/`FailingAdapter` stand-ins
|
|
264
|
+
that raise them; `mcp_server.py`'s `call_tool`/`process_message` now catch
|
|
265
|
+
exceptions raised during tool execution and return a structured
|
|
266
|
+
`{"status": "error", "error_type": ..., "message": ...}` response instead
|
|
267
|
+
of propagating uncaught. See `tests/test_mcp_server_resilience.py`.
|
|
268
|
+
- ~~No Pydantic validation for the MCP tool protocol itself~~ **Fixed.**
|
|
269
|
+
Each of the four MCP tools now has a Pydantic arg model
|
|
270
|
+
(`QueryToolArgs`/`DiscoverToolArgs`/`RegisterSourceToolArgs`/`OptimizeToolArgs`
|
|
271
|
+
in `mcp_server.py`) validated in `call_tool` before dispatch (wrong types,
|
|
272
|
+
invalid enum values, and missing required fields all now return a
|
|
273
|
+
structured `validation_errors` response instead of reaching the handler
|
|
274
|
+
unchecked), and `MCPTool.input_schema` is generated from those same models
|
|
275
|
+
via `model_json_schema()` — the advertised schema and the schema actually
|
|
276
|
+
enforced can no longer drift apart the way a hand-duplicated dict could.
|
|
277
|
+
- ~~No shipped mock MCP server / mock LLM fixture~~ **Fixed.** Added
|
|
278
|
+
`tests/conftest.py` (centralizing the `sys.path` setup 5 test files were
|
|
279
|
+
duplicating, plus `mock_adapter`/`failing_agent`/`failing_adapter`/
|
|
280
|
+
`llm_failure` pytest fixtures) and `pystreammcp/testing.py`, exporting
|
|
281
|
+
`MockAdapter` (promoted from the old test-local copy) and the
|
|
282
|
+
failure-injection stand-ins above for downstream projects' own test
|
|
283
|
+
suites to import directly.
|
|
284
|
+
- **Per-query context discovery** now has a real implementation
|
|
285
|
+
(`pystreammcp.discovery.SourceRegistry`): register data sources (name,
|
|
286
|
+
description, type, tags), and `/discover` (REST), `pystreammcp_discover`
|
|
287
|
+
(MCP), and `LangchainAdapter.discover()` all rank *registered* sources
|
|
288
|
+
by real Jaccard token-overlap against the query — an empty registry or
|
|
289
|
+
a non-matching context returns an empty list, not fabricated data. This
|
|
290
|
+
is a simple word-overlap heuristic, not semantic/embedding search — if
|
|
291
|
+
you need that, wrap `SourceRegistry` with your own similarity scoring.
|
|
292
|
+
- No open GitHub issues as of this pass.
|
|
293
|
+
|
|
220
294
|
## Rust workspace (not shipped)
|
|
221
295
|
|
|
222
296
|
`core/` and `python/src/lib.rs` are an in-progress Rust workspace intended
|
|
@@ -27,7 +27,11 @@ pip install "PyStreamMCP[semantic-kernel]" # Semantic Kernel adapter
|
|
|
27
27
|
pip install "PyStreamMCP[all-integrations]" # every framework adapter
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Requires Python 3.9
|
|
30
|
+
Requires Python 3.9+ for the core SDK (`Agent`, `SourceRegistry`,
|
|
31
|
+
`Orchestrator`). The `mcp` and `semantic-kernel` extras require Python
|
|
32
|
+
3.10+ — their upstream packages don't publish Python 3.9-compatible
|
|
33
|
+
releases, so `pip install` for those extras will fail on 3.9. CI tests
|
|
34
|
+
3.10, 3.11, and 3.12 only; 3.9 is not covered by CI.
|
|
31
35
|
|
|
32
36
|
## Quick start
|
|
33
37
|
|
|
@@ -56,6 +60,33 @@ for a full runnable walkthrough, including framework adapters for
|
|
|
56
60
|
LangChain, LlamaIndex, CrewAI, Semantic Kernel, PydanticAI, and Haystack
|
|
57
61
|
under [`pystreammcp.integrations`](python/pystreammcp/integrations/).
|
|
58
62
|
|
|
63
|
+
## Data source discovery
|
|
64
|
+
|
|
65
|
+
`SourceRegistry` (`pystreammcp.SourceRegistry`) ranks *registered* data
|
|
66
|
+
sources by real word-overlap relevance to a query — not a fabricated or
|
|
67
|
+
hardcoded result:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from pystreammcp import SourceRegistry
|
|
71
|
+
|
|
72
|
+
registry = SourceRegistry()
|
|
73
|
+
registry.register(
|
|
74
|
+
"orders_db",
|
|
75
|
+
description="customer order history and revenue by quarter",
|
|
76
|
+
type="database",
|
|
77
|
+
tags=["orders", "revenue"],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
results = registry.discover("customer revenue by quarter")
|
|
81
|
+
# [{"name": "orders_db", "type": "database", "relevance": 0.4444, "matched_terms": [...]}]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same registry backs `POST /discover` (REST), the `pystreammcp_discover`
|
|
85
|
+
MCP tool, and `LangchainAdapter.discover()` — register sources once via
|
|
86
|
+
`POST /sources` / `pystreammcp_register_source` / `registry.register()`,
|
|
87
|
+
then discover against them from any of the three surfaces. This is a
|
|
88
|
+
simple Jaccard token-overlap heuristic, not semantic/embedding search.
|
|
89
|
+
|
|
59
90
|
## MCP orchestration & federation
|
|
60
91
|
|
|
61
92
|
`Orchestrator` (`pystreammcp.Orchestrator`) discovers and routes across
|
|
@@ -153,6 +184,49 @@ pip install -e ".[dev,api,mcp,langchain,llamaindex,semantic-kernel]"
|
|
|
153
184
|
pytest tests/ -v
|
|
154
185
|
```
|
|
155
186
|
|
|
187
|
+
Note: this exact command needs Python 3.10+ (see [Install](#install)); on
|
|
188
|
+
3.9 the `mcp` and `semantic-kernel` extras fail to resolve, so drop them
|
|
189
|
+
from the extras list if you're on 3.9.
|
|
190
|
+
|
|
191
|
+
## Known Issues
|
|
192
|
+
|
|
193
|
+
- ~~No end-to-end tests for LLM failure states~~ **Fixed.** PyStreamMCP's
|
|
194
|
+
own code never calls a model provider directly (no code path here to add
|
|
195
|
+
`except RateLimitError`/`APIError` handling *to* — see `AgentFrameworkAdapter`
|
|
196
|
+
below), so what's now tested is the part this repo actually owns: that the
|
|
197
|
+
MCP tool-call path doesn't crash when whatever it's orchestrating fails.
|
|
198
|
+
`pystreammcp.testing` exports `RateLimitError`, `ContextWindowExceededError`,
|
|
199
|
+
`MalformedModelResponseError`, and `FailingAgent`/`FailingAdapter` stand-ins
|
|
200
|
+
that raise them; `mcp_server.py`'s `call_tool`/`process_message` now catch
|
|
201
|
+
exceptions raised during tool execution and return a structured
|
|
202
|
+
`{"status": "error", "error_type": ..., "message": ...}` response instead
|
|
203
|
+
of propagating uncaught. See `tests/test_mcp_server_resilience.py`.
|
|
204
|
+
- ~~No Pydantic validation for the MCP tool protocol itself~~ **Fixed.**
|
|
205
|
+
Each of the four MCP tools now has a Pydantic arg model
|
|
206
|
+
(`QueryToolArgs`/`DiscoverToolArgs`/`RegisterSourceToolArgs`/`OptimizeToolArgs`
|
|
207
|
+
in `mcp_server.py`) validated in `call_tool` before dispatch (wrong types,
|
|
208
|
+
invalid enum values, and missing required fields all now return a
|
|
209
|
+
structured `validation_errors` response instead of reaching the handler
|
|
210
|
+
unchecked), and `MCPTool.input_schema` is generated from those same models
|
|
211
|
+
via `model_json_schema()` — the advertised schema and the schema actually
|
|
212
|
+
enforced can no longer drift apart the way a hand-duplicated dict could.
|
|
213
|
+
- ~~No shipped mock MCP server / mock LLM fixture~~ **Fixed.** Added
|
|
214
|
+
`tests/conftest.py` (centralizing the `sys.path` setup 5 test files were
|
|
215
|
+
duplicating, plus `mock_adapter`/`failing_agent`/`failing_adapter`/
|
|
216
|
+
`llm_failure` pytest fixtures) and `pystreammcp/testing.py`, exporting
|
|
217
|
+
`MockAdapter` (promoted from the old test-local copy) and the
|
|
218
|
+
failure-injection stand-ins above for downstream projects' own test
|
|
219
|
+
suites to import directly.
|
|
220
|
+
- **Per-query context discovery** now has a real implementation
|
|
221
|
+
(`pystreammcp.discovery.SourceRegistry`): register data sources (name,
|
|
222
|
+
description, type, tags), and `/discover` (REST), `pystreammcp_discover`
|
|
223
|
+
(MCP), and `LangchainAdapter.discover()` all rank *registered* sources
|
|
224
|
+
by real Jaccard token-overlap against the query — an empty registry or
|
|
225
|
+
a non-matching context returns an empty list, not fabricated data. This
|
|
226
|
+
is a simple word-overlap heuristic, not semantic/embedding search — if
|
|
227
|
+
you need that, wrap `SourceRegistry` with your own similarity scoring.
|
|
228
|
+
- No open GitHub issues as of this pass.
|
|
229
|
+
|
|
156
230
|
## Rust workspace (not shipped)
|
|
157
231
|
|
|
158
232
|
`core/` and `python/src/lib.rs` are an in-progress Rust workspace intended
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "PyStreamMCP"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.0"
|
|
8
8
|
description = "Intelligent MCP orchestration hub - Intent understanding, capability matching, tool ranking with cohesive foundation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Proprietary License — Free to use with explicit attribution"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyStreamMCP
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Intelligent MCP orchestration hub - Intent understanding, capability matching, tool ranking with cohesive foundation
|
|
5
5
|
Author-email: Georgi Mammen Mullassery <mullassery@gmail.com>
|
|
6
6
|
License: Proprietary License — Free to use with explicit attribution
|
|
@@ -91,7 +91,11 @@ pip install "PyStreamMCP[semantic-kernel]" # Semantic Kernel adapter
|
|
|
91
91
|
pip install "PyStreamMCP[all-integrations]" # every framework adapter
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Requires Python 3.9
|
|
94
|
+
Requires Python 3.9+ for the core SDK (`Agent`, `SourceRegistry`,
|
|
95
|
+
`Orchestrator`). The `mcp` and `semantic-kernel` extras require Python
|
|
96
|
+
3.10+ — their upstream packages don't publish Python 3.9-compatible
|
|
97
|
+
releases, so `pip install` for those extras will fail on 3.9. CI tests
|
|
98
|
+
3.10, 3.11, and 3.12 only; 3.9 is not covered by CI.
|
|
95
99
|
|
|
96
100
|
## Quick start
|
|
97
101
|
|
|
@@ -120,6 +124,33 @@ for a full runnable walkthrough, including framework adapters for
|
|
|
120
124
|
LangChain, LlamaIndex, CrewAI, Semantic Kernel, PydanticAI, and Haystack
|
|
121
125
|
under [`pystreammcp.integrations`](python/pystreammcp/integrations/).
|
|
122
126
|
|
|
127
|
+
## Data source discovery
|
|
128
|
+
|
|
129
|
+
`SourceRegistry` (`pystreammcp.SourceRegistry`) ranks *registered* data
|
|
130
|
+
sources by real word-overlap relevance to a query — not a fabricated or
|
|
131
|
+
hardcoded result:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pystreammcp import SourceRegistry
|
|
135
|
+
|
|
136
|
+
registry = SourceRegistry()
|
|
137
|
+
registry.register(
|
|
138
|
+
"orders_db",
|
|
139
|
+
description="customer order history and revenue by quarter",
|
|
140
|
+
type="database",
|
|
141
|
+
tags=["orders", "revenue"],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
results = registry.discover("customer revenue by quarter")
|
|
145
|
+
# [{"name": "orders_db", "type": "database", "relevance": 0.4444, "matched_terms": [...]}]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The same registry backs `POST /discover` (REST), the `pystreammcp_discover`
|
|
149
|
+
MCP tool, and `LangchainAdapter.discover()` — register sources once via
|
|
150
|
+
`POST /sources` / `pystreammcp_register_source` / `registry.register()`,
|
|
151
|
+
then discover against them from any of the three surfaces. This is a
|
|
152
|
+
simple Jaccard token-overlap heuristic, not semantic/embedding search.
|
|
153
|
+
|
|
123
154
|
## MCP orchestration & federation
|
|
124
155
|
|
|
125
156
|
`Orchestrator` (`pystreammcp.Orchestrator`) discovers and routes across
|
|
@@ -217,6 +248,49 @@ pip install -e ".[dev,api,mcp,langchain,llamaindex,semantic-kernel]"
|
|
|
217
248
|
pytest tests/ -v
|
|
218
249
|
```
|
|
219
250
|
|
|
251
|
+
Note: this exact command needs Python 3.10+ (see [Install](#install)); on
|
|
252
|
+
3.9 the `mcp` and `semantic-kernel` extras fail to resolve, so drop them
|
|
253
|
+
from the extras list if you're on 3.9.
|
|
254
|
+
|
|
255
|
+
## Known Issues
|
|
256
|
+
|
|
257
|
+
- ~~No end-to-end tests for LLM failure states~~ **Fixed.** PyStreamMCP's
|
|
258
|
+
own code never calls a model provider directly (no code path here to add
|
|
259
|
+
`except RateLimitError`/`APIError` handling *to* — see `AgentFrameworkAdapter`
|
|
260
|
+
below), so what's now tested is the part this repo actually owns: that the
|
|
261
|
+
MCP tool-call path doesn't crash when whatever it's orchestrating fails.
|
|
262
|
+
`pystreammcp.testing` exports `RateLimitError`, `ContextWindowExceededError`,
|
|
263
|
+
`MalformedModelResponseError`, and `FailingAgent`/`FailingAdapter` stand-ins
|
|
264
|
+
that raise them; `mcp_server.py`'s `call_tool`/`process_message` now catch
|
|
265
|
+
exceptions raised during tool execution and return a structured
|
|
266
|
+
`{"status": "error", "error_type": ..., "message": ...}` response instead
|
|
267
|
+
of propagating uncaught. See `tests/test_mcp_server_resilience.py`.
|
|
268
|
+
- ~~No Pydantic validation for the MCP tool protocol itself~~ **Fixed.**
|
|
269
|
+
Each of the four MCP tools now has a Pydantic arg model
|
|
270
|
+
(`QueryToolArgs`/`DiscoverToolArgs`/`RegisterSourceToolArgs`/`OptimizeToolArgs`
|
|
271
|
+
in `mcp_server.py`) validated in `call_tool` before dispatch (wrong types,
|
|
272
|
+
invalid enum values, and missing required fields all now return a
|
|
273
|
+
structured `validation_errors` response instead of reaching the handler
|
|
274
|
+
unchecked), and `MCPTool.input_schema` is generated from those same models
|
|
275
|
+
via `model_json_schema()` — the advertised schema and the schema actually
|
|
276
|
+
enforced can no longer drift apart the way a hand-duplicated dict could.
|
|
277
|
+
- ~~No shipped mock MCP server / mock LLM fixture~~ **Fixed.** Added
|
|
278
|
+
`tests/conftest.py` (centralizing the `sys.path` setup 5 test files were
|
|
279
|
+
duplicating, plus `mock_adapter`/`failing_agent`/`failing_adapter`/
|
|
280
|
+
`llm_failure` pytest fixtures) and `pystreammcp/testing.py`, exporting
|
|
281
|
+
`MockAdapter` (promoted from the old test-local copy) and the
|
|
282
|
+
failure-injection stand-ins above for downstream projects' own test
|
|
283
|
+
suites to import directly.
|
|
284
|
+
- **Per-query context discovery** now has a real implementation
|
|
285
|
+
(`pystreammcp.discovery.SourceRegistry`): register data sources (name,
|
|
286
|
+
description, type, tags), and `/discover` (REST), `pystreammcp_discover`
|
|
287
|
+
(MCP), and `LangchainAdapter.discover()` all rank *registered* sources
|
|
288
|
+
by real Jaccard token-overlap against the query — an empty registry or
|
|
289
|
+
a non-matching context returns an empty list, not fabricated data. This
|
|
290
|
+
is a simple word-overlap heuristic, not semantic/embedding search — if
|
|
291
|
+
you need that, wrap `SourceRegistry` with your own similarity scoring.
|
|
292
|
+
- No open GitHub issues as of this pass.
|
|
293
|
+
|
|
220
294
|
## Rust workspace (not shipped)
|
|
221
295
|
|
|
222
296
|
`core/` and `python/src/lib.rs` are an in-progress Rust workspace intended
|
|
@@ -31,6 +31,7 @@ python/pystreammcp/quality.py
|
|
|
31
31
|
python/pystreammcp/query.py
|
|
32
32
|
python/pystreammcp/server.py
|
|
33
33
|
python/pystreammcp/streaming.py
|
|
34
|
+
python/pystreammcp/testing.py
|
|
34
35
|
python/pystreammcp/webhook_handlers.py
|
|
35
36
|
python/pystreammcp/webhook_router.py
|
|
36
37
|
python/pystreammcp/integrations/__init__.py
|
|
@@ -57,10 +58,12 @@ python/pystreammcp/qa/__init__.py
|
|
|
57
58
|
python/pystreammcp/qa/validator.py
|
|
58
59
|
python/pystreammcp/scripts/__init__.py
|
|
59
60
|
python/pystreammcp/scripts/post_install.py
|
|
61
|
+
tests/test_discovery.py
|
|
60
62
|
tests/test_integration_phase2.py
|
|
61
63
|
tests/test_langchain_integration.py
|
|
62
64
|
tests/test_llamaindex_integration.py
|
|
63
65
|
tests/test_mcp_connector_discovery.py
|
|
66
|
+
tests/test_mcp_server_resilience.py
|
|
64
67
|
tests/test_mcp_tools_handler.py
|
|
65
68
|
tests/test_okf_core.py
|
|
66
69
|
tests/test_okf_discovery.py
|
|
@@ -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.0"
|
|
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 []),
|
|
@@ -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",
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
|
21
|
+
from dataclasses import dataclass, field
|
|
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()))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SourceType(str, Enum):
|
|
34
|
+
"""Type of data source."""
|
|
35
|
+
|
|
36
|
+
TABLE = "table"
|
|
37
|
+
INDEX = "index"
|
|
38
|
+
CACHE = "cache"
|
|
39
|
+
EXTERNAL = "external"
|
|
40
|
+
COMPUTED = "computed"
|
|
41
|
+
UNKNOWN = "unknown"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DiscoveredSource:
|
|
46
|
+
"""A discovered data source, returned by SourceRegistry.discover()."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
source_type: SourceType
|
|
50
|
+
relevance_score: float
|
|
51
|
+
estimated_tokens: int
|
|
52
|
+
freshness_score: float = 1.0
|
|
53
|
+
source_id: str = ""
|
|
54
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
55
|
+
|
|
56
|
+
def __post_init__(self):
|
|
57
|
+
if not self.source_id:
|
|
58
|
+
import uuid
|
|
59
|
+
|
|
60
|
+
self.source_id = str(uuid.uuid4())
|
|
61
|
+
self.relevance_score = max(0.0, min(1.0, self.relevance_score))
|
|
62
|
+
self.freshness_score = max(0.0, min(1.0, self.freshness_score))
|
|
63
|
+
|
|
64
|
+
def quality_score(self) -> float:
|
|
65
|
+
"""Combined relevance and freshness score."""
|
|
66
|
+
return self.relevance_score * self.freshness_score
|
|
67
|
+
|
|
68
|
+
def with_freshness(self, score: float) -> "DiscoveredSource":
|
|
69
|
+
"""Set freshness score."""
|
|
70
|
+
self.freshness_score = max(0.0, min(1.0, score))
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Discovery:
|
|
76
|
+
"""Discovery result for a query."""
|
|
77
|
+
|
|
78
|
+
query_id: str
|
|
79
|
+
discovered_sources: List[DiscoveredSource] = field(default_factory=list)
|
|
80
|
+
discovery_id: str = ""
|
|
81
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
82
|
+
|
|
83
|
+
def __post_init__(self):
|
|
84
|
+
if not self.discovery_id:
|
|
85
|
+
import uuid
|
|
86
|
+
|
|
87
|
+
self.discovery_id = str(uuid.uuid4())
|
|
88
|
+
|
|
89
|
+
def add_source(self, source: DiscoveredSource) -> "Discovery":
|
|
90
|
+
"""Add a discovered source."""
|
|
91
|
+
self.discovered_sources.append(source)
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def total_available_tokens(self) -> int:
|
|
95
|
+
"""Total tokens across all sources."""
|
|
96
|
+
return sum(s.estimated_tokens for s in self.discovered_sources)
|
|
97
|
+
|
|
98
|
+
def top_sources(self, limit: int = 5) -> List[DiscoveredSource]:
|
|
99
|
+
"""Get top sources by quality score."""
|
|
100
|
+
sorted_sources = sorted(
|
|
101
|
+
self.discovered_sources, key=lambda s: s.quality_score(), reverse=True
|
|
102
|
+
)
|
|
103
|
+
return sorted_sources[:limit]
|
|
104
|
+
|
|
105
|
+
def high_relevance_sources(self, threshold: float = 0.8) -> List[DiscoveredSource]:
|
|
106
|
+
"""Get high-relevance sources."""
|
|
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]
|