PyStreamMCP 0.3.0__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 +24 -0
- pystreammcp/api.py +273 -0
- pystreammcp/cli.py +385 -0
- pystreammcp/core.py +567 -0
- pystreammcp/persistence.py +276 -0
- pystreammcp/routing.py +239 -0
- pystreammcp-0.3.0.dist-info/METADATA +324 -0
- pystreammcp-0.3.0.dist-info/RECORD +12 -0
- pystreammcp-0.3.0.dist-info/WHEEL +5 -0
- pystreammcp-0.3.0.dist-info/entry_points.txt +2 -0
- pystreammcp-0.3.0.dist-info/licenses/LICENSE +21 -0
- pystreammcp-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""SQLite persistence layer for PyStreamMCP metadata."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pystreammcp.core import Metadata, Capability, CapabilityClassification
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SqlitePersistence:
|
|
12
|
+
"""SQLite-based persistence for metadata indexing."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, path: str = ":memory:"):
|
|
15
|
+
"""Initialize SQLite store."""
|
|
16
|
+
self.path = path
|
|
17
|
+
self.conn = sqlite3.connect(path, check_same_thread=False, timeout=30.0)
|
|
18
|
+
self.conn.row_factory = sqlite3.Row
|
|
19
|
+
self._init_schema()
|
|
20
|
+
|
|
21
|
+
def _init_schema(self) -> None:
|
|
22
|
+
"""Initialize database schema."""
|
|
23
|
+
self.conn.executescript(
|
|
24
|
+
"""
|
|
25
|
+
PRAGMA journal_mode = WAL;
|
|
26
|
+
PRAGMA synchronous = NORMAL;
|
|
27
|
+
PRAGMA temp_store = MEMORY;
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
description TEXT,
|
|
34
|
+
server_id TEXT NOT NULL,
|
|
35
|
+
tool_id TEXT NOT NULL,
|
|
36
|
+
input_schema TEXT,
|
|
37
|
+
output_schema TEXT,
|
|
38
|
+
tags TEXT,
|
|
39
|
+
created_at TEXT,
|
|
40
|
+
updated_at TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS capabilities (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
metadata_id TEXT NOT NULL,
|
|
46
|
+
classification TEXT NOT NULL,
|
|
47
|
+
inputs TEXT,
|
|
48
|
+
outputs TEXT,
|
|
49
|
+
cost_estimate REAL,
|
|
50
|
+
latency_estimate_ms INTEGER,
|
|
51
|
+
reliability_score REAL,
|
|
52
|
+
FOREIGN KEY(metadata_id) REFERENCES metadata(id)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS schemas (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
metadata_id TEXT NOT NULL,
|
|
58
|
+
base_type TEXT NOT NULL,
|
|
59
|
+
properties TEXT,
|
|
60
|
+
required_fields TEXT,
|
|
61
|
+
is_array BOOLEAN,
|
|
62
|
+
extraction_confidence REAL,
|
|
63
|
+
FOREIGN KEY(metadata_id) REFERENCES metadata(id)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS search_index (
|
|
67
|
+
metadata_id TEXT PRIMARY KEY,
|
|
68
|
+
name_lower TEXT,
|
|
69
|
+
description_lower TEXT,
|
|
70
|
+
all_text TEXT,
|
|
71
|
+
FOREIGN KEY(metadata_id) REFERENCES metadata(id)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_server_id ON metadata(server_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_tool_id ON metadata(tool_id);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_capability_classification ON capabilities(classification);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_capability_metadata ON capabilities(metadata_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_schema_metadata ON schemas(metadata_id);
|
|
79
|
+
"""
|
|
80
|
+
)
|
|
81
|
+
self.conn.commit()
|
|
82
|
+
|
|
83
|
+
def insert_metadata(self, metadata: Metadata) -> None:
|
|
84
|
+
"""Insert or update metadata."""
|
|
85
|
+
cursor = self.conn.cursor()
|
|
86
|
+
cursor.execute(
|
|
87
|
+
"""INSERT OR REPLACE INTO metadata
|
|
88
|
+
(id, name, description, server_id, tool_id, input_schema, output_schema, tags, created_at, updated_at)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
90
|
+
(
|
|
91
|
+
metadata.id,
|
|
92
|
+
metadata.name,
|
|
93
|
+
metadata.description,
|
|
94
|
+
metadata.server_id,
|
|
95
|
+
metadata.tool_id,
|
|
96
|
+
json.dumps(metadata.input_schema),
|
|
97
|
+
json.dumps(metadata.output_schema),
|
|
98
|
+
",".join(metadata.tags),
|
|
99
|
+
metadata.created_at,
|
|
100
|
+
metadata.updated_at,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Update search index
|
|
105
|
+
cursor.execute(
|
|
106
|
+
"""INSERT OR REPLACE INTO search_index
|
|
107
|
+
(metadata_id, name_lower, description_lower, all_text)
|
|
108
|
+
VALUES (?, ?, ?, ?)""",
|
|
109
|
+
(
|
|
110
|
+
metadata.id,
|
|
111
|
+
metadata.name.lower(),
|
|
112
|
+
metadata.description.lower(),
|
|
113
|
+
f"{metadata.name} {metadata.description} {' '.join(metadata.tags)}",
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
self.conn.commit()
|
|
117
|
+
|
|
118
|
+
def get_metadata(self, id: str) -> Optional[Metadata]:
|
|
119
|
+
"""Get metadata by ID."""
|
|
120
|
+
cursor = self.conn.cursor()
|
|
121
|
+
cursor.execute(
|
|
122
|
+
"""SELECT id, name, description, server_id, tool_id, input_schema, output_schema, tags, created_at, updated_at
|
|
123
|
+
FROM metadata WHERE id = ?""",
|
|
124
|
+
(id,),
|
|
125
|
+
)
|
|
126
|
+
row = cursor.fetchone()
|
|
127
|
+
if row:
|
|
128
|
+
return Metadata(
|
|
129
|
+
id=row["id"],
|
|
130
|
+
name=row["name"],
|
|
131
|
+
description=row["description"],
|
|
132
|
+
server_id=row["server_id"],
|
|
133
|
+
tool_id=row["tool_id"],
|
|
134
|
+
input_schema=json.loads(row["input_schema"]),
|
|
135
|
+
output_schema=json.loads(row["output_schema"]),
|
|
136
|
+
tags=row["tags"].split(",") if row["tags"] else [],
|
|
137
|
+
created_at=row["created_at"],
|
|
138
|
+
updated_at=row["updated_at"],
|
|
139
|
+
)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def search_metadata(self, query: str, limit: int = 100) -> List[Metadata]:
|
|
143
|
+
"""Search metadata by name/description."""
|
|
144
|
+
cursor = self.conn.cursor()
|
|
145
|
+
pattern = f"%{query.lower()}%"
|
|
146
|
+
cursor.execute(
|
|
147
|
+
"""SELECT m.id, m.name, m.description, m.server_id, m.tool_id,
|
|
148
|
+
m.input_schema, m.output_schema, m.tags, m.created_at, m.updated_at
|
|
149
|
+
FROM metadata m
|
|
150
|
+
INNER JOIN search_index s ON m.id = s.metadata_id
|
|
151
|
+
WHERE s.name_lower LIKE ? OR s.description_lower LIKE ?
|
|
152
|
+
LIMIT ?""",
|
|
153
|
+
(pattern, pattern, limit),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
results = []
|
|
157
|
+
for row in cursor.fetchall():
|
|
158
|
+
results.append(
|
|
159
|
+
Metadata(
|
|
160
|
+
id=row["id"],
|
|
161
|
+
name=row["name"],
|
|
162
|
+
description=row["description"],
|
|
163
|
+
server_id=row["server_id"],
|
|
164
|
+
tool_id=row["tool_id"],
|
|
165
|
+
input_schema=json.loads(row["input_schema"]),
|
|
166
|
+
output_schema=json.loads(row["output_schema"]),
|
|
167
|
+
tags=row["tags"].split(",") if row["tags"] else [],
|
|
168
|
+
created_at=row["created_at"],
|
|
169
|
+
updated_at=row["updated_at"],
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return results
|
|
173
|
+
|
|
174
|
+
def get_by_server(self, server_id: str) -> List[Metadata]:
|
|
175
|
+
"""Get all metadata for a server."""
|
|
176
|
+
cursor = self.conn.cursor()
|
|
177
|
+
cursor.execute(
|
|
178
|
+
"""SELECT id, name, description, server_id, tool_id, input_schema, output_schema, tags, created_at, updated_at
|
|
179
|
+
FROM metadata WHERE server_id = ?""",
|
|
180
|
+
(server_id,),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
results = []
|
|
184
|
+
for row in cursor.fetchall():
|
|
185
|
+
results.append(
|
|
186
|
+
Metadata(
|
|
187
|
+
id=row["id"],
|
|
188
|
+
name=row["name"],
|
|
189
|
+
description=row["description"],
|
|
190
|
+
server_id=row["server_id"],
|
|
191
|
+
tool_id=row["tool_id"],
|
|
192
|
+
input_schema=json.loads(row["input_schema"]),
|
|
193
|
+
output_schema=json.loads(row["output_schema"]),
|
|
194
|
+
tags=row["tags"].split(",") if row["tags"] else [],
|
|
195
|
+
created_at=row["created_at"],
|
|
196
|
+
updated_at=row["updated_at"],
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
def insert_capability(self, capability: Capability) -> None:
|
|
202
|
+
"""Insert or update capability."""
|
|
203
|
+
cursor = self.conn.cursor()
|
|
204
|
+
cursor.execute(
|
|
205
|
+
"""INSERT OR REPLACE INTO capabilities
|
|
206
|
+
(id, metadata_id, classification, inputs, outputs, cost_estimate, latency_estimate_ms, reliability_score)
|
|
207
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
208
|
+
(
|
|
209
|
+
capability.id,
|
|
210
|
+
capability.metadata_id,
|
|
211
|
+
capability.classification.value,
|
|
212
|
+
",".join(capability.inputs),
|
|
213
|
+
",".join(capability.outputs),
|
|
214
|
+
capability.cost_estimate,
|
|
215
|
+
capability.latency_estimate_ms,
|
|
216
|
+
capability.reliability_score,
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
self.conn.commit()
|
|
220
|
+
|
|
221
|
+
def get_capabilities_by_classification(
|
|
222
|
+
self, classification: CapabilityClassification
|
|
223
|
+
) -> List[Capability]:
|
|
224
|
+
"""Get capabilities by classification."""
|
|
225
|
+
cursor = self.conn.cursor()
|
|
226
|
+
cursor.execute(
|
|
227
|
+
"""SELECT id, metadata_id, classification, inputs, outputs, cost_estimate, latency_estimate_ms, reliability_score
|
|
228
|
+
FROM capabilities WHERE classification = ?""",
|
|
229
|
+
(classification.value,),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
results = []
|
|
233
|
+
for row in cursor.fetchall():
|
|
234
|
+
results.append(
|
|
235
|
+
Capability(
|
|
236
|
+
id=row["id"],
|
|
237
|
+
metadata_id=row["metadata_id"],
|
|
238
|
+
classification=CapabilityClassification(row["classification"]),
|
|
239
|
+
inputs=[s for s in row["inputs"].split(",") if s],
|
|
240
|
+
outputs=[s for s in row["outputs"].split(",") if s],
|
|
241
|
+
cost_estimate=row["cost_estimate"],
|
|
242
|
+
latency_estimate_ms=row["latency_estimate_ms"],
|
|
243
|
+
reliability_score=row["reliability_score"],
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
return results
|
|
247
|
+
|
|
248
|
+
def count_metadata(self) -> int:
|
|
249
|
+
"""Get count of metadata entries."""
|
|
250
|
+
cursor = self.conn.cursor()
|
|
251
|
+
cursor.execute("SELECT COUNT(*) as count FROM metadata")
|
|
252
|
+
return cursor.fetchone()["count"]
|
|
253
|
+
|
|
254
|
+
def clear(self) -> None:
|
|
255
|
+
"""Clear all data."""
|
|
256
|
+
self.conn.executescript(
|
|
257
|
+
"""
|
|
258
|
+
DELETE FROM capabilities;
|
|
259
|
+
DELETE FROM schemas;
|
|
260
|
+
DELETE FROM search_index;
|
|
261
|
+
DELETE FROM metadata;
|
|
262
|
+
"""
|
|
263
|
+
)
|
|
264
|
+
self.conn.commit()
|
|
265
|
+
|
|
266
|
+
def close(self) -> None:
|
|
267
|
+
"""Close database connection."""
|
|
268
|
+
self.conn.close()
|
|
269
|
+
|
|
270
|
+
def __enter__(self):
|
|
271
|
+
"""Context manager entry."""
|
|
272
|
+
return self
|
|
273
|
+
|
|
274
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
275
|
+
"""Context manager exit."""
|
|
276
|
+
self.close()
|
pystreammcp/routing.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Semantic routing engine for intelligent tool selection."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List
|
|
5
|
+
from pystreammcp.core import Metadata, Capability, CapabilityClassification
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class RoutingScore:
|
|
10
|
+
"""Score for routing a query to a tool."""
|
|
11
|
+
|
|
12
|
+
metadata_id: str
|
|
13
|
+
tool_name: str
|
|
14
|
+
server_id: str
|
|
15
|
+
relevance_score: float
|
|
16
|
+
capability_match: float
|
|
17
|
+
schema_match: float
|
|
18
|
+
cost_efficiency: float
|
|
19
|
+
overall_score: float
|
|
20
|
+
|
|
21
|
+
def __lt__(self, other):
|
|
22
|
+
"""Sort by overall_score descending."""
|
|
23
|
+
return self.overall_score > other.overall_score
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SemanticRouter:
|
|
27
|
+
"""Route queries to the most relevant tools."""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def score_tool(
|
|
31
|
+
query: str, metadata: Metadata, capabilities: List[Capability]
|
|
32
|
+
) -> RoutingScore:
|
|
33
|
+
"""Score a tool for a given query."""
|
|
34
|
+
relevance = SemanticRouter._calculate_relevance(query, metadata)
|
|
35
|
+
capability_match = SemanticRouter._calculate_capability_match(query, capabilities)
|
|
36
|
+
schema_match = SemanticRouter._calculate_schema_match(query, metadata)
|
|
37
|
+
cost_efficiency = SemanticRouter._calculate_cost_efficiency(capabilities)
|
|
38
|
+
|
|
39
|
+
overall_score = (
|
|
40
|
+
(relevance * 0.4)
|
|
41
|
+
+ (capability_match * 0.35)
|
|
42
|
+
+ (schema_match * 0.15)
|
|
43
|
+
+ (cost_efficiency * 0.1)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return RoutingScore(
|
|
47
|
+
metadata_id=metadata.id,
|
|
48
|
+
tool_name=metadata.name,
|
|
49
|
+
server_id=metadata.server_id,
|
|
50
|
+
relevance_score=relevance,
|
|
51
|
+
capability_match=capability_match,
|
|
52
|
+
schema_match=schema_match,
|
|
53
|
+
cost_efficiency=cost_efficiency,
|
|
54
|
+
overall_score=min(max(overall_score, 0.0), 1.0),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def rank_tools(
|
|
59
|
+
query: str, metadata_list: List[Metadata], capabilities_map: dict
|
|
60
|
+
) -> List[RoutingScore]:
|
|
61
|
+
"""Rank all tools for a query."""
|
|
62
|
+
scores = []
|
|
63
|
+
for metadata in metadata_list:
|
|
64
|
+
caps = capabilities_map.get(metadata.id, [])
|
|
65
|
+
score = SemanticRouter.score_tool(query, metadata, caps)
|
|
66
|
+
scores.append(score)
|
|
67
|
+
|
|
68
|
+
scores.sort()
|
|
69
|
+
return scores
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def select_best_tools(
|
|
73
|
+
scores: List[RoutingScore], threshold: float = 0.7, max_count: int = 5
|
|
74
|
+
) -> List[RoutingScore]:
|
|
75
|
+
"""Select best tools above threshold."""
|
|
76
|
+
return [s for s in scores if s.overall_score >= threshold][:max_count]
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _calculate_relevance(query: str, metadata: Metadata) -> float:
|
|
80
|
+
"""Calculate relevance score."""
|
|
81
|
+
query_lower = query.lower()
|
|
82
|
+
name_lower = metadata.name.lower()
|
|
83
|
+
desc_lower = metadata.description.lower()
|
|
84
|
+
|
|
85
|
+
relevance = 0.0
|
|
86
|
+
|
|
87
|
+
# Exact name match
|
|
88
|
+
if query_lower in name_lower:
|
|
89
|
+
relevance += 0.8
|
|
90
|
+
|
|
91
|
+
# Word-level matches in name
|
|
92
|
+
for word in query_lower.split():
|
|
93
|
+
if len(word) > 2 and word in name_lower:
|
|
94
|
+
relevance += 0.3
|
|
95
|
+
|
|
96
|
+
# Description matches
|
|
97
|
+
desc_matches = sum(
|
|
98
|
+
1 for word in query_lower.split() if len(word) > 2 and word in desc_lower
|
|
99
|
+
)
|
|
100
|
+
relevance += desc_matches * 0.15
|
|
101
|
+
|
|
102
|
+
# Tag matches
|
|
103
|
+
tag_matches = sum(
|
|
104
|
+
1 for tag in metadata.tags if query_lower in tag.lower()
|
|
105
|
+
)
|
|
106
|
+
relevance += tag_matches * 0.2
|
|
107
|
+
|
|
108
|
+
return min(relevance, 1.0)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _calculate_capability_match(
|
|
112
|
+
query: str, capabilities: List[Capability]
|
|
113
|
+
) -> float:
|
|
114
|
+
"""Calculate capability match score."""
|
|
115
|
+
query_lower = query.lower()
|
|
116
|
+
match_score = 0.0
|
|
117
|
+
|
|
118
|
+
classifications = [
|
|
119
|
+
(CapabilityClassification.Search, "search"),
|
|
120
|
+
(CapabilityClassification.Read, "read"),
|
|
121
|
+
(CapabilityClassification.Write, "write"),
|
|
122
|
+
(CapabilityClassification.Compute, "compute"),
|
|
123
|
+
(CapabilityClassification.Transform, "transform"),
|
|
124
|
+
(CapabilityClassification.Aggregate, "aggregate"),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
for cap in capabilities:
|
|
128
|
+
for class_type, keyword in classifications:
|
|
129
|
+
if cap.classification == class_type and keyword in query_lower:
|
|
130
|
+
match_score += 0.6
|
|
131
|
+
|
|
132
|
+
# Check for input/output parameter matches
|
|
133
|
+
for input_param in cap.inputs:
|
|
134
|
+
if input_param.lower() in query_lower:
|
|
135
|
+
match_score += 0.2
|
|
136
|
+
|
|
137
|
+
return min(match_score, 1.0)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _calculate_schema_match(query: str, metadata: Metadata) -> float:
|
|
141
|
+
"""Calculate schema match score."""
|
|
142
|
+
query_lower = query.lower()
|
|
143
|
+
schema_score = 0.0
|
|
144
|
+
|
|
145
|
+
# Check input schema
|
|
146
|
+
if "properties" in metadata.input_schema:
|
|
147
|
+
props = metadata.input_schema["properties"]
|
|
148
|
+
for key in props.keys():
|
|
149
|
+
if key.lower() in query_lower:
|
|
150
|
+
schema_score += 0.25
|
|
151
|
+
|
|
152
|
+
# Check output schema
|
|
153
|
+
if "properties" in metadata.output_schema:
|
|
154
|
+
props = metadata.output_schema["properties"]
|
|
155
|
+
for key in props.keys():
|
|
156
|
+
if key.lower() in query_lower:
|
|
157
|
+
schema_score += 0.15
|
|
158
|
+
|
|
159
|
+
return min(schema_score, 1.0)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _calculate_cost_efficiency(capabilities: List[Capability]) -> float:
|
|
163
|
+
"""Calculate cost efficiency score."""
|
|
164
|
+
if not capabilities:
|
|
165
|
+
return 0.5
|
|
166
|
+
|
|
167
|
+
avg_cost = sum(c.cost_estimate for c in capabilities) / len(capabilities)
|
|
168
|
+
avg_reliability = sum(c.reliability_score for c in capabilities) / len(
|
|
169
|
+
capabilities
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
cost_score = 1.0 - min(avg_cost, 1.0)
|
|
173
|
+
reliability_score = avg_reliability
|
|
174
|
+
|
|
175
|
+
return min(
|
|
176
|
+
max((cost_score * 0.5 + reliability_score * 0.5), 0.0), 1.0
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class RoutingPlan:
|
|
181
|
+
"""Execution plan for a query."""
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
query: str,
|
|
186
|
+
primary_tools: List[RoutingScore],
|
|
187
|
+
secondary_tools: List[RoutingScore],
|
|
188
|
+
):
|
|
189
|
+
self.query = query
|
|
190
|
+
self.primary_tools = primary_tools
|
|
191
|
+
self.secondary_tools = secondary_tools
|
|
192
|
+
self.estimated_cost = len(primary_tools) * 0.1
|
|
193
|
+
self.estimated_latency_ms = min(len(primary_tools) * 50, 1000)
|
|
194
|
+
self.confidence = (
|
|
195
|
+
sum(t.overall_score for t in primary_tools) / len(primary_tools)
|
|
196
|
+
if primary_tools
|
|
197
|
+
else 0.0
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def to_dict(self):
|
|
201
|
+
"""Convert to dict."""
|
|
202
|
+
return {
|
|
203
|
+
"query": self.query,
|
|
204
|
+
"primary_tools": [
|
|
205
|
+
{
|
|
206
|
+
"tool_name": t.tool_name,
|
|
207
|
+
"server_id": t.server_id,
|
|
208
|
+
"overall_score": t.overall_score,
|
|
209
|
+
"relevance": t.relevance_score,
|
|
210
|
+
"capability_match": t.capability_match,
|
|
211
|
+
}
|
|
212
|
+
for t in self.primary_tools
|
|
213
|
+
],
|
|
214
|
+
"secondary_tools": [
|
|
215
|
+
{
|
|
216
|
+
"tool_name": t.tool_name,
|
|
217
|
+
"server_id": t.server_id,
|
|
218
|
+
"overall_score": t.overall_score,
|
|
219
|
+
}
|
|
220
|
+
for t in self.secondary_tools[:3]
|
|
221
|
+
],
|
|
222
|
+
"estimated_cost": self.estimated_cost,
|
|
223
|
+
"estimated_latency_ms": self.estimated_latency_ms,
|
|
224
|
+
"confidence": self.confidence,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class RoutingPlanner:
|
|
229
|
+
"""Plan routing for queries."""
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def plan(
|
|
233
|
+
query: str, all_scores: List[RoutingScore], max_primary: int = 3
|
|
234
|
+
) -> RoutingPlan:
|
|
235
|
+
"""Create routing plan."""
|
|
236
|
+
primary = SemanticRouter.select_best_tools(all_scores, 0.7, max_primary)
|
|
237
|
+
secondary = SemanticRouter.select_best_tools(all_scores, 0.4, 5)
|
|
238
|
+
|
|
239
|
+
return RoutingPlan(query, primary, secondary)
|