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 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.0"
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
- main()
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
- self.dashboard_cmd = dashboard_cmd
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=True,
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.
@@ -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=__import__("datetime").datetime.utcnow().isoformat(),
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
- agent = Agent(agent_id=self.agent_id)
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__(self, workflow_id: str, agent_id: str):
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.utcnow)
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.utcnow)
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.utcnow() - self.last_validated).total_seconds()
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.utcnow() - cached.last_validated).total_seconds()
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.utcnow() - result.last_validated
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.utcnow().isoformat(),
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.utcnow().isoformat(),
345
+ "timestamp": datetime.now(timezone.utc).isoformat(),
346
346
  }
347
347
 
348
348