Spaces:
Running on Zero
Running on Zero
File size: 49,048 Bytes
1e214ed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 | """
EIM Chat - a multimodal chat app on top of the EIM engine
============================================================
python chat_app.py start the app
python chat_app.py --selftest check the core without a model, GPU or Gradio
What it does
* Chat with streaming answers, in the language of the user's message (Arabic or English).
* Attach files in the same message: code, text, PDF, Word, Excel/CSV, notebooks, zip, images.
Big files are not blindly truncated: a BM25 retriever picks the parts relevant to each question.
* Images: shown in the chat; understood through an optional vision model (EIM_VISION_MODEL); without one the app
says so honestly instead of inventing what is in the picture. OCR is used when pytesseract is installed.
* Verified code: whenever the message contains tests (assert lines, a fenced test block, or an attached
test_*.py), the EIM+ loop runs instead of plain chat: it can start from YOUR code, repairs it in the sandbox,
checks the result on hidden tests, flags hard-coded answers, and gives you solution.py to download.
* Mode switch: Auto / Chat / EIM.
Environment
EIM_MODEL code model used by the EIM loop (default from app.py)
EIM_CHAT_MODEL model for conversation (default: the same model; a bigger instruct model chats much better,
for example Qwen/Qwen2.5-7B-Instruct)
EIM_VISION_MODEL optional vision-language model, for example Qwen/Qwen2-VL-2B-Instruct (not loaded unless set)
EIM_CONTEXT_TOKENS prompt budget for chat history + file excerpts (default 6000, or 4000 on weak machines)
EIM_PROFILE lean | balanced | full - force the hardware profile (default: detected from CPU / RAM / GPU)
EIM_TIME_BUDGET seconds after which the EIM loop starts no new round and returns its best version (default: off)
EIM_GPU_SECONDS ZeroGPU only: the `duration` of the @spaces.GPU call in app.py (default 60). The EIM loop stops
starting new rounds at 65% of it and returns its best version instead of being cut off
EIM_MEMORY_REPO optional private HF dataset ("user/name", needs HF_TOKEN) that keeps the learned memory across restarts
EIM_REPAIR_LOG 0 disables the verified-repair log (eim_repairs.jsonl next to the memory file; EIM_REPAIR_LOG_PATH moves it)
EIM_RESULT_CACHE 0 disables replaying identical, fully successful EIM runs from memory
Security: uploaded files are only READ as text. Code is executed only by the EIM sandbox (see app.py for its limits;
for public traffic run everything in a container without network access).
"""
from __future__ import annotations
import ast
import os
import re
import sys
import tempfile
import threading
import time
from dataclasses import dataclass, field
from typing import Iterator, Protocol, Sequence
from app import CFG, EUTV, Config, ExperienceMemory, Step, gpu, get_lm
from eim_plus import (Attachment, EIMPlus, Report, SmartMemory, hardware_profile, is_test_file, read_attachment,
select_context, tests_from_source)
from correction_memory import CorrectionMemory
def _accepts(function, name: str) -> bool:
try:
import inspect
return name in inspect.signature(function).parameters
except (TypeError, ValueError):
return False
_GEN_LOCK = threading.Lock() # one generation at a time on the shared model
# ============================================================================
# 1. Language helpers
# ============================================================================
STR = {
"reading_image": ("🔍 Reading the image…", "🔍 جارٍ قراءة الصورة…"),
"thinking": ("⏳ Thinking…", "⏳ جارٍ التفكير…"),
"eim_start": ("⏳ Generating the first version and verifying it in the sandbox…",
"⏳ جارٍ توليد النسخة الأولى والتحقق منها في البيئة المعزولة…"),
"eim_needs_tests": ("EIM mode needs tests. Add lines such as `assert f(2) == 4`, or attach a `test_*.py` file.",
"وضع EIM يحتاج اختبارات. أضف أسطراً مثل `assert f(2) == 4` أو أرفق ملف `test_*.py`."),
"default_q": ("Summarise the attached files and tell me what is in them.",
"لخّص الملفات المرفقة وأخبرني بما فيها."),
"ctx_head": ("Content of the files the user attached (use it; say so if the answer is not in it):",
"محتوى الملفات التي أرفقها المستخدم (استخدمه، وصرّح إن لم تكن الإجابة فيه):"),
"ctx_trunc": ("(Large files: only the parts most relevant to the question are shown.)",
"(الملفات كبيرة: عُرضت فقط الأجزاء الأقرب لسؤال المستخدم.)"),
"no_vision": ("An image named {name} ({w}x{h}) was attached, but no vision model is configured, so its content is unknown. "
"Do not guess what it shows.",
"أُرفقت صورة باسم {name} ({w}x{h}) لكن لا يوجد نموذج رؤية مُفعّل، فمحتواها مجهول. لا تخمّن ما فيها."),
"ocr": ("Text found in the image by OCR:", "نص مستخرج من الصورة بالـ OCR:"),
"error": ("⚠️ Something went wrong: {err}", "⚠️ حدث خطأ: {err}"),
"ok": ("✅ All visible tests pass", "✅ نجحت كل الاختبارات الظاهرة"),
"partial": ("⚠️ Best version found: {p}/{t} visible tests", "⚠️ أفضل نسخة وُجدت: {p}/{t} من الاختبارات الظاهرة"),
"reward": ("Reward", "المكافأة"),
"visible": ("Visible tests", "الاختبارات الظاهرة"),
"hidden": ("Hidden tests", "الاختبارات المخفية"),
"iters": ("Iterations", "التكرارات"),
"restarts": ("Restarts", "إعادات البدء"),
"final_code": ("Final code", "الكود النهائي"),
"file_note": ("Files you can download: solution.py", "ملف للتنزيل: solution.py"),
"attached": ("[attached: {names}]", "[مرفق: {names}]"),
"skipped": ("{n} test(s) using pytest fixtures/classes were skipped.", "تم تخطي {n} اختبار يعتمد على fixtures/classes الخاصة بـ pytest."),
}
def detect_lang(text: str) -> str:
return "ar" if re.search(r"[\u0600-\u06FF]", text or "") else "en"
def tr(key: str, lang: str, **kwargs) -> str:
en, ar = STR[key]
return (ar if lang == "ar" else en).format(**kwargs)
SYS_CHAT = (
"You are EIM Assistant, a careful, capable assistant for programming and for working with documents. "
"Always answer in the language of the user's latest message. "
"When file excerpts are provided, ground your answer in them and mention the file name; if the answer is not in them, say so. "
"Never invent what an image shows: rely only on the image description you are given. "
"For code, give complete, runnable code in fenced blocks and explain briefly. "
"Write general solutions that are correct for every valid input, not only for the examples shown: never embed "
"expected values taken from tests, never special-case test inputs, and never build lookup tables of answers. "
"Think about edge cases first (empty input, negatives, duplicates, boundaries). "
"Keep the function names and signatures that the user's code or tests use, and when fixing code change only what "
"is needed. Prefer the standard library and make no network calls at import time. "
"Treat explicit user corrections and rejected approaches as constraints: do not repeat an approach the user said "
"was wrong; use the correction in the next answer, and briefly state what changed. Before claiming code works, "
"separate tests actually run from reasoning or unverified assumptions. "
"If the user wants code you can be sure about, suggest they add `assert` tests: the app then runs the code against "
"them in a sandbox and tries to repair it."
)
# ============================================================================
# 2. Model backends
# ============================================================================
class ChatBackend(Protocol):
def stream(self, messages: list[dict], max_new_tokens: int, temperature: float) -> Iterator[str]: ...
class VisionBackend(Protocol):
def describe(self, image_path: str, question: str) -> str: ...
class LockedCodeLM:
"""The EIM loop's model: lazy-loaded, and serialised with chat generation."""
def generate(self, prompts: Sequence[str], temperature: float, max_new_tokens: int) -> list[str]:
with _GEN_LOCK:
return get_lm().generate(prompts, temperature, max_new_tokens)
def _remote_chat_prompt(messages: list[dict], max_chars: int, protect: int = 0) -> str:
"""Serialise role-tagged chat for APIs that currently accept a single user prompt."""
cleaned = []
for message in messages:
role = str(message.get("role", "user")).upper()[:20]
content = str(message.get("content", ""))
cleaned.append((role, content))
if not cleaned:
return "You are a helpful assistant."
# Keep the system instruction and newest question; discard oldest history first.
system = cleaned[0] if cleaned[0][0] == "SYSTEM" else ("SYSTEM", "You are a helpful assistant.")
remaining = max(500, max_chars - len(system[1]) - 32)
kept = []
for role, content in reversed(cleaned[1:] if cleaned[0][0] == "SYSTEM" else cleaned):
if remaining <= 0:
break
if len(content) > remaining:
if role == "USER" and protect > 0:
tail = min(protect, len(content), remaining // 2)
head = max(0, remaining - tail - 20)
content = content[:head] + "\n[… earlier context trimmed …]\n" + content[-tail:] if tail else content[:remaining]
else:
content = content[-remaining:]
kept.append((role, content))
remaining -= len(content) + len(role) + 8
kept.reverse()
rows = [f"SYSTEM:\n{system[1]}"]
rows.extend(f"{role}:\n{content}" for role, content in kept)
rows.append("ASSISTANT:")
return "\n\n".join(rows)[-max_chars - 64:]
class HFChat:
"""Streaming chat on a Hugging Face causal LM. Reuses the EIM model when no separate chat model is configured."""
def __init__(self, name: str, max_context_tokens: int = 6000):
self.name, self.max_context_tokens = name, max_context_tokens
self._model = self._tok = None
self._remote_client = None
def _load(self) -> None:
if self._model is not None or self._tok is not None or self._remote_client is not None:
return
if self.name == CFG.model_name:
shared = get_lm()
else:
from app import CodeLM
shared = CodeLM(self.name)
if getattr(shared, "is_remote", False):
self._remote_client = shared
return
self._model, self._tok = shared.model, shared.tokenizer
def _length(self, messages: list[dict]) -> int:
text = self._tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
return len(self._tok(text, add_special_tokens=False).input_ids)
def _fit(self, messages: list[dict], protect: int = 0, budget: int | None = None) -> list[dict]:
"""Trim the prompt to `budget` tokens (default: max_context_tokens).
protect = number of characters at the END of the newest message that must never be cut: that is the user's own
question (file excerpts come before it). Excerpts are shrunk first; only if the question alone is still too long
is it shortened, and then from the middle, so its beginning and its end both survive."""
limit = self.max_context_tokens if budget is None else budget
msgs = list(messages)
if len(msgs) > 2 and self._length(msgs) > limit:
# Drop the oldest turns (keep system + newest). Same result as dropping one turn at a time, but found by
# binary search: ~log2(n) tokenisations of the whole prompt instead of up to n - on a weak CPU that is
# the difference between an instant and a multi-second wait before the first token.
lo, hi = 1, len(msgs) - 2 # d = number of oldest turns dropped after the system turn
while lo < hi:
mid = (lo + hi) // 2
if self._length(msgs[:1] + msgs[1 + mid:]) <= limit:
hi = mid
else:
lo = mid + 1
msgs = msgs[:1] + msgs[1 + lo:]
for _ in range(8): # still too long: shrink the newest message
if self._length(msgs) <= limit:
break
content = msgs[-1]["content"]
keep = min(max(0, protect), len(content))
body, tail = content[: len(content) - keep], content[len(content) - keep:]
if protect and body:
content = body[: len(body) // 2] + tail # cut the excerpts, keep the question whole
elif protect:
quarter = max(1, len(tail) // 4) # nothing left to cut but the question itself
content = tail[:quarter] + " ... " + tail[-quarter:]
else:
content = content[: len(content) // 2]
msgs[-1] = {**msgs[-1], "content": content}
return msgs
def stream(self, messages: list[dict], max_new_tokens: int = 900, temperature: float = 0.4,
protect: int = 0) -> Iterator[str]:
self._load()
if self._remote_client is not None:
limit = max(2000, min(24000, int(os.environ.get("EIM_REMOTE_CONTEXT_CHARS", "14000"))))
prompt = _remote_chat_prompt(messages, limit, protect)
answer = self._remote_client.generate([prompt], temperature, max_new_tokens)[0]
# Inference Providers return a completed response; chunk it for the same Gradio interface contract.
for start in range(0, len(answer), 80):
yield answer[start:start + 80]
if not answer:
yield ""
return
import torch
from transformers import StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer
tok, model = self._tok, self._model
window = getattr(getattr(model, "config", None), "max_position_embeddings", None)
budget = self.max_context_tokens
if isinstance(window, int) and window > 0: # prompt + answer must fit the model's own window
budget = max(256, min(budget, window - int(max_new_tokens) - 64))
text = tok.apply_chat_template(self._fit(messages, protect, budget), tokenize=False, add_generation_prompt=True)
inputs = tok(text, return_tensors="pt", add_special_tokens=False).to(model.device)
class _Stop(StoppingCriteria):
flag = False
def __call__(self, input_ids, scores, **kwargs):
return torch.full((input_ids.shape[0],), self.flag, dtype=torch.bool, device=input_ids.device)
stop = _Stop()
streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True, timeout=600)
options = dict(**inputs, streamer=streamer, max_new_tokens=int(max_new_tokens),
pad_token_id=tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id,
stopping_criteria=StoppingCriteriaList([stop]))
if temperature > 0:
options.update(do_sample=True, temperature=float(temperature), top_p=0.95)
else:
options.update(do_sample=False)
errors: list[BaseException] = []
def work() -> None:
try:
with torch.inference_mode():
model.generate(**options)
except BaseException as exc: # surface it instead of hanging the stream
errors.append(exc)
streamer.end()
with _GEN_LOCK:
thread = threading.Thread(target=work, daemon=True)
thread.start()
try:
for piece in streamer:
yield piece
finally: # also runs when the user presses Stop
# The flag stays True: this _Stop class belongs to this call only, and clearing it after a join timeout
# would let a still-running generation continue in the background and overlap the next one.
_Stop.flag = True
thread.join(timeout=30)
if errors:
raise errors[0]
class HFVision:
"""Optional vision-language model (Qwen2-VL style). NOT loaded unless EIM_VISION_MODEL is set."""
def __init__(self, name: str):
self.name = name
self._model = self._processor = None
def _load(self) -> None:
if self._model is not None:
return
import torch
from transformers import AutoProcessor
try:
from transformers import AutoModelForImageTextToText as AutoVision
except ImportError:
from transformers import AutoModelForVision2Seq as AutoVision
self._processor = AutoProcessor.from_pretrained(self.name)
use_cuda = torch.cuda.is_available() or bool(os.environ.get("SPACES_ZERO_GPU"))
self._model = AutoVision.from_pretrained(self.name, torch_dtype=torch.bfloat16 if use_cuda else torch.float32)
if use_cuda:
self._model.to("cuda")
self._model.eval()
def describe(self, image_path: str, question: str = "") -> str:
import torch
from PIL import Image
self._load()
image = Image.open(image_path).convert("RGB")
image.thumbnail((1024, 1024))
prompt = (f"The user asks: {question}\nAnswer using the image, and also transcribe any visible text."
if question.strip() else
"Describe this image in detail. Transcribe any visible text exactly. Mention charts, tables and code if present.")
messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": prompt}]}]
text = self._processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = self._processor(text=[text], images=[image], return_tensors="pt").to(self._model.device)
with _GEN_LOCK, torch.inference_mode():
output = self._model.generate(**inputs, max_new_tokens=500, do_sample=False)
return self._processor.batch_decode(output[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0].strip()
def ocr_text(path: str) -> str:
try:
import pytesseract
from PIL import Image
with Image.open(path) as image:
return pytesseract.image_to_string(image, lang="ara+eng").strip()
except Exception:
return ""
# ============================================================================
# 3. Session, options, intent
# ============================================================================
@dataclass
class Session:
history: list[dict] = field(default_factory=list) # what the chat model sees (plain text turns)
files: dict[str, Attachment] = field(default_factory=dict)
working_code: str = "" # latest uploaded .py or EIM result
workdir: str = ""
result_path: str | None = None
runs: int = 0
@dataclass
class Options:
mode: str = "auto" # auto | chat | eim
iterations: int = 4
candidates: int = 3
holdout: float = 0.25
temperature: float = 0.4
max_new_tokens: int = 900
context_chars: int = 6000
@dataclass
class Intent:
kind: str # chat | eim | need_tests
task: str = ""
tests: list[str] = field(default_factory=list)
initial_code: str = ""
skipped: int = 0
_FENCE = re.compile(r"```[ \t]*([\w+-]*)[ \t]*\n(.*?)```", re.S)
def split_message(text: str) -> tuple[str, list[str]]:
"""(prose without code blocks, python-ish fenced blocks)"""
blocks = [m.group(2) for m in _FENCE.finditer(text) if m.group(1).lower() in ("", "python", "py")]
return _FENCE.sub("", text).strip(), blocks
def _looks_like_code(block: str) -> bool:
return bool(re.search(r"^\s*(def |class |import |from \S+ import )", block, re.M))
def _defined_names(code: str) -> set[str]:
try:
return {n.name for n in ast.parse(code).body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))}
except (SyntaxError, ValueError):
return set()
def _used_names(tests: Sequence[str]) -> set[str]:
used: set[str] = set()
for test in tests:
try:
used |= {n.id for n in ast.walk(ast.parse(test)) if isinstance(n, ast.Name)}
except (SyntaxError, ValueError):
pass
return used
def detect_intent(text: str, new_atts: Sequence[Attachment], session: Session, mode: str) -> Intent:
prose, blocks = split_message(text)
tests: list[str] = []
skipped = 0
initial = ""
for block in blocks:
found, skip = tests_from_source(block)
skipped += skip
if found:
tests += found
elif _looks_like_code(block) and not initial:
initial = block
prose_lines = prose.splitlines()
tests += [line.strip() for line in prose_lines if line.strip().startswith("assert ")]
task = "\n".join(line for line in prose_lines if not line.strip().startswith("assert ")).strip()
spec_parts = []
for att in new_atts:
if att.kind == "code" and att.name.lower().endswith(".py"):
found, skip = tests_from_source(att.text)
skipped += skip
if is_test_file(att.name, att.text, found):
tests += found
elif not initial:
initial = att.text
elif att.kind in ("text", "document") and att.text:
spec_parts.append(f"Specification from {att.name}:\n{att.text[:3000]}")
if mode == "chat":
return Intent("chat")
if not tests:
return Intent("need_tests") if mode == "eim" else Intent("chat")
if not initial and session.working_code and _defined_names(session.working_code) & _used_names(tests):
initial = session.working_code # the tests talk about code we already have
if not task:
task = "Fix the code so that all tests pass." if initial else "Write code that passes all the tests."
if spec_parts:
task += "\n\n" + "\n\n".join(spec_parts)
return Intent("eim", task, tests, initial, skipped)
# ============================================================================
# 4. Assistant: the whole behaviour, independent of Gradio
# ============================================================================
def render_progress(steps: Sequence[Step], lang: str, running: bool = True) -> str:
rows = ["| # | Status | Strategy | Tests | Reward |", "|---|---|---|---|---|"]
for s in steps:
rows.append(f"| {s.iteration} | {s.label} | {s.strategy} | {s.result.passed}/{s.result.total} | {s.score.reward:.3f} |")
accepted = [s for s in steps if s.accepted]
code = f"\n\n```python\n{accepted[-1].code}\n```" if accepted else ""
return ("\n".join(rows) + code + (f"\n\n{tr('thinking', lang)}" if running else ""))
def render_final(report: Report, steps: Sequence[Step], lang: str, skipped: int) -> str:
best = report.best
ok = best.result.pass_rate == 1.0
head = tr("ok", lang) if ok else tr("partial", lang, p=best.result.passed, t=best.result.total)
facts = [
f"**{tr('reward', lang)}:** {report.first.score.reward:.3f} → {best.score.reward:.3f}",
f"**{tr('visible', lang)}:** {best.result.passed}/{best.result.total}",
]
if report.hidden is not None:
facts.append(f"**{tr('hidden', lang)}:** {report.hidden.passed}/{report.hidden.total}")
facts.append(f"**{tr('iters', lang)}:** {report.iterations} · **{tr('restarts', lang)}:** {report.restarts}")
notes = [f"- ⚠️ {w}" for w in report.warnings]
if skipped:
notes.append(f"- ℹ️ {tr('skipped', lang, n=skipped)}")
parts = [f"### {head}", " · ".join(facts)]
if notes:
parts.append("\n".join(notes))
parts += [f"#### {tr('final_code', lang)}", f"```python\n{best.code}\n```", f"_{tr('file_note', lang)}_",
"<details><summary>log</summary>\n\n" + render_progress(steps, lang, running=False) + "\n\n</details>"]
return "\n\n".join(parts)
class Assistant:
def __init__(self, chat: ChatBackend, vision: VisionBackend | None = None, code_lm=None,
verifier: EUTV | None = None, memory: ExperienceMemory | None = None, cfg: Config = CFG):
self.chat, self.vision, self.cfg = chat, vision, cfg
self.code_lm = code_lm or LockedCodeLM()
self.verifier = verifier or EUTV(cfg)
self.memory = memory if memory is not None else SmartMemory(path=cfg.memory_path)
self.corrections = CorrectionMemory()
# -------------------------------------------------------------- public
def handle(self, session: Session, text: str, paths: Sequence[str], opts: Options) -> Iterator[str]:
"""Yields the FULL assistant message so far, each time it grows."""
text = (text or "").strip()
lang = detect_lang(text) if text else "en"
new_atts = self._ingest(session, paths)
if not text and not new_atts:
return
try:
for att in new_atts:
if att.kind == "image":
yield tr("reading_image", lang)
att.text = self._describe(att, text, lang)
if not text:
text = tr("default_q", lang)
if not paths and not text:
return
intent = detect_intent(text, new_atts, session, opts.mode)
if intent.kind == "need_tests":
yield tr("eim_needs_tests", lang)
elif intent.kind == "eim":
yield from self._run_eim(session, text, intent, opts, lang, new_atts)
else:
yield from self._run_chat(session, text, opts, lang, new_atts)
except Exception as exc:
yield tr("error", lang, err=f"{type(exc).__name__}: {exc}")
# -------------------------------------------------------------- internals
def _ingest(self, session: Session, paths: Sequence[str]) -> list[Attachment]:
out = []
for path in paths:
att = read_attachment(path)
base, ext, n = att.name, os.path.splitext(att.name)[1], 1
while att.name in session.files: # same name uploaded twice: keep both
n += 1
att.name = f"{os.path.splitext(base)[0]} ({n}){ext}"
session.files[att.name] = att
if att.kind == "code" and att.name.lower().endswith(".py"):
found, _ = tests_from_source(att.text)
if not is_test_file(att.name, att.text, found):
session.working_code = att.text
out.append(att)
return out
def _describe(self, att: Attachment, question: str, lang: str) -> str:
width, height = att.image_size or (0, 0)
if self.vision is not None:
try:
return f"[image {att.name}] " + self.vision.describe(att.path, question)
except Exception as exc:
return f"[image {att.name}: the vision model failed ({type(exc).__name__}: {exc})]"
text = tr("no_vision", lang, name=att.name, w=width, h=height)
ocr = ocr_text(att.path)
return text + (f"\n{tr('ocr', lang)}\n{ocr}" if ocr else "")
def _run_chat(self, session: Session, text: str, opts: Options, lang: str, new_atts) -> Iterator[str]:
# Persist explicit rejections/corrections before building this turn's prompt.
self.corrections.add(text)
context, truncated = select_context(list(session.files.values()), text, opts.context_chars)
parts = []
if context:
parts.append(f"{tr('ctx_head', lang)}\n{context}")
if truncated:
parts.append(tr("ctx_trunc", lang))
parts.append(text)
correction_context = self.corrections.prompt(text)
system_text = SYS_CHAT + ("\n\n" + correction_context if correction_context else "")
messages = [{"role": "system", "content": system_text}] + session.history[-40:] + \
[{"role": "user", "content": "\n\n".join(parts)}]
yield tr("thinking", lang)
answer = ""
last = 0.0
extra = {"protect": len(text)} if _accepts(self.chat.stream, "protect") else {} # custom chat models may not
for piece in self.chat.stream(messages, opts.max_new_tokens, opts.temperature, **extra):
answer += piece
now = time.monotonic()
if now - last > 0.05: # throttle UI updates
last = now
yield answer
yield answer
names = ", ".join(a.name for a in new_atts)
shown = text + (("\n" + tr("attached", lang, names=names)) if names else "")
session.history += [{"role": "user", "content": shown}, {"role": "assistant", "content": answer}]
def _run_eim(self, session: Session, text: str, intent: Intent, opts: Options, lang: str, new_atts) -> Iterator[str]:
yield tr("eim_start", lang)
self.corrections.add(text)
correction_context = self.corrections.prompt(text)
task = intent.task + ("\n\n" + correction_context if correction_context else "")
engine = EIMPlus(self.code_lm, self.verifier, self.cfg, self.memory)
steps: list[Step] = []
report: Report | None = None
for event in engine.run(task, intent.tests, intent.initial_code or None,
int(opts.iterations), int(opts.candidates), float(opts.holdout)):
if isinstance(event, Report):
report = event
break
steps.append(event)
yield render_progress(steps, lang, running=True)
assert report is not None
final = render_final(report, steps, lang, intent.skipped)
session.runs += 1
if not session.workdir:
session.workdir = tempfile.mkdtemp(prefix="eimchat_")
path = os.path.join(session.workdir, f"solution_{session.runs}.py")
with open(path, "w", encoding="utf-8") as handle:
handle.write(report.best.code + "\n")
session.result_path = path
session.working_code = report.best.code
yield final
names = ", ".join(a.name for a in new_atts)
shown = text + (("\n" + tr("attached", lang, names=names)) if names else "")
summary = f"{tr('final_code', lang)} ({report.best.result.passed}/{report.best.result.total}):\n```python\n{report.best.code}\n```"
session.history += [{"role": "user", "content": shown}, {"role": "assistant", "content": summary}]
# ============================================================================
# 5. Gradio UI
# ============================================================================
CSS = """
#chat .message, #chat .prose, #chat .message-content { unicode-bidi: plaintext; text-align: start; }
#chat pre, #chat code { direction: ltr; unicode-bidi: isolate; text-align: left; }
footer { display: none !important; }
"""
ACCEPTED_FILES = [".py", ".txt", ".md", ".json", ".csv", ".tsv", ".pdf", ".docx", ".xlsx", ".ipynb", ".zip",
".js", ".ts", ".java", ".c", ".cpp", ".go", ".rs", ".html", ".css", ".sql", ".yaml", ".yml",
".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"]
_ASSISTANT: Assistant | None = None
_ASSISTANT_LOCK = threading.Lock()
def get_assistant() -> Assistant:
global _ASSISTANT
with _ASSISTANT_LOCK:
if _ASSISTANT is None:
# prompt prefill dominates latency on a CPU: weak machines get a smaller default window (env still wins)
default_context = "4000" if hardware_profile().name == "lean" else "6000"
chat = HFChat(os.environ.get("EIM_CHAT_MODEL", CFG.model_name),
int(os.environ.get("EIM_CONTEXT_TOKENS", default_context)))
vision_name = os.environ.get("EIM_VISION_MODEL", "")
_ASSISTANT = Assistant(chat, HFVision(vision_name) if vision_name else None)
return _ASSISTANT
def _paths(files) -> list[str]:
out = []
for item in files or []:
path = item.get("path") if isinstance(item, dict) else getattr(item, "path", item)
if isinstance(path, str) and path:
out.append(path)
return out
def _files_markdown(session: Session | None) -> str:
if not session or not session.files:
return "_no files yet / لا توجد ملفات بعد_"
lines = []
for att in session.files.values():
extra = f" — {att.note}" if att.note else ""
size = f"{att.size / 1024:.0f} KB"
lines.append(f"- 📎 **{att.name}** · {att.kind} · {size}{extra}")
return "\n".join(lines)
def _chatbot(gr):
kwargs = dict(elem_id="chat", height=560, show_copy_button=True, render_markdown=True,
latex_delimiters=[{"left": "$$", "right": "$$", "display": True}])
for attempt in (dict(type="messages", **kwargs), kwargs, dict(elem_id="chat", height=560)):
try:
return gr.Chatbot(**attempt)
except TypeError:
continue
return gr.Chatbot()
@gpu
def respond(message, history, session, mode, iterations, candidates, holdout, temperature, max_tokens):
import gradio as gr
session = session or Session()
history = list(history or [])
text = ((message or {}).get("text") or "").strip()
paths = _paths((message or {}).get("files"))
if not text and not paths:
yield history, session, gr.MultimodalTextbox(value=None, interactive=True), _files_markdown(session), session.result_path
return
for path in paths: # show the files in the chat bubble
history.append({"role": "user", "content": {"path": path}})
if text:
history.append({"role": "user", "content": text})
history.append({"role": "assistant", "content": ""})
box = gr.MultimodalTextbox(value=None, interactive=False)
opts = Options(mode=str(mode).lower(), iterations=int(iterations), candidates=int(candidates),
holdout=float(holdout), temperature=float(temperature), max_new_tokens=int(max_tokens))
yield history, session, box, _files_markdown(session), session.result_path
for snapshot in get_assistant().handle(session, text, paths, opts):
history[-1] = {"role": "assistant", "content": snapshot}
yield history, session, box, _files_markdown(session), session.result_path
yield history, session, box, _files_markdown(session), session.result_path
def build_ui():
import gradio as gr
# Gradio 6 moved theme/css from Blocks(...) to launch(...). Keep both API generations supported.
try:
major = int(str(getattr(gr, "__version__", "5")).split(".", 1)[0])
except (TypeError, ValueError):
major = 5
if major >= 6:
blocks = gr.Blocks(title="EIM Chat")
launch_kwargs = {"theme": gr.themes.Soft(), "css": CSS}
else:
blocks = gr.Blocks(title="EIM Chat", theme=gr.themes.Soft(), css=CSS)
launch_kwargs = {}
setattr(blocks, "_eim_launch_kwargs", launch_kwargs)
with blocks as demo:
gr.Markdown("## EIM Chat\nمحادثة + ملفات + صور + كود يُتحقَّق منه ويُصلَح تلقائياً · "
"Chat + files + images + code that is verified and repaired automatically")
session = gr.State(None)
with gr.Row():
with gr.Column(scale=4):
chat = _chatbot(gr)
box = gr.MultimodalTextbox(
file_count="multiple", file_types=ACCEPTED_FILES, show_label=False, interactive=True,
placeholder="اكتب رسالتك أو أرفق ملفات وصوراً… · Type, or attach files and images… "
"(add `assert f(2) == 4` lines to verify code)")
with gr.Column(scale=1, min_width=260):
mode = gr.Radio(["Auto", "Chat", "EIM"], value="Auto", label="Mode")
with gr.Accordion("EIM", open=False):
iterations = gr.Slider(1, 8, value=4, step=1, label="Max iterations")
candidates = gr.Slider(1, 6, value=3, step=1, label="Candidates per iteration")
holdout = gr.Slider(0, 0.5, value=0.25, step=0.05, label="Hidden tests share (overfitting check)")
with gr.Accordion("Chat", open=False):
temperature = gr.Slider(0, 1.2, value=0.4, step=0.05, label="Temperature")
max_tokens = gr.Slider(128, 2048, value=900, step=64, label="Max new tokens")
files_view = gr.Markdown(_files_markdown(None))
result = gr.File(label="solution.py", interactive=False)
clear = gr.Button("New chat / محادثة جديدة")
inputs = [box, chat, session, mode, iterations, candidates, holdout, temperature, max_tokens]
outputs = [chat, session, box, files_view, result]
box.submit(respond, inputs, outputs).then(lambda: gr.MultimodalTextbox(interactive=True), None, [box])
clear.click(lambda: ([], None, gr.MultimodalTextbox(value=None, interactive=True), _files_markdown(None), None),
None, outputs)
return demo
# ============================================================================
# 6. Self-test (no model, no GPU, no Gradio): python chat_app.py --selftest
# ============================================================================
class _FakeChat:
def __init__(self, answer: str = "Fake answer."):
self.answer, self.seen = answer, []
def stream(self, messages, max_new_tokens=900, temperature=0.4):
self.seen.append(messages)
for i in range(0, len(self.answer), 7):
yield self.answer[i:i + 7]
class _FakeVision:
def describe(self, image_path, question=""):
return "A red square on a white background with the text HELLO."
def run_selftest() -> int:
import shutil
from app import _ScriptedLM
checks = 0
def check(name: str, condition: bool) -> None:
nonlocal checks
checks += 1
print(("PASS " if condition else "FAIL ") + name)
if not condition:
raise SystemExit(1)
tmp = tempfile.mkdtemp(prefix="eimchat_test_")
fast = Config(per_test_seconds=1, wall_seconds=8.0, cpu_seconds=6, memory_path="")
def write(name: str, text: str) -> str:
path = os.path.join(tmp, name)
with open(path, "w", encoding="utf-8") as handle:
handle.write(text)
return path
try:
# ---- intent detection
s = Session()
msg = "Fix this:\n```python\ndef add(a, b):\n return a - b\n```\nassert add(1, 2) == 3\nassert add(2, 2) == 4"
intent = detect_intent(msg, [], s, "auto")
check("fenced code + assert lines -> EIM from the user's code",
intent.kind == "eim" and len(intent.tests) == 2 and "return a - b" in intent.initial_code)
check("plain question -> chat", detect_intent("what is a decorator?", [], s, "auto").kind == "chat")
check("mode=chat wins over tests", detect_intent(msg, [], s, "chat").kind == "chat")
check("mode=eim without tests asks for tests", detect_intent("hello", [], s, "eim").kind == "need_tests")
sol = read_attachment(write("solution.py", "def add(a, b):\n return a - b\n"))
tst = read_attachment(write("test_solution.py", "from solution import add\n\ndef test_a():\n assert add(1, 2) == 3\n"))
intent = detect_intent("make it pass", [sol, tst], Session(), "auto")
check("attached solution.py + test_solution.py -> EIM",
intent.kind == "eim" and intent.initial_code.startswith("def add") and len(intent.tests) == 1)
s2 = Session(working_code="def add(a, b):\n return a - b")
check("tests about already-known code reuse it",
detect_intent("assert add(1, 2) == 3", [], s2, "auto").initial_code.startswith("def add"))
check("unrelated tests do NOT reuse old code",
detect_intent("assert mul(1, 2) == 2", [], s2, "auto").initial_code == "")
check("language detection", detect_lang("مرحبا") == "ar" and detect_lang("hello") == "en")
# Remote API chat must not try to access .model/.tokenizer or load Transformers weights.
class _FakeRemote:
is_remote = True
def generate(self, prompts, temperature, max_new_tokens):
self.prompts = list(prompts)
return ["remote response"]
old_get_lm = globals()["get_lm"]
fake_remote = _FakeRemote()
try:
globals()["get_lm"] = lambda: fake_remote
remote = HFChat(CFG.model_name)
remote_text = "".join(remote.stream([{"role": "system", "content": "Keep safe"},
{"role": "user", "content": "Hello"}], 64, 0.0))
check("remote chat backend works without local model weights",
remote_text == "remote response" and "SYSTEM:" in fake_remote.prompts[0]
and "Hello" in fake_remote.prompts[0])
finally:
globals()["get_lm"] = old_get_lm
# ---- chat with files (retrieval across turns)
filler = "\n".join(f"row {i}: nothing relevant here at all" for i in range(700))
needle = "The warranty period for the X200 device is 26 months."
path = write("manual.txt", filler + "\n" + needle + "\n" + filler)
chat = _FakeChat("The warranty is 26 months.")
bot = Assistant(chat, None, _ScriptedLM(["x = 1"]), EUTV(fast), SmartMemory(path=""), fast)
s = Session()
out = list(bot.handle(s, "What is the warranty period for the X200?", [path], Options()))
check("chat streams and ends with the full answer", out[-1] == "The warranty is 26 months.")
check("the relevant chunk of a big file reached the model", needle in chat.seen[0][-1]["content"])
check("history keeps the plain turn, not the file dump",
"row 5:" not in s.history[0]["content"] and "[attached: manual.txt]" in s.history[0]["content"])
list(bot.handle(s, "And what about the warranty again?", [], Options()))
check("follow-up retrieves from earlier files", needle in chat.seen[1][-1]["content"] and len(chat.seen[1]) == 4)
out = list(bot.handle(Session(), "ما هي فترة الضمان؟", [write("m2.txt", "فترة الضمان ستة وعشرون شهرا")], Options()))
check("Arabic message gets the Arabic status line", out[0] == STR["thinking"][1])
# ---- images
from PIL import Image
Image.new("RGB", (20, 10), "red").save(os.path.join(tmp, "pic.png"))
chat = _FakeChat("It is a red square.")
bot_v = Assistant(chat, _FakeVision(), _ScriptedLM(["x = 1"]), EUTV(fast), SmartMemory(path=""), fast)
out = list(bot_v.handle(Session(), "What is in the picture?", [os.path.join(tmp, "pic.png")], Options()))
check("vision description reaches the chat model", "red square on a white background" in chat.seen[0][-1]["content"])
chat = _FakeChat("I cannot see it.")
bot_nv = Assistant(chat, None, _ScriptedLM(["x = 1"]), EUTV(fast), SmartMemory(path=""), fast)
list(bot_nv.handle(Session(), "What is in the picture?", [os.path.join(tmp, "pic.png")], Options()))
check("without a vision model the prompt says the image is unknown",
"no vision model is configured" in chat.seen[0][-1]["content"])
out = list(bot_nv.handle(Session(), "", [os.path.join(tmp, "pic.png")], Options()))
check("files without text still get an answer", out and out[-1] == "I cannot see it.")
# ---- history trimming: binary search == dropping one turn at a time
class _WordTok:
def apply_chat_template(self, messages, tokenize=False, add_generation_prompt=True):
return " ".join(f"<{m['role']}> {m['content']}" for m in messages)
def __call__(self, text, add_special_tokens=False):
return type("Enc", (), {"input_ids": text.split()})()
def linear_fit(hf, messages):
msgs = list(messages)
while hf._length(msgs) > hf.max_context_tokens and len(msgs) > 2:
del msgs[1]
return msgs
same = True
for budget in (8, 40, 120, 400, 5000):
hf = HFChat("x", budget)
hf._tok = _WordTok()
convo = [{"role": "system", "content": "sys prompt"}] + [
{"role": "user" if i % 2 == 0 else "assistant", "content": "word " * (3 + i * 2)} for i in range(14)]
expected = linear_fit(hf, convo)
for _ in range(6): # the original's shrink step, applied to both
if hf._length(expected) <= budget:
break
expected[-1] = {**expected[-1], "content": expected[-1]["content"][: len(expected[-1]["content"]) // 2]}
same &= hf._fit(convo) == expected
check("history trimming gives the same result as the one-by-one version", same)
hf = HFChat("x", 60)
hf._tok = _WordTok()
question = "QUESTION why does the loop never end please answer"
long_ctx = " ".join(f"ctx{i}" for i in range(400))
convo = [{"role": "system", "content": "sys"}, {"role": "user", "content": long_ctx + "\n\n" + question}]
cut = hf._fit(convo, protect=len(question))
check("trimming cuts file excerpts and keeps the user's question whole",
cut[-1]["content"].endswith(question) and hf._length(cut) <= 60)
old_way = hf._fit(convo)
check("without protect the old behaviour is unchanged (end is cut)", not old_way[-1]["content"].endswith(question))
huge_q = " ".join(f"q{i}" for i in range(300))
cut = hf._fit([{"role": "system", "content": "sys"}, {"role": "user", "content": huge_q}], protect=len(huge_q))
check("an oversized question keeps its beginning AND its end",
cut[-1]["content"].startswith("q0") and cut[-1]["content"].endswith("q299") and hf._length(cut) <= 60)
check("a smaller explicit budget is honoured", hf._length(hf._fit(convo, protect=len(question), budget=30)) <= 30)
# ---- unsupported / broken files never crash the turn
out = list(bot.handle(Session(), "read this", [write("x.bin", "\x00\x00\x00")], Options()))
check("a binary file does not crash the turn", out and not out[-1].startswith("⚠️ Something went wrong"))
# ---- EIM flow
fixed = "def add(a, b):\n return a + b"
bot_e = Assistant(_FakeChat(), None, _ScriptedLM([fixed]), EUTV(fast), SmartMemory(path=""), fast)
s = Session()
out = list(bot_e.handle(s, msg, [], Options(iterations=3, candidates=2)))
check("EIM flow streams progress then a final report",
"| # | Status" in out[1] and out[-1].startswith("### ✅") and "return a + b" in out[-1])
check("solution.py is produced and holds the repaired code",
s.result_path and "return a + b" in open(s.result_path, encoding="utf-8").read())
check("working code and history are updated", s.working_code.strip() == fixed and "Final code" in s.history[-1]["content"])
out = list(bot_e.handle(Session(), "اكتب دالة الجمع\nassert add(1, 2) == 3\nassert add(2, 2) == 4", [], Options()))
check("EIM report speaks Arabic when asked in Arabic", "الاختبارات الظاهرة" in out[-1])
out = list(bot_e.handle(Session(), "hello", [], Options(mode="eim")))
check("EIM mode without tests explains what to add", "assert" in out[-1])
finally:
shutil.rmtree(tmp, ignore_errors=True)
print(f"\nAll {checks} checks passed.")
return 0
if __name__ == "__main__" and "--selftest" in sys.argv:
raise SystemExit(run_selftest())
if __name__ == "__main__":
demo = build_ui()
demo.queue().launch(allowed_paths=[tempfile.gettempdir()], **getattr(demo, "_eim_launch_kwargs", {}))
|