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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: figureout
3
- Version: 0.2.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.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
- return [r for r in parsed["roles"] if r in valid_roles]
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 classify_role(self.chat_lite, user_query, self._classification_prompt, self._valid_roles)
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": 0,
279
- "output_tokens": 0,
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
- total_input_tokens = 0
293
- total_output_tokens = 0
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.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:
@@ -1,4 +1,5 @@
1
1
  fastmcp
2
+ mcp<2,>=1.25
2
3
  python-dotenv
3
4
 
4
5
  [all]
@@ -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