fluxcompute 0.1.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.
- fluxcompute/__init__.py +28 -0
- fluxcompute/classifier/__init__.py +3 -0
- fluxcompute/classifier/heuristic.py +313 -0
- fluxcompute/client.py +515 -0
- fluxcompute/cost.py +97 -0
- fluxcompute/intelligence/__init__.py +0 -0
- fluxcompute/intelligence/drift.py +244 -0
- fluxcompute/intelligence/oracle.py +222 -0
- fluxcompute/models.py +145 -0
- fluxcompute/router/__init__.py +3 -0
- fluxcompute/router/dispatcher.py +287 -0
- fluxcompute/state/__init__.py +5 -0
- fluxcompute/state/cache_manager.py +165 -0
- fluxcompute/state/context_builder.py +196 -0
- fluxcompute/state/redis_session.py +128 -0
- fluxcompute/state/session.py +102 -0
- fluxcompute/telemetry/__init__.py +3 -0
- fluxcompute/telemetry/reporter.py +109 -0
- fluxcompute-0.1.0.dist-info/METADATA +380 -0
- fluxcompute-0.1.0.dist-info/RECORD +22 -0
- fluxcompute-0.1.0.dist-info/WHEEL +5 -0
- fluxcompute-0.1.0.dist-info/top_level.txt +1 -0
fluxcompute/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FluxCompute — The compiler for agentic systems.
|
|
3
|
+
|
|
4
|
+
Route every query to the optimal model. Same accuracy. 60-70% cost reduction.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from fluxcompute import FluxClient
|
|
8
|
+
|
|
9
|
+
client = FluxClient(
|
|
10
|
+
anthropic_key="sk-ant-xxx",
|
|
11
|
+
fluxcompute_key="flx_xxx", # optional, enables telemetry + dashboard
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
response = client.messages.create(
|
|
15
|
+
model="auto", # let FluxCompute decide
|
|
16
|
+
messages=[{"role": "user", "content": "What is 2+2?"}],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
print(response.fluxcompute.model_selected) # claude-3-5-haiku
|
|
20
|
+
print(response.fluxcompute.savings_usd) # 0.0035
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
from fluxcompute.client import FluxClient
|
|
26
|
+
from fluxcompute.models import FluxResponse, FluxMetadata, FluxStreamChunk, ClassificationResult, CacheStats
|
|
27
|
+
|
|
28
|
+
__all__ = ["FluxClient", "FluxResponse", "FluxMetadata", "FluxStreamChunk", "ClassificationResult", "CacheStats"]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heuristic difficulty classifier for agentic queries.
|
|
3
|
+
|
|
4
|
+
Classifies queries on a 0.0–1.0 difficulty scale using seven signal
|
|
5
|
+
categories. No ML required — rule-based heuristics that get 70%+ accuracy.
|
|
6
|
+
Designed to be replaced by a trained model once production data is available.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from typing import Dict, List, Set
|
|
13
|
+
|
|
14
|
+
from fluxcompute.models import ClassificationResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Keyword sets
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
REASONING_KEYWORDS: Set[str] = {
|
|
22
|
+
"why",
|
|
23
|
+
"explain",
|
|
24
|
+
"analyze",
|
|
25
|
+
"compare",
|
|
26
|
+
"evaluate",
|
|
27
|
+
"reason",
|
|
28
|
+
"think through",
|
|
29
|
+
"break down",
|
|
30
|
+
"consider",
|
|
31
|
+
"implications",
|
|
32
|
+
"trade-offs",
|
|
33
|
+
"trade offs",
|
|
34
|
+
"pros and cons",
|
|
35
|
+
"what would happen if",
|
|
36
|
+
"how would you approach",
|
|
37
|
+
"argue for",
|
|
38
|
+
"argue against",
|
|
39
|
+
"critique",
|
|
40
|
+
"assess",
|
|
41
|
+
"justify",
|
|
42
|
+
"interpret",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
MATH_KEYWORDS: Set[str] = {
|
|
46
|
+
"calculate",
|
|
47
|
+
"compute",
|
|
48
|
+
"solve",
|
|
49
|
+
"equation",
|
|
50
|
+
"formula",
|
|
51
|
+
"derivative",
|
|
52
|
+
"integral",
|
|
53
|
+
"probability",
|
|
54
|
+
"statistics",
|
|
55
|
+
"optimize",
|
|
56
|
+
"maximize",
|
|
57
|
+
"minimize",
|
|
58
|
+
"regression",
|
|
59
|
+
"matrix",
|
|
60
|
+
"vector",
|
|
61
|
+
"eigenvalue",
|
|
62
|
+
"proof",
|
|
63
|
+
"theorem",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
CODE_KEYWORDS: Set[str] = {
|
|
67
|
+
"write code",
|
|
68
|
+
"implement",
|
|
69
|
+
"debug",
|
|
70
|
+
"refactor",
|
|
71
|
+
"algorithm",
|
|
72
|
+
"function that",
|
|
73
|
+
"class that",
|
|
74
|
+
"api endpoint",
|
|
75
|
+
"sql query",
|
|
76
|
+
"regex",
|
|
77
|
+
"script",
|
|
78
|
+
"write a program",
|
|
79
|
+
"build a",
|
|
80
|
+
"create a function",
|
|
81
|
+
"fix this code",
|
|
82
|
+
"code review",
|
|
83
|
+
"unit test",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
SIMPLE_KEYWORDS: Set[str] = {
|
|
87
|
+
"what is",
|
|
88
|
+
"define",
|
|
89
|
+
"list",
|
|
90
|
+
"name",
|
|
91
|
+
"when did",
|
|
92
|
+
"who is",
|
|
93
|
+
"where is",
|
|
94
|
+
"how many",
|
|
95
|
+
"translate",
|
|
96
|
+
"format",
|
|
97
|
+
"convert",
|
|
98
|
+
"summarize briefly",
|
|
99
|
+
"yes or no",
|
|
100
|
+
"true or false",
|
|
101
|
+
"what does",
|
|
102
|
+
"spell",
|
|
103
|
+
"abbreviation",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
CREATIVE_KEYWORDS: Set[str] = {
|
|
107
|
+
"write a story",
|
|
108
|
+
"write a poem",
|
|
109
|
+
"creative writing",
|
|
110
|
+
"brainstorm",
|
|
111
|
+
"imagine",
|
|
112
|
+
"fiction",
|
|
113
|
+
"narrative",
|
|
114
|
+
"dialogue",
|
|
115
|
+
"screenplay",
|
|
116
|
+
"compose",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Model mapping
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
# Anthropic model tiers
|
|
125
|
+
ANTHROPIC_MODELS = {
|
|
126
|
+
"easy": "claude-3-5-haiku-20241022",
|
|
127
|
+
"medium": "claude-sonnet-4-20250514",
|
|
128
|
+
"hard": "claude-opus-4-20250918",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# OpenAI model tiers
|
|
132
|
+
OPENAI_MODELS = {
|
|
133
|
+
"easy": "gpt-4o-mini",
|
|
134
|
+
"medium": "gpt-4o",
|
|
135
|
+
"hard": "o1",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Classifier
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _count_keyword_hits(text: str, keywords: Set[str]) -> int:
|
|
144
|
+
"""Count how many keywords appear in the text."""
|
|
145
|
+
return sum(1 for kw in keywords if kw in text)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def classify(
|
|
149
|
+
messages: List[Dict[str, str]],
|
|
150
|
+
provider: str = "anthropic",
|
|
151
|
+
easy_threshold: float = 0.18,
|
|
152
|
+
hard_threshold: float = 0.45,
|
|
153
|
+
) -> ClassificationResult:
|
|
154
|
+
"""
|
|
155
|
+
Classify query difficulty using heuristic rules.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
messages: List of message dicts with "role" and "content" keys.
|
|
159
|
+
provider: "anthropic" or "openai" — determines model selection.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
ClassificationResult with score, label, model, reasoning.
|
|
163
|
+
"""
|
|
164
|
+
start = time.monotonic()
|
|
165
|
+
|
|
166
|
+
# Extract last user message
|
|
167
|
+
last_msg = ""
|
|
168
|
+
for msg in reversed(messages):
|
|
169
|
+
if msg.get("role") == "user":
|
|
170
|
+
last_msg = msg.get("content", "").lower()
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
if not last_msg:
|
|
174
|
+
# No user message found — default to medium
|
|
175
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
176
|
+
models = ANTHROPIC_MODELS if provider == "anthropic" else OPENAI_MODELS
|
|
177
|
+
return ClassificationResult(
|
|
178
|
+
score=0.5,
|
|
179
|
+
label="medium",
|
|
180
|
+
model=models["medium"],
|
|
181
|
+
reasoning="no user message found, defaulting to medium",
|
|
182
|
+
classification_ms=round(elapsed, 2),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
score = 0.0
|
|
186
|
+
reasons: list[str] = []
|
|
187
|
+
|
|
188
|
+
# -----------------------------------------------------------------------
|
|
189
|
+
# Signal 1: Query length (0 – 0.15)
|
|
190
|
+
# -----------------------------------------------------------------------
|
|
191
|
+
word_count = len(last_msg.split())
|
|
192
|
+
if word_count > 200:
|
|
193
|
+
score += 0.20
|
|
194
|
+
reasons.append(f"long query ({word_count} words)")
|
|
195
|
+
elif word_count > 80:
|
|
196
|
+
score += 0.15
|
|
197
|
+
reasons.append(f"medium-long query ({word_count} words)")
|
|
198
|
+
elif word_count > 30:
|
|
199
|
+
score += 0.08
|
|
200
|
+
reasons.append(f"moderate query ({word_count} words)")
|
|
201
|
+
|
|
202
|
+
# -----------------------------------------------------------------------
|
|
203
|
+
# Signal 2: Reasoning keywords (0 – 0.35)
|
|
204
|
+
# -----------------------------------------------------------------------
|
|
205
|
+
reasoning_hits = _count_keyword_hits(last_msg, REASONING_KEYWORDS)
|
|
206
|
+
if reasoning_hits >= 3:
|
|
207
|
+
score += 0.35
|
|
208
|
+
reasons.append(f"heavy reasoning ({reasoning_hits} signals)")
|
|
209
|
+
elif reasoning_hits >= 2:
|
|
210
|
+
score += 0.25
|
|
211
|
+
reasons.append(f"moderate reasoning ({reasoning_hits} signals)")
|
|
212
|
+
elif reasoning_hits >= 1:
|
|
213
|
+
score += 0.18
|
|
214
|
+
reasons.append(f"some reasoning ({reasoning_hits} signals)")
|
|
215
|
+
|
|
216
|
+
# -----------------------------------------------------------------------
|
|
217
|
+
# Signal 3: Math / computation keywords (0 – 0.30)
|
|
218
|
+
# -----------------------------------------------------------------------
|
|
219
|
+
math_hits = _count_keyword_hits(last_msg, MATH_KEYWORDS)
|
|
220
|
+
if math_hits >= 3:
|
|
221
|
+
score += 0.30
|
|
222
|
+
reasons.append(f"heavy math ({math_hits} signals)")
|
|
223
|
+
elif math_hits >= 2:
|
|
224
|
+
score += 0.22
|
|
225
|
+
reasons.append(f"moderate math ({math_hits} signals)")
|
|
226
|
+
elif math_hits >= 1:
|
|
227
|
+
score += 0.18
|
|
228
|
+
reasons.append(f"light math ({math_hits} signals)")
|
|
229
|
+
|
|
230
|
+
# -----------------------------------------------------------------------
|
|
231
|
+
# Signal 4: Code generation keywords (0 – 0.25)
|
|
232
|
+
# -----------------------------------------------------------------------
|
|
233
|
+
code_hits = _count_keyword_hits(last_msg, CODE_KEYWORDS)
|
|
234
|
+
if code_hits >= 2:
|
|
235
|
+
score += 0.30
|
|
236
|
+
reasons.append(f"code generation ({code_hits} signals)")
|
|
237
|
+
elif code_hits >= 1:
|
|
238
|
+
score += 0.15
|
|
239
|
+
reasons.append(f"light coding ({code_hits} signals)")
|
|
240
|
+
|
|
241
|
+
# -----------------------------------------------------------------------
|
|
242
|
+
# Signal 5: Simple query indicators (negative: -0.20 – 0)
|
|
243
|
+
# -----------------------------------------------------------------------
|
|
244
|
+
simple_hits = _count_keyword_hits(last_msg, SIMPLE_KEYWORDS)
|
|
245
|
+
if simple_hits >= 3:
|
|
246
|
+
score -= 0.20
|
|
247
|
+
reasons.append(f"very simple query ({simple_hits} signals)")
|
|
248
|
+
elif simple_hits >= 2:
|
|
249
|
+
score -= 0.15
|
|
250
|
+
reasons.append(f"simple query ({simple_hits} signals)")
|
|
251
|
+
elif simple_hits >= 1:
|
|
252
|
+
score -= 0.08
|
|
253
|
+
reasons.append(f"likely simple ({simple_hits} signals)")
|
|
254
|
+
|
|
255
|
+
# -----------------------------------------------------------------------
|
|
256
|
+
# Signal 6: Multi-turn depth (0 – 0.15)
|
|
257
|
+
# -----------------------------------------------------------------------
|
|
258
|
+
turn_count = len([m for m in messages if m.get("role") == "user"])
|
|
259
|
+
if turn_count > 10:
|
|
260
|
+
score += 0.15
|
|
261
|
+
reasons.append(f"deep conversation ({turn_count} user turns)")
|
|
262
|
+
elif turn_count > 5:
|
|
263
|
+
score += 0.10
|
|
264
|
+
reasons.append(f"multi-turn ({turn_count} user turns)")
|
|
265
|
+
elif turn_count > 2:
|
|
266
|
+
score += 0.05
|
|
267
|
+
reasons.append(f"short multi-turn ({turn_count} user turns)")
|
|
268
|
+
|
|
269
|
+
# -----------------------------------------------------------------------
|
|
270
|
+
# Signal 7: System prompt complexity (0 – 0.10)
|
|
271
|
+
# -----------------------------------------------------------------------
|
|
272
|
+
system_msgs = [m for m in messages if m.get("role") == "system"]
|
|
273
|
+
if system_msgs:
|
|
274
|
+
sys_len = len(system_msgs[0].get("content", "").split())
|
|
275
|
+
if sys_len > 500:
|
|
276
|
+
score += 0.10
|
|
277
|
+
reasons.append(f"complex system prompt ({sys_len} words)")
|
|
278
|
+
elif sys_len > 200:
|
|
279
|
+
score += 0.05
|
|
280
|
+
reasons.append(f"moderate system prompt ({sys_len} words)")
|
|
281
|
+
|
|
282
|
+
# -----------------------------------------------------------------------
|
|
283
|
+
# Signal 8: Creative writing (0 – 0.10)
|
|
284
|
+
# -----------------------------------------------------------------------
|
|
285
|
+
creative_hits = _count_keyword_hits(last_msg, CREATIVE_KEYWORDS)
|
|
286
|
+
if creative_hits >= 1:
|
|
287
|
+
score += 0.10
|
|
288
|
+
reasons.append(f"creative writing ({creative_hits} signals)")
|
|
289
|
+
|
|
290
|
+
# -----------------------------------------------------------------------
|
|
291
|
+
# Clamp and map to label + model
|
|
292
|
+
# -----------------------------------------------------------------------
|
|
293
|
+
score = max(0.0, min(1.0, score))
|
|
294
|
+
|
|
295
|
+
if score < easy_threshold:
|
|
296
|
+
label = "easy"
|
|
297
|
+
elif score < hard_threshold:
|
|
298
|
+
label = "medium"
|
|
299
|
+
else:
|
|
300
|
+
label = "hard"
|
|
301
|
+
|
|
302
|
+
models = ANTHROPIC_MODELS if provider == "anthropic" else OPENAI_MODELS
|
|
303
|
+
model = models[label]
|
|
304
|
+
|
|
305
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
306
|
+
|
|
307
|
+
return ClassificationResult(
|
|
308
|
+
score=round(score, 3),
|
|
309
|
+
label=label,
|
|
310
|
+
model=model,
|
|
311
|
+
reasoning="; ".join(reasons) if reasons else "default classification",
|
|
312
|
+
classification_ms=round(elapsed, 2),
|
|
313
|
+
)
|