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.
@@ -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,3 @@
1
+ from fluxcompute.classifier.heuristic import classify
2
+
3
+ __all__ = ["classify"]
@@ -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
+ )