figureout 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {figureout-0.2.2/src/figureout.egg-info → figureout-0.2.4}/PKG-INFO +6 -2
- {figureout-0.2.2 → figureout-0.2.4}/README.md +4 -1
- {figureout-0.2.2 → figureout-0.2.4}/pyproject.toml +2 -2
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout/engine.py +24 -6
- {figureout-0.2.2 → figureout-0.2.4/src/figureout.egg-info}/PKG-INFO +6 -2
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout.egg-info/requires.txt +1 -0
- {figureout-0.2.2 → figureout-0.2.4}/tests/test_figureout.py +63 -0
- {figureout-0.2.2 → figureout-0.2.4}/LICENSE +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/setup.cfg +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout/__init__.py +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout/exceptions.py +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout/llm.py +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout/roles.py +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout/utils.py +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout.egg-info/SOURCES.txt +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout.egg-info/dependency_links.txt +0 -0
- {figureout-0.2.2 → figureout-0.2.4}/src/figureout.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: figureout
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: FigureOut - a lightweight and resilient multi-LLM orchestrator
|
|
5
5
|
Author: balajeekalyan
|
|
6
6
|
License: MIT
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.8
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: fastmcp
|
|
11
|
+
Requires-Dist: mcp<2,>=1.25
|
|
11
12
|
Requires-Dist: python-dotenv
|
|
12
13
|
Provides-Extra: openai
|
|
13
14
|
Requires-Dist: openai; extra == "openai"
|
|
@@ -222,9 +223,12 @@ With `verbose=True`, the response is wrapped under a `"response"` key and a `"de
|
|
|
222
223
|
```python
|
|
223
224
|
concierge = FigureOut(..., verbose=True)
|
|
224
225
|
result = await concierge.run("Find flights to Tokyo")
|
|
225
|
-
# {"response": {"flights": [...]}, "debug": {"input_tokens": 512, "output_tokens": 256,
|
|
226
|
+
# {"response": {"flights": [...]}, "debug": {"input_tokens": 512, "output_tokens": 256,
|
|
227
|
+
# "classifier_input_tokens": 180, "classifier_output_tokens": 8, ...}}
|
|
226
228
|
```
|
|
227
229
|
|
|
230
|
+
`input_tokens` and `output_tokens` are totals for the whole call, including the role-classification call. `classifier_input_tokens` and `classifier_output_tokens` give the classification share on its own; both are 0 when `role=` is passed explicitly.
|
|
231
|
+
|
|
228
232
|
## Examples
|
|
229
233
|
|
|
230
234
|
The `examples/` directory contains complete domain-specific backends with React+Vite frontends:
|
|
@@ -186,9 +186,12 @@ With `verbose=True`, the response is wrapped under a `"response"` key and a `"de
|
|
|
186
186
|
```python
|
|
187
187
|
concierge = FigureOut(..., verbose=True)
|
|
188
188
|
result = await concierge.run("Find flights to Tokyo")
|
|
189
|
-
# {"response": {"flights": [...]}, "debug": {"input_tokens": 512, "output_tokens": 256,
|
|
189
|
+
# {"response": {"flights": [...]}, "debug": {"input_tokens": 512, "output_tokens": 256,
|
|
190
|
+
# "classifier_input_tokens": 180, "classifier_output_tokens": 8, ...}}
|
|
190
191
|
```
|
|
191
192
|
|
|
193
|
+
`input_tokens` and `output_tokens` are totals for the whole call, including the role-classification call. `classifier_input_tokens` and `classifier_output_tokens` give the classification share on its own; both are 0 when `role=` is passed explicitly.
|
|
194
|
+
|
|
192
195
|
## Examples
|
|
193
196
|
|
|
194
197
|
The `examples/` directory contains complete domain-specific backends with React+Vite frontends:
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "figureout"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "FigureOut - a lightweight and resilient multi-LLM orchestrator"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
|
-
dependencies = ["fastmcp", "python-dotenv"]
|
|
12
|
+
dependencies = ["fastmcp", "mcp>=1.25,<2", "python-dotenv"] # mcp 2.x renamed FastMCP, whose internals FigureOut reads
|
|
13
13
|
authors = [
|
|
14
14
|
{name = "balajeekalyan"},
|
|
15
15
|
]
|
|
@@ -23,13 +23,20 @@ def _cache_key(query: str, context: str | None = None) -> str:
|
|
|
23
23
|
|
|
24
24
|
async def classify_role(chat, user_query: str, classification_prompt: str, valid_roles: set[str]) -> list[str]:
|
|
25
25
|
"""Use the LLM to determine the appropriate roles from a user query."""
|
|
26
|
+
roles, _, _ = await _classify_role_with_usage(chat, user_query, classification_prompt, valid_roles)
|
|
27
|
+
return roles
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _classify_role_with_usage(chat, user_query: str, classification_prompt: str, valid_roles: set[str]) -> tuple[list[str], int, int]:
|
|
31
|
+
"""Classify the query and also return the classifier call's input and output token counts."""
|
|
26
32
|
messages = [
|
|
27
33
|
{"role": "system", "content": classification_prompt},
|
|
28
34
|
{"role": "user", "content": user_query},
|
|
29
35
|
]
|
|
30
36
|
result = await chat(messages, json_mode=True)
|
|
31
37
|
parsed = _extract_json(result["text"])
|
|
32
|
-
|
|
38
|
+
roles = [r for r in parsed["roles"] if r in valid_roles]
|
|
39
|
+
return roles, result.get("input_tokens", 0), result.get("output_tokens", 0)
|
|
33
40
|
|
|
34
41
|
|
|
35
42
|
class FigureOut:
|
|
@@ -241,6 +248,8 @@ class FigureOut:
|
|
|
241
248
|
output_schema: str | None = None,
|
|
242
249
|
) -> dict:
|
|
243
250
|
"""Send query to LLM with role-based system prompt, return result."""
|
|
251
|
+
classifier_input_tokens = 0
|
|
252
|
+
classifier_output_tokens = 0
|
|
244
253
|
if role is not None:
|
|
245
254
|
roles = [role]
|
|
246
255
|
cache_key = None
|
|
@@ -258,7 +267,9 @@ class FigureOut:
|
|
|
258
267
|
return cached_response
|
|
259
268
|
|
|
260
269
|
try:
|
|
261
|
-
roles = await
|
|
270
|
+
roles, classifier_input_tokens, classifier_output_tokens = await _classify_role_with_usage(
|
|
271
|
+
self.chat_lite, user_query, self._classification_prompt, self._valid_roles
|
|
272
|
+
)
|
|
262
273
|
except Exception as exc:
|
|
263
274
|
raise RuntimeError(f"Role classification failed: {exc}") from exc
|
|
264
275
|
|
|
@@ -275,8 +286,10 @@ class FigureOut:
|
|
|
275
286
|
"context": context,
|
|
276
287
|
"roles_matched": roles,
|
|
277
288
|
"roles_selected": [],
|
|
278
|
-
"input_tokens":
|
|
279
|
-
"output_tokens":
|
|
289
|
+
"input_tokens": classifier_input_tokens,
|
|
290
|
+
"output_tokens": classifier_output_tokens,
|
|
291
|
+
"classifier_input_tokens": classifier_input_tokens,
|
|
292
|
+
"classifier_output_tokens": classifier_output_tokens,
|
|
280
293
|
"tools_used": [],
|
|
281
294
|
"assistant_messages": [],
|
|
282
295
|
},
|
|
@@ -289,8 +302,9 @@ class FigureOut:
|
|
|
289
302
|
user_content = user_query
|
|
290
303
|
|
|
291
304
|
assistant_messages = []
|
|
292
|
-
|
|
293
|
-
|
|
305
|
+
# Totals include the classifier call so reported cost covers the whole pipeline
|
|
306
|
+
total_input_tokens = classifier_input_tokens
|
|
307
|
+
total_output_tokens = classifier_output_tokens
|
|
294
308
|
|
|
295
309
|
# LLM calls — one per selected role, in parallel, with native tool calling loop
|
|
296
310
|
async def _run_role(role_to_run: str) -> tuple[str, dict]:
|
|
@@ -422,6 +436,8 @@ class FigureOut:
|
|
|
422
436
|
"roles_selected": selected_roles,
|
|
423
437
|
"input_tokens": total_input_tokens,
|
|
424
438
|
"output_tokens": total_output_tokens,
|
|
439
|
+
"classifier_input_tokens": classifier_input_tokens,
|
|
440
|
+
"classifier_output_tokens": classifier_output_tokens,
|
|
425
441
|
"tools_used": all_tools_used,
|
|
426
442
|
"assistant_messages": assistant_messages,
|
|
427
443
|
},
|
|
@@ -442,6 +458,8 @@ class FigureOut:
|
|
|
442
458
|
"roles_selected": selected_roles,
|
|
443
459
|
"input_tokens": total_input_tokens,
|
|
444
460
|
"output_tokens": total_output_tokens,
|
|
461
|
+
"classifier_input_tokens": classifier_input_tokens,
|
|
462
|
+
"classifier_output_tokens": classifier_output_tokens,
|
|
445
463
|
"tools_used": all_tools_used,
|
|
446
464
|
"assistant_messages": assistant_messages,
|
|
447
465
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: figureout
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: FigureOut - a lightweight and resilient multi-LLM orchestrator
|
|
5
5
|
Author: balajeekalyan
|
|
6
6
|
License: MIT
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.8
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: fastmcp
|
|
11
|
+
Requires-Dist: mcp<2,>=1.25
|
|
11
12
|
Requires-Dist: python-dotenv
|
|
12
13
|
Provides-Extra: openai
|
|
13
14
|
Requires-Dist: openai; extra == "openai"
|
|
@@ -222,9 +223,12 @@ With `verbose=True`, the response is wrapped under a `"response"` key and a `"de
|
|
|
222
223
|
```python
|
|
223
224
|
concierge = FigureOut(..., verbose=True)
|
|
224
225
|
result = await concierge.run("Find flights to Tokyo")
|
|
225
|
-
# {"response": {"flights": [...]}, "debug": {"input_tokens": 512, "output_tokens": 256,
|
|
226
|
+
# {"response": {"flights": [...]}, "debug": {"input_tokens": 512, "output_tokens": 256,
|
|
227
|
+
# "classifier_input_tokens": 180, "classifier_output_tokens": 8, ...}}
|
|
226
228
|
```
|
|
227
229
|
|
|
230
|
+
`input_tokens` and `output_tokens` are totals for the whole call, including the role-classification call. `classifier_input_tokens` and `classifier_output_tokens` give the classification share on its own; both are 0 when `role=` is passed explicitly.
|
|
231
|
+
|
|
228
232
|
## Examples
|
|
229
233
|
|
|
230
234
|
The `examples/` directory contains complete domain-specific backends with React+Vite frontends:
|
|
@@ -388,3 +388,66 @@ def test_interpret_tool_response_false_returns_raw_output(mock_get_llm):
|
|
|
388
388
|
mock_tool.fn.assert_called_once_with(query="test")
|
|
389
389
|
assert isinstance(result, list)
|
|
390
390
|
assert result[0]["id"] == 1
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@patch("figureout.engine.get_llm_client")
|
|
394
|
+
def test_verbose_tokens_include_classifier(mock_get_llm):
|
|
395
|
+
mock_classify_chat = AsyncMock(return_value={
|
|
396
|
+
"text": json.dumps({"roles": ["discovery"]}),
|
|
397
|
+
"input_tokens": 10,
|
|
398
|
+
"output_tokens": 5,
|
|
399
|
+
"tools_used": [],
|
|
400
|
+
})
|
|
401
|
+
mock_main_chat = AsyncMock(return_value={
|
|
402
|
+
"text": json.dumps({"results": [], "summary": "No results"}),
|
|
403
|
+
"input_tokens": 20,
|
|
404
|
+
"output_tokens": 10,
|
|
405
|
+
"tools_used": [],
|
|
406
|
+
"tool_calls": [],
|
|
407
|
+
"assistant_message": {"role": "assistant", "content": "{}"},
|
|
408
|
+
})
|
|
409
|
+
mock_get_llm.side_effect = [mock_main_chat, mock_classify_chat]
|
|
410
|
+
pkg = FigureOut(roles=ROLES, verbose=True, cache_enabled=False)
|
|
411
|
+
|
|
412
|
+
debug = asyncio.run(pkg.run("find something"))["debug"]
|
|
413
|
+
assert debug["classifier_input_tokens"] == 10
|
|
414
|
+
assert debug["classifier_output_tokens"] == 5
|
|
415
|
+
assert debug["input_tokens"] == 30
|
|
416
|
+
assert debug["output_tokens"] == 15
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@patch("figureout.engine.get_llm_client")
|
|
420
|
+
def test_verbose_off_topic_reports_classifier_tokens(mock_get_llm):
|
|
421
|
+
mock_classify_chat = AsyncMock(return_value={
|
|
422
|
+
"text": json.dumps({"roles": []}),
|
|
423
|
+
"input_tokens": 10,
|
|
424
|
+
"output_tokens": 5,
|
|
425
|
+
"tools_used": [],
|
|
426
|
+
})
|
|
427
|
+
mock_main_chat = AsyncMock()
|
|
428
|
+
mock_get_llm.side_effect = [mock_main_chat, mock_classify_chat]
|
|
429
|
+
pkg = FigureOut(roles=ROLES, verbose=True, cache_enabled=False)
|
|
430
|
+
|
|
431
|
+
debug = asyncio.run(pkg.run("what's the weather"))["debug"]
|
|
432
|
+
assert mock_main_chat.call_count == 0
|
|
433
|
+
assert debug["input_tokens"] == 10
|
|
434
|
+
assert debug["output_tokens"] == 5
|
|
435
|
+
assert debug["classifier_input_tokens"] == 10
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@patch("figureout.engine.get_llm_client")
|
|
439
|
+
def test_verbose_explicit_role_has_no_classifier_tokens(mock_get_llm):
|
|
440
|
+
mock_main_chat = AsyncMock(return_value={
|
|
441
|
+
"text": json.dumps({"results": [], "summary": "No results"}),
|
|
442
|
+
"input_tokens": 20,
|
|
443
|
+
"output_tokens": 10,
|
|
444
|
+
"tools_used": [],
|
|
445
|
+
"tool_calls": [],
|
|
446
|
+
"assistant_message": {"role": "assistant", "content": "{}"},
|
|
447
|
+
})
|
|
448
|
+
mock_get_llm.return_value = mock_main_chat
|
|
449
|
+
pkg = FigureOut(roles=ROLES, verbose=True)
|
|
450
|
+
|
|
451
|
+
debug = asyncio.run(pkg.run("find something", role="discovery"))["debug"]
|
|
452
|
+
assert debug["classifier_input_tokens"] == 0
|
|
453
|
+
assert debug["input_tokens"] == 20
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|