JakeFake222 commited on
Commit
7a12b9e
·
verified ·
1 Parent(s): 62f1a95

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +152 -438
app.py CHANGED
@@ -1,50 +1,35 @@
1
  """
2
- MAKER Framework - Hugging Face Space
3
- =====================================
4
- Reliable AI Agent with Web Search & File Upload
5
- Based on: https://arxiv.org/abs/2511.09030
6
  """
7
 
8
  import gradio as gr
9
  import asyncio
10
- import json
11
- import re
12
- import base64
13
  from collections import Counter
14
- from dataclasses import dataclass, field
15
- from typing import Any, Callable, Optional
16
  from pathlib import Path
17
 
18
  # ============================================================================
19
- # MAKER Core (Embedded)
20
  # ============================================================================
21
 
22
  @dataclass
23
  class VotingConfig:
24
  k: int = 3
25
  max_samples: int = 30
26
- temperature_first: float = 0.0
27
  temperature_rest: float = 0.1
28
  parallel_samples: int = 3
29
 
30
- @dataclass
31
- class RedFlagConfig:
32
- max_response_chars: int = 3000
33
- min_response_length: int = 5
34
- banned_patterns: list = field(default_factory=lambda: [r"I don't know", r"I cannot"])
35
-
36
 
37
  class LLMClient:
38
- """Universal LLM client."""
39
-
40
  def __init__(self, provider: str, api_key: str, model: str = None):
41
  self.provider = provider.lower()
42
  self.api_key = api_key
43
  self.model = model
44
  self._client = None
45
- self._setup_client()
46
 
47
- def _setup_client(self):
48
  if self.provider == "openai":
49
  from openai import AsyncOpenAI
50
  self._client = AsyncOpenAI(api_key=self.api_key)
@@ -66,332 +51,176 @@ class LLMClient:
66
  self._client = AsyncOpenAI(api_key=self.api_key, base_url="https://openrouter.ai/api/v1")
67
  self.model = self.model or "openai/gpt-4o-mini"
68
 
69
- async def generate(self, prompt: str, temperature: float = 0.0, max_tokens: int = 1000) -> str:
70
  if self.provider == "anthropic":
71
- r = await self._client.messages.create(
72
- model=self.model, max_tokens=max_tokens,
73
- messages=[{"role": "user", "content": prompt}]
74
- )
 
 
 
 
 
 
 
75
  return r.content[0].text
76
  else:
77
  r = await self._client.chat.completions.create(
78
- model=self.model,
79
- messages=[{"role": "user", "content": prompt}],
80
- temperature=temperature, max_tokens=max_tokens
81
  )
82
  return r.choices[0].message.content
83
 
84
 
85
  class WebSearch:
86
- """Web search using DuckDuckGo (free)."""
87
-
88
  @staticmethod
89
- async def search(query: str, num_results: int = 5) -> list:
90
  try:
91
  from duckduckgo_search import DDGS
92
  results = []
93
  with DDGS() as ddgs:
94
- for r in ddgs.text(query, max_results=num_results):
95
- results.append({
96
- "title": r.get("title", ""),
97
- "url": r.get("href", ""),
98
- "snippet": r.get("body", "")
99
- })
100
- return results
101
- except Exception as e:
102
- return [{"title": "Error", "url": "", "snippet": str(e)}]
103
 
104
 
105
  class FileHandler:
106
- """Handle file uploads."""
107
-
108
  @staticmethod
109
- async def load_file(file_path: str) -> dict:
110
- path = Path(file_path)
111
- ext = path.suffix.lower()
112
-
113
  try:
114
- if ext in {'.txt', '.md', '.json', '.py', '.js', '.html', '.css', '.csv'}:
115
- content = path.read_text(encoding='utf-8', errors='replace')
116
- return {"type": "text", "name": path.name, "content": content[:50000]}
117
-
118
- elif ext == '.pdf':
119
- try:
120
- import pymupdf
121
- doc = pymupdf.open(str(path))
122
- text = "\n\n".join([page.get_text() for page in doc])
123
- doc.close()
124
- return {"type": "pdf", "name": path.name, "content": text[:50000]}
125
- except ImportError:
126
- return {"type": "error", "name": path.name, "content": "PDF requires: pip install pymupdf"}
127
-
128
  elif ext == '.docx':
129
- try:
130
- from docx import Document
131
- doc = Document(str(path))
132
- text = "\n\n".join([p.text for p in doc.paragraphs])
133
- return {"type": "docx", "name": path.name, "content": text[:50000]}
134
- except ImportError:
135
- return {"type": "error", "name": path.name, "content": "DOCX requires: pip install python-docx"}
136
-
137
- elif ext in {'.png', '.jpg', '.jpeg', '.gif', '.webp'}:
138
- content = path.read_bytes()
139
- b64 = base64.b64encode(content).decode('utf-8')
140
- return {"type": "image", "name": path.name, "base64": b64}
141
-
142
  else:
143
- content = path.read_text(encoding='utf-8', errors='replace')
144
- return {"type": "text", "name": path.name, "content": content[:50000]}
145
-
146
  except Exception as e:
147
- return {"type": "error", "name": path.name, "content": str(e)}
148
 
149
 
150
  class MAKERAgent:
151
- """MAKER Framework Agent."""
152
-
153
- def __init__(self, llm: LLMClient, voting: VotingConfig = None, red_flags: RedFlagConfig = None):
154
  self.llm = llm
155
  self.voting = voting or VotingConfig()
156
- self.red_flags = red_flags or RedFlagConfig()
157
- self.stats = {"samples": 0, "red_flags": 0, "tool_calls": 0}
158
-
159
- def _check_red_flags(self, response: str) -> bool:
160
- if len(response) > self.red_flags.max_response_chars:
161
- return True
162
- if len(response) < self.red_flags.min_response_length:
163
- return True
164
- for pattern in self.red_flags.banned_patterns:
165
- if re.search(pattern, response, re.IGNORECASE):
166
- return True
167
- return False
168
-
169
- def _parse_json(self, response: str) -> Optional[dict]:
170
- response = re.sub(r'^```(?:json)?\s*', '', response.strip())
171
- response = re.sub(r'\s*```$', '', response)
172
- try:
173
- result = json.loads(response)
174
- return result if isinstance(result, dict) else None
175
- except:
176
- return None
177
-
178
- def _serialize(self, result) -> str:
179
- if isinstance(result, dict):
180
- return json.dumps(result, sort_keys=True)
181
- return str(result)
182
 
183
- async def execute(self, prompt: str, expected_keys: list = None, use_tools: bool = False,
184
- file_context: str = None, progress_callback: Callable = None) -> dict:
 
185
 
186
- full_prompt = ""
187
- if file_context:
188
- full_prompt += f"CONTEXT FROM FILES:\n{file_context}\n\n"
189
- full_prompt += prompt
 
190
 
191
- if use_tools:
192
- full_prompt += '\n\nTo search web: {"tool": "web_search", "query": "..."}'
193
- full_prompt += "\n\nRespond with valid JSON only."
194
 
 
195
  votes: Counter = Counter()
196
- results_map = {}
197
- samples, flagged = 0, 0
198
- tool_results = []
199
 
200
- if progress_callback:
201
- progress_callback(0.1, "Getting first sample...")
202
-
203
- response = await self.llm.generate(full_prompt, temperature=0.0)
204
  samples += 1
205
- self.stats["samples"] += 1
206
-
207
- # Handle tool calls
208
- if use_tools:
209
- parsed = self._parse_json(response)
210
- if parsed and parsed.get("tool") == "web_search":
211
- query = parsed.get("query", "")
212
- if progress_callback:
213
- progress_callback(0.2, f"Searching: {query}...")
214
-
215
- search_results = await WebSearch.search(query)
216
- tool_results.append({"query": query, "results": search_results})
217
- self.stats["tool_calls"] += 1
218
-
219
- search_text = "\n".join([f"- {r['title']}: {r['snippet']}" for r in search_results[:5]])
220
- full_prompt += f"\n\nSEARCH RESULTS:\n{search_text}\n\nNow provide final JSON answer."
221
- response = await self.llm.generate(full_prompt, temperature=0.0)
222
- samples += 1
223
-
224
- # Parse response
225
- if self._check_red_flags(response):
226
- flagged += 1
227
- self.stats["red_flags"] += 1
228
- else:
229
- parsed = self._parse_json(response)
230
- if parsed and (not expected_keys or all(k in parsed for k in expected_keys)):
231
- key = self._serialize(parsed)
232
- votes[key] += 1
233
- results_map[key] = parsed
234
 
235
- # Voting loop
236
- round_num = 1
237
  while samples < self.voting.max_samples:
238
- if votes:
239
- top = votes.most_common(2)
240
- top_count = top[0][1]
241
- second_count = top[1][1] if len(top) > 1 else 0
242
- if top_count - second_count >= self.voting.k:
243
- break
244
-
245
- round_num += 1
246
- if progress_callback:
247
- progress_callback(0.2 + 0.6 * (samples / self.voting.max_samples), f"Voting round {round_num}...")
248
 
249
  for _ in range(self.voting.parallel_samples):
250
  if samples >= self.voting.max_samples:
251
  break
252
-
253
- response = await self.llm.generate(full_prompt, temperature=self.voting.temperature_rest)
254
  samples += 1
255
- self.stats["samples"] += 1
256
-
257
- if self._check_red_flags(response):
258
- flagged += 1
259
- continue
260
-
261
- parsed = self._parse_json(response)
262
- if parsed and (not expected_keys or all(k in parsed for k in expected_keys)):
263
- key = self._serialize(parsed)
264
- votes[key] += 1
265
- if key not in results_map:
266
- results_map[key] = parsed
267
 
268
- if progress_callback:
269
- progress_callback(1.0, "Complete!")
270
 
271
- if votes:
272
- top_key, top_count = votes.most_common(1)[0]
273
- return {
274
- "success": True, "result": results_map[top_key],
275
- "votes": top_count, "total_samples": samples,
276
- "red_flagged": flagged, "vote_distribution": dict(votes),
277
- "tool_results": tool_results
278
- }
279
 
280
- return {"success": False, "result": None, "votes": 0, "total_samples": samples,
281
- "red_flagged": flagged, "vote_distribution": {}, "tool_results": tool_results}
282
-
283
-
284
- # ============================================================================
285
- # Custom CSS
286
- # ============================================================================
287
-
288
- CUSTOM_CSS = """
289
- .gradio-container {
290
- max-width: 1200px !important;
291
- }
292
-
293
- .header-title {
294
- background: linear-gradient(90deg, #6366f1, #8b5cf6, #a855f7);
295
- -webkit-background-clip: text;
296
- -webkit-text-fill-color: transparent;
297
- font-size: 2.5rem !important;
298
- font-weight: 800 !important;
299
- text-align: center;
300
- }
301
-
302
- .header-sub {
303
- color: #64748b !important;
304
- text-align: center;
305
- margin-bottom: 1.5rem !important;
306
- }
307
-
308
- .primary-btn {
309
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
310
- border: none !important;
311
- font-weight: 600 !important;
312
- border-radius: 8px !important;
313
- }
314
 
315
- .primary-btn:hover {
316
- transform: translateY(-2px) !important;
317
- box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4) !important;
318
- }
319
- """
320
 
321
  # ============================================================================
322
- # State & Functions
323
  # ============================================================================
324
 
325
- current_agent = None
326
- loaded_files = {}
327
 
328
- def create_agent(provider, api_key, model, k_votes):
329
- global current_agent
330
- if not api_key:
331
- return "❌ Please enter API key"
332
  try:
333
- llm = LLMClient(provider, api_key, model if model else None)
334
- current_agent = MAKERAgent(llm, VotingConfig(k=k_votes))
335
- return f"✅ Agent ready: {provider} / {llm.model}"
336
  except Exception as e:
337
- return f"❌ Error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
- async def run_query_async(prompt, use_search, use_files, expected_keys, progress=gr.Progress()):
340
- global current_agent, loaded_files
341
 
342
- if not current_agent:
343
- return {"error": "Create agent first"}, "❌ No agent", ""
344
 
345
- file_context = None
346
- if use_files and loaded_files:
347
- parts = [f"=== {n} ===\n{i.get('content', '')[:10000]}"
348
- for n, i in loaded_files.items() if i["type"] != "image"]
349
- file_context = "\n\n".join(parts) if parts else None
350
 
351
- keys = [k.strip() for k in expected_keys.split(",") if k.strip()] if expected_keys else None
 
352
 
353
- def update_progress(pct, msg):
354
- progress(pct, desc=msg)
355
 
 
356
  try:
357
- result = await current_agent.execute(prompt, keys, use_search, file_context, update_progress)
358
-
359
- stats = f"""### Stats
360
- - **Success**: {'✅' if result['success'] else '❌'}
361
- - **Votes**: {result['votes']}
362
- - **Samples**: {result['total_samples']}
363
- - **Red-flagged**: {result['red_flagged']}"""
364
-
365
- votes = "### Vote Distribution\n" + "\n".join([f"- {v} votes: {k[:80]}..." for k, v in
366
- sorted(result['vote_distribution'].items(), key=lambda x: -x[1])[:3]]) if result['vote_distribution'] else ""
367
-
368
- return result['result'], stats, votes
369
- except Exception as e:
370
- return {"error": str(e)}, f"❌ {e}", ""
371
-
372
- def run_query(prompt, use_search, use_files, expected_keys, progress=gr.Progress()):
373
- return asyncio.run(run_query_async(prompt, use_search, use_files, expected_keys, progress))
374
-
375
- def handle_files(files):
376
- global loaded_files
377
- if not files:
378
- loaded_files = {}
379
- return "No files"
380
-
381
- loaded_files = {}
382
- results = []
383
- for f in files:
384
- info = asyncio.run(FileHandler.load_file(f.name))
385
- loaded_files[info['name']] = info
386
- if info['type'] == 'error':
387
- results.append(f"❌ {info['name']}: {info['content']}")
388
- elif info['type'] == 'image':
389
- results.append(f"🖼️ {info['name']}")
390
- else:
391
- results.append(f"✅ {info['name']} ({len(info.get('content', ''))} chars)")
392
 
393
- return "\n".join(results)
394
 
 
 
 
 
 
 
395
 
396
  # ============================================================================
397
  # UI
@@ -399,162 +228,47 @@ def handle_files(files):
399
 
400
  with gr.Blocks(title="MAKER Agent") as demo:
401
 
402
- gr.HTML("""
403
- <div style="text-align: center; padding: 20px 0;">
404
- <h1 class="header-title">🔧 MAKER Agent</h1>
405
- <p class="header-sub">Reliable AI with Voting & Red-Flagging | Based on arxiv.org/abs/2511.09030</p>
406
- </div>
407
- """)
408
 
409
- with gr.Tabs():
410
-
411
- # Setup Tab
412
- with gr.Tab("⚙️ Setup"):
413
- gr.Markdown("### Configure your LLM provider")
414
- with gr.Row():
415
- with gr.Column():
416
- provider = gr.Dropdown(
417
- ["openai", "anthropic", "groq", "together", "openrouter"],
418
- value="openai", label="Provider"
419
- )
420
- api_key = gr.Textbox(label="API Key", type="password", placeholder="sk-...")
421
- model = gr.Textbox(label="Model (optional)", placeholder="Leave blank for default")
422
-
423
- with gr.Column():
424
- k_votes = gr.Slider(1, 10, value=3, step=1, label="K (votes needed to win)",
425
- info="Higher = more reliable but slower")
426
- gr.Markdown("""
427
- ### How MAKER Works
428
- 1. **Voting**: Samples multiple responses, winner needs K votes ahead
429
- 2. **Red-Flagging**: Discards suspicious outputs (too long, malformed)
430
- 3. **Tools**: Optional web search for current information
431
- """)
432
-
433
- setup_btn = gr.Button("🚀 Create Agent", elem_classes="primary-btn")
434
- setup_status = gr.Markdown("👆 Enter your API key and click Create Agent to start")
435
- setup_btn.click(create_agent, [provider, api_key, model, k_votes], setup_status)
436
-
437
- # Query Tab
438
- with gr.Tab("💬 Query"):
439
- gr.Markdown("### Ask a question")
440
- with gr.Row():
441
- with gr.Column(scale=2):
442
- prompt = gr.Textbox(
443
- label="Your Query",
444
- lines=4,
445
- placeholder="Ask anything... The agent will use voting to ensure reliable answers.\n\nExample: What are the key factors for startup success? Return as JSON with keys: factors, explanation"
446
- )
447
- with gr.Row():
448
- use_search = gr.Checkbox(label="🔍 Enable Web Search", info="Search DuckDuckGo for current info")
449
- use_files = gr.Checkbox(label="📁 Use Uploaded Files", info="Include file content in context")
450
- expected_keys = gr.Textbox(
451
- label="Expected JSON keys (optional)",
452
- placeholder="answer, confidence, sources",
453
- info="Comma-separated list of required keys in response"
454
- )
455
- run_btn = gr.Button("▶️ Run Query", elem_classes="primary-btn")
456
-
457
- with gr.Column(scale=1):
458
- gr.Markdown("""### Example Queries
459
-
460
- **Simple Analysis:**
461
- ```
462
- What factors lead to startup success?
463
- ```
464
-
465
- **With Web Search:**
466
- ```
467
- What are the latest AI news this week?
468
- ```
469
-
470
- **With Expected Keys:**
471
- ```
472
- Analyze the pros and cons of remote work.
473
- Expected keys: pros, cons, recommendation
474
- ```
475
- """)
476
-
477
- gr.Markdown("---")
478
- gr.Markdown("### Results")
479
-
480
- with gr.Row():
481
- with gr.Column(scale=2):
482
- result_json = gr.JSON(label="Response")
483
- with gr.Column(scale=1):
484
- stats_md = gr.Markdown("*Run a query to see stats*")
485
- votes_md = gr.Markdown("")
486
-
487
- run_btn.click(
488
- run_query,
489
- [prompt, use_search, use_files, expected_keys],
490
- [result_json, stats_md, votes_md]
491
  )
 
492
 
493
- # Files Tab
494
- with gr.Tab("📁 Files"):
495
- gr.Markdown("### Upload files for analysis")
496
- gr.Markdown("Supported formats: PDF, DOCX, TXT, MD, JSON, CSV, PNG, JPG")
497
-
498
- file_upload = gr.File(
499
- label="Upload Files",
500
  file_count="multiple",
501
- file_types=[".pdf", ".docx", ".txt", ".md", ".json", ".csv", ".png", ".jpg", ".jpeg"]
 
 
502
  )
503
- file_status = gr.Markdown("*No files uploaded*")
504
- file_upload.change(handle_files, file_upload, file_status)
505
-
506
- gr.Markdown("""
507
- ### How to use files
508
- 1. Upload your files above
509
- 2. Go to the **Query** tab
510
- 3. Check **"Use Uploaded Files"**
511
- 4. Ask questions about your documents!
512
- """)
513
-
514
- # About Tab
515
- with gr.Tab("ℹ️ About"):
516
- gr.Markdown("""
517
- ## About MAKER Framework
518
-
519
- **MAKER** (Massively Decomposed Agentic Processes) achieves near-zero errors through:
520
-
521
- | Pillar | Description |
522
- |--------|-------------|
523
- | **Maximal Decomposition** | Break tasks into single-step atomic operations |
524
- | **K-Voting** | Sample multiple times, winner needs K votes ahead |
525
- | **Red-Flagging** | Discard suspicious outputs (don't try to repair them) |
526
-
527
- ### Key Insight
528
-
529
- > *"Reliability is an engineering problem, not a model problem."*
530
-
531
- Instead of waiting for better models, you can achieve near-zero errors TODAY using smaller, cheaper models with statistical voting.
532
-
533
- ### Results from the Paper
534
-
535
- The researchers achieved **1,000,000 steps with ZERO errors** using gpt-4.1-mini!
536
-
537
- ### Links
538
-
539
- - 📄 **Paper**: [arxiv.org/abs/2511.09030](https://arxiv.org/abs/2511.09030)
540
- - 🎥 **Video Explanation**: [YouTube](https://youtube.com/watch?v=TJ-vWGCosdQ)
541
-
542
- ### Supported LLM Providers
543
-
544
- | Provider | Example Models |
545
- |----------|----------------|
546
- | OpenAI | gpt-4o-mini, gpt-4o |
547
- | Anthropic | claude-sonnet, claude-opus |
548
- | Groq | llama-3.3-70b (very fast!) |
549
- | Together | Llama, Mistral, Qwen |
550
- | OpenRouter | 100+ models |
551
- """)
552
 
553
- gr.HTML("""
554
- <div style="text-align:center; color:#64748b; padding:20px; border-top: 1px solid #e2e8f0; margin-top: 20px;">
555
- MAKER Framework | Based on <a href="https://arxiv.org/abs/2511.09030" style="color:#6366f1">arxiv.org/abs/2511.09030</a>
556
- </div>
557
- """)
558
 
559
- if __name__ == "__main__":
560
- demo.launch()
 
1
  """
2
+ MAKER Agent - Clean Chat Interface
 
 
 
3
  """
4
 
5
  import gradio as gr
6
  import asyncio
 
 
 
7
  from collections import Counter
8
+ from dataclasses import dataclass
9
+ from typing import Callable
10
  from pathlib import Path
11
 
12
  # ============================================================================
13
+ # Core
14
  # ============================================================================
15
 
16
  @dataclass
17
  class VotingConfig:
18
  k: int = 3
19
  max_samples: int = 30
 
20
  temperature_rest: float = 0.1
21
  parallel_samples: int = 3
22
 
 
 
 
 
 
 
23
 
24
  class LLMClient:
 
 
25
  def __init__(self, provider: str, api_key: str, model: str = None):
26
  self.provider = provider.lower()
27
  self.api_key = api_key
28
  self.model = model
29
  self._client = None
30
+ self._setup()
31
 
32
+ def _setup(self):
33
  if self.provider == "openai":
34
  from openai import AsyncOpenAI
35
  self._client = AsyncOpenAI(api_key=self.api_key)
 
51
  self._client = AsyncOpenAI(api_key=self.api_key, base_url="https://openrouter.ai/api/v1")
52
  self.model = self.model or "openai/gpt-4o-mini"
53
 
54
+ async def generate(self, messages: list, temperature: float = 0.0) -> str:
55
  if self.provider == "anthropic":
56
+ system = ""
57
+ conv = []
58
+ for m in messages:
59
+ if m["role"] == "system":
60
+ system = m["content"]
61
+ else:
62
+ conv.append(m)
63
+ kwargs = {"model": self.model, "max_tokens": 2000, "messages": conv}
64
+ if system:
65
+ kwargs["system"] = system
66
+ r = await self._client.messages.create(**kwargs)
67
  return r.content[0].text
68
  else:
69
  r = await self._client.chat.completions.create(
70
+ model=self.model, messages=messages,
71
+ temperature=temperature, max_tokens=2000
 
72
  )
73
  return r.choices[0].message.content
74
 
75
 
76
  class WebSearch:
 
 
77
  @staticmethod
78
+ async def search(query: str) -> str:
79
  try:
80
  from duckduckgo_search import DDGS
81
  results = []
82
  with DDGS() as ddgs:
83
+ for r in ddgs.text(query, max_results=5):
84
+ results.append(f"• {r.get('title', '')}: {r.get('body', '')}")
85
+ return "\n".join(results) if results else ""
86
+ except:
87
+ return ""
 
 
 
 
88
 
89
 
90
  class FileHandler:
 
 
91
  @staticmethod
92
+ def load(path: str) -> dict:
93
+ p = Path(path)
94
+ ext = p.suffix.lower()
 
95
  try:
96
+ if ext == '.pdf':
97
+ import pymupdf
98
+ doc = pymupdf.open(str(p))
99
+ text = "\n".join([page.get_text() for page in doc])
100
+ doc.close()
101
+ return {"name": p.name, "content": text[:20000]}
 
 
 
 
 
 
 
 
102
  elif ext == '.docx':
103
+ from docx import Document
104
+ doc = Document(str(p))
105
+ text = "\n".join([para.text for para in doc.paragraphs])
106
+ return {"name": p.name, "content": text[:20000]}
 
 
 
 
 
 
 
 
 
107
  else:
108
+ return {"name": p.name, "content": p.read_text(errors='replace')[:20000]}
 
 
109
  except Exception as e:
110
+ return {"name": p.name, "content": f"[Error reading file: {e}]"}
111
 
112
 
113
  class MAKERAgent:
114
+ def __init__(self, llm: LLMClient, voting: VotingConfig = None):
 
 
115
  self.llm = llm
116
  self.voting = voting or VotingConfig()
117
+ self.history = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ async def chat(self, message: str, search: bool = False, files: str = None) -> str:
120
+ messages = [{"role": "system", "content": "You are a helpful assistant." + (f"\n\nFiles:\n{files}" if files else "")}]
121
+ messages.extend(self.history[-10:])
122
 
123
+ user_content = message
124
+ if search:
125
+ results = await WebSearch.search(message)
126
+ if results:
127
+ user_content += f"\n\n[Web search results]\n{results}"
128
 
129
+ messages.append({"role": "user", "content": user_content})
 
 
130
 
131
+ # Voting
132
  votes: Counter = Counter()
133
+ samples = 0
 
 
134
 
135
+ response = await self.llm.generate(messages, temperature=0.0)
 
 
 
136
  samples += 1
137
+ votes[response] += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
 
 
139
  while samples < self.voting.max_samples:
140
+ top = votes.most_common(2)
141
+ if top[0][1] - (top[1][1] if len(top) > 1 else 0) >= self.voting.k:
142
+ break
 
 
 
 
 
 
 
143
 
144
  for _ in range(self.voting.parallel_samples):
145
  if samples >= self.voting.max_samples:
146
  break
147
+ r = await self.llm.generate(messages, temperature=self.voting.temperature_rest)
 
148
  samples += 1
149
+ votes[r] += 1
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ winner = votes.most_common(1)[0][0] if votes else "Sorry, I couldn't generate a response."
 
152
 
153
+ self.history.append({"role": "user", "content": message})
154
+ self.history.append({"role": "assistant", "content": winner})
 
 
 
 
 
 
155
 
156
+ return winner
157
+
158
+ def clear(self):
159
+ self.history = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
 
 
 
 
 
161
 
162
  # ============================================================================
163
+ # State
164
  # ============================================================================
165
 
166
+ agent = None
167
+ file_content = ""
168
 
169
+ def connect(provider, key, model, k):
170
+ global agent
171
+ if not key.strip():
172
+ return "❌ Enter API key"
173
  try:
174
+ agent = MAKERAgent(LLMClient(provider, key.strip(), model.strip() or None), VotingConfig(k=k))
175
+ return f"✅ Connected: {agent.llm.model}"
 
176
  except Exception as e:
177
+ return f"❌ {e}"
178
+
179
+ def load_files(files):
180
+ global file_content
181
+ if not files:
182
+ file_content = ""
183
+ return "No files"
184
+
185
+ parts = []
186
+ names = []
187
+ for f in files:
188
+ data = FileHandler.load(f.name)
189
+ parts.append(f"[{data['name']}]\n{data['content']}")
190
+ names.append(data['name'])
191
+
192
+ file_content = "\n\n".join(parts)
193
+ return f"📎 {', '.join(names)}"
194
 
195
+ def respond(message, history, search, files):
196
+ global agent, file_content
197
 
198
+ if files:
199
+ load_files(files)
200
 
201
+ if not agent:
202
+ return history + [[message, "⚠️ Connect to an LLM first (open Settings below)"]], ""
 
 
 
203
 
204
+ if not message.strip():
205
+ return history, ""
206
 
207
+ async def run():
208
+ return await agent.chat(message.strip(), search, file_content or None)
209
 
210
+ loop = asyncio.new_event_loop()
211
  try:
212
+ response = loop.run_until_complete(run())
213
+ finally:
214
+ loop.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
+ return history + [[message, response]], ""
217
 
218
+ def clear():
219
+ global agent, file_content
220
+ if agent:
221
+ agent.clear()
222
+ file_content = ""
223
+ return [], None
224
 
225
  # ============================================================================
226
  # UI
 
228
 
229
  with gr.Blocks(title="MAKER Agent") as demo:
230
 
231
+ gr.Markdown("## 🔧 MAKER Agent")
 
 
 
 
 
232
 
233
+ chatbot = gr.Chatbot(height=450, show_label=False, bubble_full_width=False)
234
+
235
+ # Input row with everything together
236
+ with gr.Group():
237
+ with gr.Row():
238
+ msg = gr.Textbox(
239
+ placeholder="Message MAKER...",
240
+ show_label=False,
241
+ scale=10,
242
+ container=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  )
244
+ send = gr.Button("↑", variant="primary", scale=1, min_width=50)
245
 
246
+ with gr.Row():
247
+ files = gr.File(
 
 
 
 
 
248
  file_count="multiple",
249
+ file_types=[".pdf", ".docx", ".txt", ".md", ".json", ".csv", ".py"],
250
+ label="",
251
+ scale=3
252
  )
253
+ search = gr.Checkbox(label="🔍 Web search", scale=1)
254
+ clear_btn = gr.Button("Clear chat", scale=1)
255
+
256
+ # Settings
257
+ with gr.Accordion("⚙️ Settings", open=False):
258
+ with gr.Row():
259
+ provider = gr.Dropdown(["groq", "openai", "anthropic", "together", "openrouter"], value="groq", label="Provider")
260
+ api_key = gr.Textbox(label="API Key", type="password")
261
+ model = gr.Textbox(label="Model", placeholder="default")
262
+ k = gr.Slider(1, 7, value=3, step=1, label="K (reliability)")
263
+ with gr.Row():
264
+ connect_btn = gr.Button("Connect", variant="primary")
265
+ status = gr.Markdown("*Not connected*")
266
+ gr.Markdown("Free API: [console.groq.com](https://console.groq.com) • [Paper](https://arxiv.org/abs/2511.09030)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ # Events
269
+ connect_btn.click(connect, [provider, api_key, model, k], status)
270
+ msg.submit(respond, [msg, chatbot, search, files], [chatbot, msg])
271
+ send.click(respond, [msg, chatbot, search, files], [chatbot, msg])
272
+ clear_btn.click(clear, None, [chatbot, files])
273
 
274
+ demo.launch()