fusefable 0.3.1__tar.gz → 0.4.0__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.
Files changed (54) hide show
  1. {fusefable-0.3.1 → fusefable-0.4.0}/PKG-INFO +15 -1
  2. {fusefable-0.3.1 → fusefable-0.4.0}/README.md +12 -0
  3. fusefable-0.4.0/fusefable/__init__.py +1 -0
  4. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/cli.py +8 -0
  5. fusefable-0.4.0/fusefable/desktop.py +82 -0
  6. fusefable-0.4.0/fusefable/web.py +115 -0
  7. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable.egg-info/PKG-INFO +15 -1
  8. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable.egg-info/SOURCES.txt +3 -0
  9. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable.egg-info/requires.txt +3 -0
  10. {fusefable-0.3.1 → fusefable-0.4.0}/pyproject.toml +2 -1
  11. fusefable-0.4.0/tests/test_desktop.py +49 -0
  12. fusefable-0.3.1/fusefable/__init__.py +0 -1
  13. {fusefable-0.3.1 → fusefable-0.4.0}/LICENSE +0 -0
  14. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/cache.py +0 -0
  15. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/client.py +0 -0
  16. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/compressor.py +0 -0
  17. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/config.py +0 -0
  18. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/core.py +0 -0
  19. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/cost.py +0 -0
  20. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/ensemble.py +0 -0
  21. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/fanout.py +0 -0
  22. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/fusion.py +0 -0
  23. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/judge.py +0 -0
  24. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/mcp_server.py +0 -0
  25. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/models.py +0 -0
  26. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/providers/__init__.py +0 -0
  27. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/providers/anthropic.py +0 -0
  28. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/providers/base.py +0 -0
  29. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/providers/factory.py +0 -0
  30. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/providers/google.py +0 -0
  31. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/providers/openai_compat.py +0 -0
  32. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/routing.py +0 -0
  33. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable/wizard.py +0 -0
  34. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable.egg-info/dependency_links.txt +0 -0
  35. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable.egg-info/entry_points.txt +0 -0
  36. {fusefable-0.3.1 → fusefable-0.4.0}/fusefable.egg-info/top_level.txt +0 -0
  37. {fusefable-0.3.1 → fusefable-0.4.0}/setup.cfg +0 -0
  38. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_cache.py +0 -0
  39. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_cli.py +0 -0
  40. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_client.py +0 -0
  41. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_compressor.py +0 -0
  42. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_config.py +0 -0
  43. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_core.py +0 -0
  44. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_cost.py +0 -0
  45. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_ensemble.py +0 -0
  46. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_fanout.py +0 -0
  47. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_fusion.py +0 -0
  48. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_judge.py +0 -0
  49. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_mcp_server.py +0 -0
  50. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_models.py +0 -0
  51. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_native_providers.py +0 -0
  52. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_openai_compat.py +0 -0
  53. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_routing.py +0 -0
  54. {fusefable-0.3.1 → fusefable-0.4.0}/tests/test_wizard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusefable
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Fuse multiple AI models and judge the best answer for coding
5
5
  Author: proultrax9
6
6
  License: MIT
@@ -24,6 +24,8 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
24
24
  Requires-Dist: respx>=0.21; extra == "dev"
25
25
  Provides-Extra: mcp
26
26
  Requires-Dist: mcp>=1.2; extra == "mcp"
27
+ Provides-Extra: app
28
+ Requires-Dist: pywebview>=5; extra == "app"
27
29
  Dynamic: license-file
28
30
 
29
31
  # Fuse Fable
@@ -45,6 +47,7 @@ and as a **subagent / pipe** (callable by other tools and scripts).
45
47
  ```bash
46
48
  pip install fusefable # core
47
49
  pip install "fusefable[mcp]" # if you want the MCP server
50
+ pip install "fusefable[app]" # if you want the desktop window (fusefable gui)
48
51
  ```
49
52
  From source:
50
53
  ```bash
@@ -75,6 +78,17 @@ setx OPENROUTER_API_KEY "sk-..." # Windows (open a new terminal afterwards)
75
78
 
76
79
  Config is stored at `~/.fusefable/config.yaml`.
77
80
 
81
+ ## 0) Use as a desktop app
82
+
83
+ A native window (like Cursor/VS Code), no browser tab — chat UI with toggles for
84
+ compress / ensemble / cache and a model filter:
85
+ ```bash
86
+ pip install "fusefable[app]"
87
+ fusefable gui
88
+ ```
89
+ Built on PyWebView (uses the system webview), so it's lightweight and cross-platform.
90
+ Requires a completed `fusefable config`.
91
+
78
92
  ## 1) Use as a CLI
79
93
  ```bash
80
94
  fusefable ask "Write a quicksort function in Python"
@@ -17,6 +17,7 @@ and as a **subagent / pipe** (callable by other tools and scripts).
17
17
  ```bash
18
18
  pip install fusefable # core
19
19
  pip install "fusefable[mcp]" # if you want the MCP server
20
+ pip install "fusefable[app]" # if you want the desktop window (fusefable gui)
20
21
  ```
21
22
  From source:
22
23
  ```bash
@@ -47,6 +48,17 @@ setx OPENROUTER_API_KEY "sk-..." # Windows (open a new terminal afterwards)
47
48
 
48
49
  Config is stored at `~/.fusefable/config.yaml`.
49
50
 
51
+ ## 0) Use as a desktop app
52
+
53
+ A native window (like Cursor/VS Code), no browser tab — chat UI with toggles for
54
+ compress / ensemble / cache and a model filter:
55
+ ```bash
56
+ pip install "fusefable[app]"
57
+ fusefable gui
58
+ ```
59
+ Built on PyWebView (uses the system webview), so it's lightweight and cross-platform.
60
+ Requires a completed `fusefable config`.
61
+
50
62
  ## 1) Use as a CLI
51
63
  ```bash
52
64
  fusefable ask "Write a quicksort function in Python"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -131,5 +131,13 @@ def mcp():
131
131
  run_mcp()
132
132
 
133
133
 
134
+ @app.command()
135
+ def gui():
136
+ """เปิดหน้าต่างโปรแกรม (desktop window) แบบ Cursor/VS Code."""
137
+ cfg = _load_or_die()
138
+ from fusefable.desktop import run_app
139
+ run_app(cfg)
140
+
141
+
134
142
  if __name__ == "__main__":
135
143
  app()
@@ -0,0 +1,82 @@
1
+ """Desktop window (PyWebView) — หน้าต่างโปรแกรมแบบ Cursor/VS Code.
2
+
3
+ ห่อ web UI ด้วย webview ของระบบ; JS เรียก Python ผ่าน js_api (ไม่ต้องมี web server).
4
+ แยก answer_to_dict / run_query (test ได้) ออกจาก run_app (เปิดหน้าต่างจริง).
5
+ """
6
+ from __future__ import annotations
7
+ import asyncio
8
+ from typing import Optional
9
+ from fusefable.config import Config
10
+ from fusefable.core import fuse
11
+ from fusefable.models import FinalAnswer
12
+
13
+
14
+ def answer_to_dict(ans: FinalAnswer) -> dict:
15
+ """แปลง FinalAnswer → dict ส่งให้ JS."""
16
+ d = {
17
+ "answer": ans.text,
18
+ "chosen_model": ans.chosen_model,
19
+ "reason": ans.reason,
20
+ "cost_usd": ans.cost_usd,
21
+ "cached": ans.cached,
22
+ "budget_warning": ans.budget_warning,
23
+ "candidates": [{"model": c.model, "text": c.text}
24
+ for c in ans.all_completions],
25
+ }
26
+ if ans.compression is not None:
27
+ c = ans.compression
28
+ d["compression"] = {
29
+ "original_chars": c.original_chars,
30
+ "final_chars": c.final_chars,
31
+ "saved_pct": round(c.saved_pct, 1),
32
+ "method": c.method,
33
+ }
34
+ return d
35
+
36
+
37
+ def _models_from_payload(payload: dict) -> Optional[list[str]]:
38
+ raw = payload.get("models")
39
+ if not raw:
40
+ return None
41
+ if isinstance(raw, str):
42
+ items = [m.strip() for m in raw.split(",") if m.strip()]
43
+ return items or None
44
+ return list(raw) or None
45
+
46
+
47
+ def run_query(cfg: Config, payload: dict) -> dict:
48
+ """รัน fuse จาก payload ของ UI. คืน dict (มี key 'error' ถ้าพัง) — ไม่โยน."""
49
+ try:
50
+ ans = asyncio.run(fuse(
51
+ cfg, payload["question"],
52
+ models=_models_from_payload(payload),
53
+ compress=payload.get("compress"),
54
+ ensemble=payload.get("ensemble"),
55
+ use_cache=payload.get("cache"),
56
+ ))
57
+ return answer_to_dict(ans)
58
+ except Exception as e: # noqa: BLE001 — ส่ง error กลับ UI ไม่ให้หน้าต่างค้าง
59
+ return {"error": str(e)}
60
+
61
+
62
+ class Api:
63
+ """js_api ให้ฝั่ง JS เรียก (pywebview.api.ask(...))."""
64
+
65
+ def __init__(self, cfg: Config):
66
+ self.cfg = cfg
67
+
68
+ def ask(self, payload: dict) -> dict:
69
+ return run_query(self.cfg, payload)
70
+
71
+
72
+ def run_app(cfg: Config) -> None:
73
+ """เปิดหน้าต่าง desktop. ต้องติดตั้ง: pip install 'fusefable[app]'."""
74
+ try:
75
+ import webview
76
+ except ImportError:
77
+ raise SystemExit("ติดตั้งก่อน: pip install 'fusefable[app]'")
78
+ from fusefable.web import INDEX_HTML
79
+ api = Api(cfg)
80
+ webview.create_window("Fuse Fable", html=INDEX_HTML, js_api=api,
81
+ width=960, height=720, min_size=(640, 480))
82
+ webview.start()
@@ -0,0 +1,115 @@
1
+ """UI ของหน้าต่าง desktop (HTML/CSS/JS ฝังในไฟล์เดียว — ไม่พึ่ง network)."""
2
+
3
+ INDEX_HTML = r"""<!DOCTYPE html>
4
+ <html lang="th">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <title>Fuse Fable</title>
9
+ <style>
10
+ :root { --bg:#0f1115; --panel:#171a21; --panel2:#1f2430; --line:#2a3040;
11
+ --text:#e6e9ef; --muted:#8b93a7; --accent:#5b9cff; --green:#3fb950; }
12
+ * { box-sizing:border-box; }
13
+ html,body { margin:0; height:100%; }
14
+ body { background:var(--bg); color:var(--text); font:14px/1.55 -apple-system,Segoe UI,Roboto,sans-serif;
15
+ display:flex; flex-direction:column; }
16
+ header { padding:12px 16px; border-bottom:1px solid var(--line); display:flex;
17
+ align-items:center; gap:10px; background:var(--panel); }
18
+ header .dot { width:9px; height:9px; border-radius:50%; background:var(--green); }
19
+ header h1 { font-size:15px; margin:0; font-weight:600; }
20
+ header .sub { color:var(--muted); font-size:12px; margin-left:auto; }
21
+ #opts { display:flex; gap:14px; align-items:center; flex-wrap:wrap;
22
+ padding:8px 16px; border-bottom:1px solid var(--line); background:var(--panel);
23
+ font-size:13px; color:var(--muted); }
24
+ #opts label { display:flex; gap:5px; align-items:center; cursor:pointer; }
25
+ #opts input[type=text] { background:var(--panel2); border:1px solid var(--line);
26
+ color:var(--text); border-radius:6px; padding:4px 8px; font-size:12px; width:200px; }
27
+ #msgs { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:12px; }
28
+ .bubble { max-width:88%; padding:10px 13px; border-radius:12px; white-space:pre-wrap;
29
+ word-wrap:break-word; }
30
+ .user { align-self:flex-end; background:var(--accent); color:#fff; border-bottom-right-radius:3px; }
31
+ .bot { align-self:flex-start; background:var(--panel2); border:1px solid var(--line);
32
+ border-bottom-left-radius:3px; }
33
+ .meta { color:var(--muted); font-size:12px; margin-top:7px; padding-top:7px;
34
+ border-top:1px solid var(--line); }
35
+ .err { background:#3a1d1d; border-color:#5c2b2b; color:#ffb4b4; }
36
+ details { margin-top:6px; } summary { cursor:pointer; color:var(--accent); font-size:12px; }
37
+ details pre { background:var(--bg); padding:8px; border-radius:6px; overflow:auto;
38
+ font-size:12px; border:1px solid var(--line); }
39
+ #inbar { display:flex; gap:8px; padding:12px 16px; border-top:1px solid var(--line);
40
+ background:var(--panel); }
41
+ #q { flex:1; resize:none; height:44px; max-height:160px; background:var(--panel2);
42
+ border:1px solid var(--line); color:var(--text); border-radius:8px; padding:10px 12px;
43
+ font:14px inherit; }
44
+ #send { background:var(--accent); color:#fff; border:0; border-radius:8px; padding:0 18px;
45
+ font-weight:600; cursor:pointer; }
46
+ #send:disabled { opacity:.5; cursor:default; }
47
+ .spin { color:var(--muted); font-size:13px; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <header>
52
+ <span class="dot"></span><h1>Fuse Fable</h1>
53
+ <span class="sub">fan-out · judge/ensemble · best answer</span>
54
+ </header>
55
+ <div id="opts">
56
+ <label><input type="checkbox" id="compress"> compress</label>
57
+ <label><input type="checkbox" id="ensemble"> ensemble</label>
58
+ <label><input type="checkbox" id="cache"> cache</label>
59
+ <input type="text" id="models" placeholder="models (comma, ว่าง=ทุกตัว)">
60
+ </div>
61
+ <div id="msgs"></div>
62
+ <div id="inbar">
63
+ <textarea id="q" placeholder="พิมพ์คำถาม… (Enter ส่ง, Shift+Enter ขึ้นบรรทัด)"></textarea>
64
+ <button id="send">Send</button>
65
+ </div>
66
+ <script>
67
+ const msgs = document.getElementById('msgs');
68
+ const q = document.getElementById('q');
69
+ const send = document.getElementById('send');
70
+
71
+ function esc(s){ const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
72
+ function add(cls, html){ const el=document.createElement('div'); el.className='bubble '+cls;
73
+ el.innerHTML=html; msgs.appendChild(el); msgs.scrollTop=msgs.scrollHeight; return el; }
74
+
75
+ function renderBot(r){
76
+ if(r.error){ add('bot err', '⚠️ '+esc(r.error)); return; }
77
+ let meta = [];
78
+ meta.push(r.chosen_model==='ensemble' ? 'ensemble' : 'from '+esc(r.chosen_model));
79
+ meta.push(r.cached ? 'cached, $0' : '$'+(r.cost_usd||0).toFixed(4));
80
+ if(r.compression) meta.push('compressed '+r.compression.original_chars+'→'+
81
+ r.compression.final_chars+' (~'+r.compression.saved_pct+'%)');
82
+ let cand = '';
83
+ if(r.candidates && r.candidates.length){
84
+ cand = '<details><summary>'+r.candidates.length+' candidates</summary>'+
85
+ r.candidates.map(c=>'<div><b>'+esc(c.model)+'</b><pre>'+esc(c.text)+'</pre></div>').join('')+
86
+ '</details>';
87
+ }
88
+ let warn = r.budget_warning ? '<div class="meta">⚠️ '+esc(r.budget_warning)+'</div>' : '';
89
+ add('bot', esc(r.answer) + warn + '<div class="meta">'+meta.join(' · ')+'</div>' + cand);
90
+ }
91
+
92
+ async function ask(){
93
+ const text = q.value.trim(); if(!text) return;
94
+ add('user', esc(text)); q.value='';
95
+ send.disabled=true;
96
+ const spin = add('bot spin', 'กำลังฟิวชั่นหลายโมเดล…');
97
+ const payload = { question:text,
98
+ compress:document.getElementById('compress').checked,
99
+ ensemble:document.getElementById('ensemble').checked,
100
+ cache:document.getElementById('cache').checked,
101
+ models:document.getElementById('models').value };
102
+ try {
103
+ const r = await window.pywebview.api.ask(payload);
104
+ spin.remove(); renderBot(r);
105
+ } catch(e){ spin.remove(); add('bot err','⚠️ '+esc(String(e))); }
106
+ send.disabled=false; q.focus();
107
+ }
108
+
109
+ send.addEventListener('click', ask);
110
+ q.addEventListener('keydown', e=>{ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); ask(); }});
111
+ q.focus();
112
+ </script>
113
+ </body>
114
+ </html>
115
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusefable
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Fuse multiple AI models and judge the best answer for coding
5
5
  Author: proultrax9
6
6
  License: MIT
@@ -24,6 +24,8 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
24
24
  Requires-Dist: respx>=0.21; extra == "dev"
25
25
  Provides-Extra: mcp
26
26
  Requires-Dist: mcp>=1.2; extra == "mcp"
27
+ Provides-Extra: app
28
+ Requires-Dist: pywebview>=5; extra == "app"
27
29
  Dynamic: license-file
28
30
 
29
31
  # Fuse Fable
@@ -45,6 +47,7 @@ and as a **subagent / pipe** (callable by other tools and scripts).
45
47
  ```bash
46
48
  pip install fusefable # core
47
49
  pip install "fusefable[mcp]" # if you want the MCP server
50
+ pip install "fusefable[app]" # if you want the desktop window (fusefable gui)
48
51
  ```
49
52
  From source:
50
53
  ```bash
@@ -75,6 +78,17 @@ setx OPENROUTER_API_KEY "sk-..." # Windows (open a new terminal afterwards)
75
78
 
76
79
  Config is stored at `~/.fusefable/config.yaml`.
77
80
 
81
+ ## 0) Use as a desktop app
82
+
83
+ A native window (like Cursor/VS Code), no browser tab — chat UI with toggles for
84
+ compress / ensemble / cache and a model filter:
85
+ ```bash
86
+ pip install "fusefable[app]"
87
+ fusefable gui
88
+ ```
89
+ Built on PyWebView (uses the system webview), so it's lightweight and cross-platform.
90
+ Requires a completed `fusefable config`.
91
+
78
92
  ## 1) Use as a CLI
79
93
  ```bash
80
94
  fusefable ask "Write a quicksort function in Python"
@@ -9,6 +9,7 @@ fusefable/compressor.py
9
9
  fusefable/config.py
10
10
  fusefable/core.py
11
11
  fusefable/cost.py
12
+ fusefable/desktop.py
12
13
  fusefable/ensemble.py
13
14
  fusefable/fanout.py
14
15
  fusefable/fusion.py
@@ -16,6 +17,7 @@ fusefable/judge.py
16
17
  fusefable/mcp_server.py
17
18
  fusefable/models.py
18
19
  fusefable/routing.py
20
+ fusefable/web.py
19
21
  fusefable/wizard.py
20
22
  fusefable.egg-info/PKG-INFO
21
23
  fusefable.egg-info/SOURCES.txt
@@ -36,6 +38,7 @@ tests/test_compressor.py
36
38
  tests/test_config.py
37
39
  tests/test_core.py
38
40
  tests/test_cost.py
41
+ tests/test_desktop.py
39
42
  tests/test_ensemble.py
40
43
  tests/test_fanout.py
41
44
  tests/test_fusion.py
@@ -2,6 +2,9 @@ httpx>=0.27
2
2
  typer>=0.12
3
3
  pyyaml>=6.0
4
4
 
5
+ [app]
6
+ pywebview>=5
7
+
5
8
  [dev]
6
9
  pytest>=8.0
7
10
  pytest-asyncio>=0.23
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fusefable"
3
- version = "0.3.1"
3
+ version = "0.4.0"
4
4
  description = "Fuse multiple AI models and judge the best answer for coding"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -27,6 +27,7 @@ Repository = "https://github.com/proultrax9/fusefable"
27
27
  [project.optional-dependencies]
28
28
  dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
29
29
  mcp = ["mcp>=1.2"]
30
+ app = ["pywebview>=5"]
30
31
 
31
32
  [project.scripts]
32
33
  fusefable = "fusefable.cli:app"
@@ -0,0 +1,49 @@
1
+ import pytest
2
+ from fusefable import desktop
3
+ from fusefable.models import FinalAnswer, Completion
4
+ from fusefable.compressor import CompressionResult
5
+
6
+
7
+ def test_answer_to_dict_includes_meta():
8
+ ans = FinalAnswer(text="best", chosen_model="gpt", reason="r", cost_usd=0.02,
9
+ all_completions=[Completion(model="gpt", text="best")],
10
+ compression=CompressionResult("x", 1000, 400, "llm"))
11
+ d = desktop.answer_to_dict(ans)
12
+ assert d["answer"] == "best"
13
+ assert d["chosen_model"] == "gpt"
14
+ assert d["candidates"][0]["model"] == "gpt"
15
+ assert d["compression"]["method"] == "llm"
16
+ assert d["compression"]["saved_pct"] == 60.0
17
+
18
+
19
+ def test_models_from_payload_parses_csv():
20
+ assert desktop._models_from_payload({"models": "a, b ,c"}) == ["a", "b", "c"]
21
+ assert desktop._models_from_payload({"models": ""}) is None
22
+ assert desktop._models_from_payload({}) is None
23
+
24
+
25
+ def test_run_query_calls_fuse_and_serializes(monkeypatch):
26
+ captured = {}
27
+
28
+ async def fake_fuse(cfg, question, models=None, compress=None,
29
+ ensemble=None, use_cache=None):
30
+ captured.update(question=question, models=models, compress=compress,
31
+ ensemble=ensemble, use_cache=use_cache)
32
+ return FinalAnswer(text="hi", chosen_model="m")
33
+
34
+ monkeypatch.setattr(desktop, "fuse", fake_fuse)
35
+ out = desktop.run_query("CFG", {"question": "Q", "models": "a,b",
36
+ "compress": True, "ensemble": False, "cache": True})
37
+ assert out["answer"] == "hi"
38
+ assert captured["question"] == "Q"
39
+ assert captured["models"] == ["a", "b"]
40
+ assert captured["compress"] is True
41
+ assert captured["use_cache"] is True
42
+
43
+
44
+ def test_run_query_returns_error_on_failure(monkeypatch):
45
+ async def boom(*a, **k):
46
+ raise RuntimeError("kaboom")
47
+ monkeypatch.setattr(desktop, "fuse", boom)
48
+ out = desktop.run_query("CFG", {"question": "Q"})
49
+ assert out["error"] == "kaboom"
@@ -1 +0,0 @@
1
- __version__ = "0.3.1"
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes