PyStreamMCP 3.3.0__py3-none-any.whl → 3.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pystreammcp/__init__.py +1 -1
- pystreammcp/cli.py +11 -160
- pystreammcp/cli_daemon.py +14 -2
- pystreammcp/integrations/langchain.py +5 -1
- pystreammcp/integrations/llamaindex.py +4 -1
- pystreammcp/integrations/semantic_kernel.py +4 -1
- 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/webhook_handlers.py +30 -30
- pystreammcp/webhook_router.py +8 -8
- pystreammcp-3.3.2.dist-info/METADATA +266 -0
- {pystreammcp-3.3.0.dist-info → pystreammcp-3.3.2.dist-info}/RECORD +20 -19
- {pystreammcp-3.3.0.dist-info → pystreammcp-3.3.2.dist-info}/WHEEL +1 -1
- pystreammcp-3.3.2.dist-info/entry_points.txt +2 -0
- pystreammcp-3.3.2.dist-info/licenses/LICENSE +202 -0
- pystreammcp-3.3.0.dist-info/METADATA +0 -305
- pystreammcp-3.3.0.dist-info/licenses/LICENSE +0 -22
- {pystreammcp-3.3.0.dist-info → pystreammcp-3.3.2.dist-info}/top_level.txt +0 -0
pystreammcp/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ 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.3.
|
|
10
|
+
__version__ = "3.3.2"
|
|
11
11
|
|
|
12
12
|
from .agent import Agent, QueryResult
|
|
13
13
|
from .query import Query, QueryIntent, QueryConstraints
|
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/cli_daemon.py
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
"""Dashboard daemon - runs in background, restarts on close"""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
3
5
|
import subprocess
|
|
4
6
|
import time
|
|
5
7
|
import signal
|
|
6
8
|
import sys
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
|
|
11
|
+
_PACKAGE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
class DashboardDaemon:
|
|
11
15
|
def __init__(self, package_name: str, dashboard_cmd: str):
|
|
16
|
+
if not isinstance(package_name, str) or not _PACKAGE_NAME_RE.match(
|
|
17
|
+
package_name
|
|
18
|
+
):
|
|
19
|
+
raise ValueError(f"Invalid package name: {package_name!r}")
|
|
12
20
|
self.package_name = package_name
|
|
13
|
-
|
|
21
|
+
# Split into argv and run without a shell so that a caller-supplied
|
|
22
|
+
# command cannot inject shell metacharacters.
|
|
23
|
+
self.dashboard_cmd = shlex.split(dashboard_cmd)
|
|
24
|
+
if not self.dashboard_cmd:
|
|
25
|
+
raise ValueError("dashboard_cmd must not be empty")
|
|
14
26
|
self.process = None
|
|
15
27
|
self.running = True
|
|
16
28
|
|
|
@@ -27,7 +39,7 @@ class DashboardDaemon:
|
|
|
27
39
|
|
|
28
40
|
self.process = subprocess.Popen(
|
|
29
41
|
self.dashboard_cmd,
|
|
30
|
-
shell=
|
|
42
|
+
shell=False,
|
|
31
43
|
stdout=subprocess.PIPE,
|
|
32
44
|
stderr=subprocess.PIPE,
|
|
33
45
|
)
|
|
@@ -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,
|
|
@@ -23,6 +23,10 @@ from pystreammcp import (
|
|
|
23
23
|
from pystreammcp.adapters import QueryResult as AdapterQueryResult
|
|
24
24
|
from pystreammcp.discovery import SourceRegistry
|
|
25
25
|
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from langchain_core.tools import Tool as LangchainTool
|
|
28
|
+
from langchain_core.retrievers import BaseRetriever
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
class LangchainAdapter(AgentFrameworkAdapter):
|
|
28
32
|
"""Langchain adapter using new AgentFrameworkAdapter base class.
|
|
@@ -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.
|
pystreammcp/multi_agent.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Multi-agent context sharing and collaboration for PyStreamMCP."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime, timezone
|
|
3
4
|
from typing import Dict, List, Optional
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from .context import Context
|
|
@@ -46,7 +47,7 @@ class ContextHub:
|
|
|
46
47
|
optimized_context=context,
|
|
47
48
|
usage_count=1,
|
|
48
49
|
agents_using=[],
|
|
49
|
-
created_at=
|
|
50
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
50
51
|
cost_savings=cost_savings,
|
|
51
52
|
)
|
|
52
53
|
self.shared_contexts[context_id] = shared
|
|
@@ -8,6 +8,7 @@ from typing import Dict, Any, Optional
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
|
|
10
10
|
from pystreammcp import Agent
|
|
11
|
+
from pystreammcp.discovery import SourceRegistry
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass
|
|
@@ -110,30 +111,38 @@ class PyStreamMCPDiscoveryOperator:
|
|
|
110
111
|
agent_id: str,
|
|
111
112
|
context_text: str,
|
|
112
113
|
max_sources: int = 10,
|
|
114
|
+
registry: Optional[SourceRegistry] = None,
|
|
113
115
|
**kwargs,
|
|
114
116
|
):
|
|
115
|
-
"""Initialize discovery operator.
|
|
117
|
+
"""Initialize discovery operator.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
registry: Optional shared SourceRegistry sources were
|
|
121
|
+
registered on elsewhere (e.g. via the FastAPI `/sources`
|
|
122
|
+
endpoint or another operator sharing the same registry).
|
|
123
|
+
Defaults to a fresh, empty registry.
|
|
124
|
+
"""
|
|
116
125
|
self.task_id = task_id
|
|
117
126
|
self.agent_id = agent_id
|
|
118
127
|
self.context_text = context_text
|
|
119
128
|
self.max_sources = max_sources
|
|
129
|
+
self.registry = registry if registry is not None else SourceRegistry()
|
|
120
130
|
self.kwargs = kwargs
|
|
121
131
|
|
|
122
132
|
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
123
|
-
"""Execute discovery.
|
|
133
|
+
"""Execute discovery using the real SourceRegistry.
|
|
134
|
+
|
|
135
|
+
Uses the same relevance ranking as the REST/MCP/LangChain
|
|
136
|
+
surfaces instead of fabricating `source_0..source_4` results. An
|
|
137
|
+
operator given no registry (or one with nothing registered)
|
|
138
|
+
honestly returns zero sources rather than padding in fake ones.
|
|
139
|
+
"""
|
|
140
|
+
sources = self.registry.discover(self.context_text, limit=self.max_sources)
|
|
124
141
|
return {
|
|
125
142
|
"task_id": self.task_id,
|
|
126
143
|
"context": self.context_text,
|
|
127
|
-
"sources":
|
|
128
|
-
|
|
129
|
-
"name": f"source_{i}",
|
|
130
|
-
"relevance": 0.95 - (i * 0.05),
|
|
131
|
-
"type": "database",
|
|
132
|
-
"estimated_tokens": 500 + (i * 100),
|
|
133
|
-
}
|
|
134
|
-
for i in range(min(self.max_sources, 5))
|
|
135
|
-
],
|
|
136
|
-
"total_sources": min(self.max_sources, 5),
|
|
144
|
+
"sources": sources,
|
|
145
|
+
"total_sources": len(sources),
|
|
137
146
|
"timestamp": str(context.get("execution_date", "")),
|
|
138
147
|
}
|
|
139
148
|
|
|
@@ -8,6 +8,7 @@ from typing import Dict, Any, Optional
|
|
|
8
8
|
import json
|
|
9
9
|
|
|
10
10
|
from pystreammcp import Agent
|
|
11
|
+
from pystreammcp.discovery import SourceRegistry
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class N8nWebhookTrigger:
|
|
@@ -22,13 +23,17 @@ class N8nWebhookTrigger:
|
|
|
22
23
|
3. Add query in body: {"text": "your query", "agent_id": "agent_1"}
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
|
-
def __init__(self, agent_id: str = "n8n_agent"):
|
|
26
|
+
def __init__(self, agent_id: str = "n8n_agent", registry: Optional[SourceRegistry] = None):
|
|
26
27
|
"""Initialize webhook trigger.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
29
30
|
agent_id: PyStreamMCP agent ID
|
|
31
|
+
registry: Optional shared SourceRegistry sources were
|
|
32
|
+
registered on elsewhere (e.g. via the FastAPI `/sources`
|
|
33
|
+
endpoint). Defaults to a fresh, empty registry.
|
|
30
34
|
"""
|
|
31
35
|
self.agent_id = agent_id
|
|
36
|
+
self.registry = registry if registry is not None else SourceRegistry()
|
|
32
37
|
|
|
33
38
|
def handle_query(self, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
34
39
|
"""Handle incoming webhook request.
|
|
@@ -61,23 +66,22 @@ class N8nWebhookTrigger:
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
def handle_discovery(self, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
|
-
"""Handle discovery webhook.
|
|
69
|
+
"""Handle discovery webhook.
|
|
70
|
+
|
|
71
|
+
Uses the real `SourceRegistry.discover()` instead of fabricating
|
|
72
|
+
`source_0..source_4` results. With nothing registered on this
|
|
73
|
+
trigger's registry, honestly returns zero sources.
|
|
74
|
+
"""
|
|
65
75
|
context = body.get("context", "")
|
|
66
76
|
|
|
67
77
|
if not context:
|
|
68
78
|
return {"error": "Missing 'context' parameter"}
|
|
69
79
|
|
|
80
|
+
sources = self.registry.discover(context)
|
|
70
81
|
return {
|
|
71
82
|
"context": context,
|
|
72
|
-
"sources":
|
|
73
|
-
|
|
74
|
-
"name": f"source_{i}",
|
|
75
|
-
"relevance": 0.95 - (i * 0.05),
|
|
76
|
-
"type": "data_source",
|
|
77
|
-
}
|
|
78
|
-
for i in range(5)
|
|
79
|
-
],
|
|
80
|
-
"total_sources": 5,
|
|
83
|
+
"sources": sources,
|
|
84
|
+
"total_sources": len(sources),
|
|
81
85
|
"success": True,
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -8,6 +8,7 @@ from typing import Dict, Any, Optional
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
|
|
10
10
|
from pystreammcp import Agent
|
|
11
|
+
from pystreammcp.discovery import SourceRegistry
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass
|
|
@@ -55,23 +56,26 @@ class TemporalDiscoveryActivity:
|
|
|
55
56
|
agent_id: str
|
|
56
57
|
context: str
|
|
57
58
|
max_sources: int = 10
|
|
59
|
+
registry: Optional[SourceRegistry] = None
|
|
58
60
|
|
|
59
61
|
def execute(self) -> Dict[str, Any]:
|
|
60
|
-
"""Execute discovery as a durable activity.
|
|
61
|
-
|
|
62
|
+
"""Execute discovery as a durable activity.
|
|
63
|
+
|
|
64
|
+
Uses the real `SourceRegistry.discover()` -- the same relevance
|
|
65
|
+
ranking used by the REST/MCP/LangChain surfaces -- instead of
|
|
66
|
+
fabricating `source_0..source_4` results. Pass `registry=` a
|
|
67
|
+
shared `SourceRegistry` instance (the one sources were registered
|
|
68
|
+
on elsewhere) so this activity can actually find them; an
|
|
69
|
+
activity given no registry gets a fresh, empty one and will
|
|
70
|
+
honestly report zero sources rather than padding in fake ones.
|
|
71
|
+
"""
|
|
72
|
+
registry = self.registry if self.registry is not None else SourceRegistry()
|
|
73
|
+
sources = registry.discover(self.context, limit=self.max_sources)
|
|
62
74
|
|
|
63
|
-
# In production, use actual discovery logic
|
|
64
75
|
return {
|
|
65
76
|
"context": self.context,
|
|
66
|
-
"sources":
|
|
67
|
-
|
|
68
|
-
"name": f"source_{i}",
|
|
69
|
-
"relevance": 0.95 - (i * 0.05),
|
|
70
|
-
"type": "database",
|
|
71
|
-
}
|
|
72
|
-
for i in range(min(self.max_sources, 5))
|
|
73
|
-
],
|
|
74
|
-
"total_sources": min(self.max_sources, 5),
|
|
77
|
+
"sources": sources,
|
|
78
|
+
"total_sources": len(sources),
|
|
75
79
|
"status": "completed",
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -87,15 +91,21 @@ class TemporalWorkflow:
|
|
|
87
91
|
5. Handle failures with retries
|
|
88
92
|
"""
|
|
89
93
|
|
|
90
|
-
def __init__(
|
|
94
|
+
def __init__(
|
|
95
|
+
self, workflow_id: str, agent_id: str, registry: Optional[SourceRegistry] = None
|
|
96
|
+
):
|
|
91
97
|
"""Initialize workflow.
|
|
92
98
|
|
|
93
99
|
Args:
|
|
94
100
|
workflow_id: Unique workflow identifier
|
|
95
101
|
agent_id: PyStreamMCP agent ID
|
|
102
|
+
registry: Optional shared SourceRegistry sources were
|
|
103
|
+
registered on elsewhere (e.g. via the FastAPI `/sources`
|
|
104
|
+
endpoint). Defaults to a fresh, empty registry.
|
|
96
105
|
"""
|
|
97
106
|
self.workflow_id = workflow_id
|
|
98
107
|
self.agent_id = agent_id
|
|
108
|
+
self.registry = registry if registry is not None else SourceRegistry()
|
|
99
109
|
|
|
100
110
|
def discover_sources(self, context: str) -> Dict[str, Any]:
|
|
101
111
|
"""Activity: Discover relevant sources.
|
|
@@ -104,11 +114,11 @@ class TemporalWorkflow:
|
|
|
104
114
|
```python
|
|
105
115
|
sources = await workflow.execute_activity(
|
|
106
116
|
TemporalDiscoveryActivity,
|
|
107
|
-
TemporalDiscoveryActivity(self.agent_id, context),
|
|
117
|
+
TemporalDiscoveryActivity(self.agent_id, context, registry=self.registry),
|
|
108
118
|
)
|
|
109
119
|
```
|
|
110
120
|
"""
|
|
111
|
-
activity = TemporalDiscoveryActivity(self.agent_id, context)
|
|
121
|
+
activity = TemporalDiscoveryActivity(self.agent_id, context, registry=self.registry)
|
|
112
122
|
return activity.execute()
|
|
113
123
|
|
|
114
124
|
def optimize_query(self, query_text: str) -> Dict[str, Any]:
|
pystreammcp/quality.py
CHANGED
|
@@ -6,7 +6,7 @@ Ensures all context included in queries has passed data quality checks.
|
|
|
6
6
|
|
|
7
7
|
from typing import Optional, List, Dict, Any
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from datetime import datetime, timedelta
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
10
|
from enum import Enum
|
|
11
11
|
import logging
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ class QualityCheck:
|
|
|
31
31
|
passed: bool
|
|
32
32
|
score: float # 0.0-1.0
|
|
33
33
|
message: str = ""
|
|
34
|
-
checked_at: datetime = field(default_factory=datetime.
|
|
34
|
+
checked_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
35
35
|
|
|
36
36
|
def __post_init__(self):
|
|
37
37
|
self.score = max(0.0, min(1.0, self.score))
|
|
@@ -46,7 +46,7 @@ class ValidationResult:
|
|
|
46
46
|
quality_score: float # 0.0-1.0
|
|
47
47
|
checks: List[QualityCheck] = field(default_factory=list)
|
|
48
48
|
errors: List[str] = field(default_factory=list)
|
|
49
|
-
last_validated: datetime = field(default_factory=datetime.
|
|
49
|
+
last_validated: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
50
50
|
validation_id: str = ""
|
|
51
51
|
|
|
52
52
|
def __post_init__(self):
|
|
@@ -66,7 +66,7 @@ class ValidationResult:
|
|
|
66
66
|
return True
|
|
67
67
|
|
|
68
68
|
if self.status == QualityStatus.STALE and max_staleness_seconds:
|
|
69
|
-
age = (datetime.
|
|
69
|
+
age = (datetime.now(timezone.utc) - self.last_validated).total_seconds()
|
|
70
70
|
return age <= max_staleness_seconds
|
|
71
71
|
|
|
72
72
|
if self.status == QualityStatus.DEGRADED:
|
|
@@ -148,7 +148,7 @@ class QualityValidator:
|
|
|
148
148
|
# Check cache first
|
|
149
149
|
cached = self._validation_cache.get(dataset_id)
|
|
150
150
|
if cached:
|
|
151
|
-
age = (datetime.
|
|
151
|
+
age = (datetime.now(timezone.utc) - cached.last_validated).total_seconds()
|
|
152
152
|
if age < gate.max_staleness_seconds:
|
|
153
153
|
logger.debug(f"Using cached validation for {dataset_id}")
|
|
154
154
|
return cached
|
|
@@ -379,7 +379,7 @@ class QualityValidator:
|
|
|
379
379
|
"status": result.status,
|
|
380
380
|
"quality_score": result.quality_score,
|
|
381
381
|
"age_seconds": (
|
|
382
|
-
datetime.
|
|
382
|
+
datetime.now(timezone.utc) - result.last_validated
|
|
383
383
|
).total_seconds(),
|
|
384
384
|
}
|
|
385
385
|
for dataset_id, result in self._validation_cache.items()
|
pystreammcp/server.py
CHANGED
|
@@ -5,7 +5,7 @@ import hashlib
|
|
|
5
5
|
import hmac
|
|
6
6
|
import os
|
|
7
7
|
from typing import Dict, Any, Optional, List, Tuple
|
|
8
|
-
from datetime import datetime
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
9
|
from pystreammcp import Agent
|
|
10
10
|
from .webhook_router import EventRouter, MCPEndpoint, Tool
|
|
11
11
|
from .webhook_handlers import OrchestrationWebhookHandlers
|
|
@@ -194,7 +194,7 @@ class PyStreamMCPServer:
|
|
|
194
194
|
"secret_key": secret_key,
|
|
195
195
|
"headers": headers or {},
|
|
196
196
|
"active": True,
|
|
197
|
-
"created_at": datetime.
|
|
197
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
198
198
|
}
|
|
199
199
|
return {
|
|
200
200
|
"status": "success",
|
|
@@ -342,7 +342,7 @@ class PyStreamMCPServer:
|
|
|
342
342
|
"status": "success",
|
|
343
343
|
"orchestration": status,
|
|
344
344
|
"webhooks": len(self.webhook_configs),
|
|
345
|
-
"timestamp": datetime.
|
|
345
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
|