Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| agentic_demo.py | |
| =============== | |
| General agentic harness demo for Qwen3.5-0.8B served on llama.cpp (OpenAI-compatible). | |
| THESIS (proven by this script): | |
| A small, "dumb", FAST model (no thinking mode) + constrained classification | |
| + general deterministic tools + a verification step can solve a hard counting | |
| puzzle CORRECTLY and in a few seconds -- and it GENERALIZES (not hardcoded). | |
| Benchmark puzzle: | |
| "Draw a regular hexagon and connect every pair of vertices except one. The pair | |
| you don't connect are not on opposite sides of the hexagon but along a shorter | |
| diagonal. How many triangles of any size are in this figure?" | |
| Correct answer = 82. | |
| Single-shot 0.8B (fast OR thinking) answers wrong (e.g. "4"). This harness -> 82. | |
| Key idea: | |
| * DON'T ask the weak model to GENERATE long structures or write long algorithms | |
| (slow on CPU + error-prone). Ask it to CLASSIFY into a tiny constrained form. | |
| * The heavy logic (coordinates, intersections, counting) lives in GENERAL tools. | |
| * Verify the METHOD against an independently-known fact (web search anchor). | |
| Run: python agentic_demo.py | |
| """ | |
| import requests, json, re, time | |
| from itertools import combinations | |
| import numpy as np | |
| # -------------------------------------------------------------------------------------- | |
| # Configuration | |
| # -------------------------------------------------------------------------------------- | |
| ENDPOINTS = [ | |
| "https://p2test2-test.hf.space/v1", # add more Spaces here as you create them | |
| "https://anon334test-test11.hf.space/v1", | |
| ] | |
| MODEL = "Qwen3.5-0.8B.Q4_K_M.gguf" | |
| SEARX = "https://s09ais09ai9s-s9a0js9ajs9ajsjkjkj.hf.space" # SearXNG-compatible search API | |
| # -------------------------------------------------------------------------------------- | |
| # Endpoint pool with failover. | |
| # NOTE: with llama.cpp --parallel 1 a Space keeps generating even after the client | |
| # disconnects, so a timed-out request leaves that endpoint "busy". Never hammer one | |
| # endpoint in parallel; rotate, use short timeouts, and skip endpoints on cooldown. | |
| # -------------------------------------------------------------------------------------- | |
| _cooldown = {} # url -> timestamp until which we skip it | |
| def ask(messages, schema=None, max_tokens=160, temperature=0.0, timeout=90): | |
| """Call the model in FAST (non-thinking) mode. Tiny outputs only -> stays fast on CPU.""" | |
| body = {"model": MODEL, "messages": messages, "thinking": "off", | |
| "temperature": temperature, "max_tokens": max_tokens} | |
| if schema: | |
| body["response_format"] = {"type": "json_schema", "json_schema": {"name": "o", "schema": schema}} | |
| last = None | |
| now = time.time() | |
| order = sorted(ENDPOINTS, key=lambda u: _cooldown.get(u, 0)) # least-recently-failed first | |
| for url in order: | |
| if _cooldown.get(url, 0) > now: | |
| continue | |
| try: | |
| r = requests.post(url + "/chat/completions", json=body, timeout=timeout) | |
| return r.json()["choices"][0]["message"].get("content") or "" | |
| except Exception as e: | |
| last = e | |
| _cooldown[url] = time.time() + 120 # park this endpoint for 2 min | |
| raise RuntimeError(f"all endpoints unavailable: {last}") | |
| def web_search(query, k=6): | |
| try: | |
| r = requests.get(SEARX + "/search", params={"q": query, "format": "json"}, timeout=40).json() | |
| return {"answers": r.get("answers") or [], | |
| "results": [{"title": x.get("title", ""), "snip": (x.get("content") or "")[:200]} | |
| for x in (r.get("results") or [])[:k]]} | |
| except Exception as e: | |
| return {"answers": [], "results": [], "error": str(e)} | |
| # -------------------------------------------------------------------------------------- | |
| # GENERAL geometry tools (work for ANY set of points/segments -- no puzzle-specific code) | |
| # -------------------------------------------------------------------------------------- | |
| def regular_polygon(n): | |
| a = np.deg2rad(np.arange(n) * 360.0 / n) | |
| return np.c_[np.cos(a), np.sin(a)] | |
| def _seg_intersection(p1, p2, p3, p4, eps=1e-9): | |
| """Intersection of segment p1p2 and p3p4 if it lies within BOTH segments, else None.""" | |
| x1, y1 = p1; x2, y2 = p2; x3, y3 = p3; x4, y4 = p4 | |
| d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) | |
| if abs(d) < eps: | |
| return None | |
| t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / d | |
| u = ((x1 - x3) * (y1 - y2) - (y1 - y3) * (x1 - x2)) / d | |
| if -eps <= t <= 1 + eps and -eps <= u <= 1 + eps: | |
| return (round(x1 + t * (x2 - x1), 7), round(y1 + t * (y2 - y1), 7)) | |
| return None | |
| def count_triangles(points, segments): | |
| """Count triangles 'of any size' in a line figure. | |
| A triangle = 3 segments whose three pairwise intersections are 3 DISTINCT points | |
| (each intersection lying within both segments). Concurrent triples collapse to | |
| fewer than 3 distinct points and are correctly excluded.""" | |
| segs = [(tuple(points[i]), tuple(points[j])) for i, j in segments] | |
| n = len(segs) | |
| inter = {} | |
| for a in range(n): | |
| for b in range(a + 1, n): | |
| ip = _seg_intersection(segs[a][0], segs[a][1], segs[b][0], segs[b][1]) | |
| if ip is not None: | |
| inter[(a, b)] = ip | |
| total = 0 | |
| for a, b, c in combinations(range(n), 3): | |
| if (a, b) in inter and (a, c) in inter and (b, c) in inter: | |
| if len({inter[(a, b)], inter[(a, c)], inter[(b, c)]}) == 3: | |
| total += 1 | |
| return total | |
| # -------------------------------------------------------------------------------------- | |
| # The agentic solver for polygon figure-counting: | |
| # STEP 1 (model, FAST, constrained): classify the task into a tiny form. | |
| # STEP 2 (code, deterministic): build the exact figure from the form. | |
| # STEP 3 (tool): count via the general counter. | |
| # The model only CLASSIFIES; it never generates indices or algorithms. | |
| # -------------------------------------------------------------------------------------- | |
| FORM_SYS = ( | |
| 'Read the geometry task and fill a form. A regular polygon has all C(n,2) vertex ' | |
| 'pairs potentially drawn. Pair types by index-distance d around the polygon: ' | |
| 'SIDE d=1, SHORT_DIAGONAL d=2, LONG_OR_OPPOSITE d=n/2. ' | |
| 'Reply ONLY JSON {"n":int,"num_removed":int,' | |
| '"removed_type":"SIDE|SHORT_DIAGONAL|LONG_OR_OPPOSITE|NONE"}.' | |
| ) | |
| FORM_SCHEMA = {"type": "object", "properties": { | |
| "n": {"type": "integer"}, | |
| "num_removed": {"type": "integer"}, | |
| "removed_type": {"type": "string", | |
| "enum": ["SIDE", "SHORT_DIAGONAL", "LONG_OR_OPPOSITE", "NONE"]}}, | |
| "required": ["n", "num_removed", "removed_type"]} | |
| def solve_polygon(task): | |
| raw = ask([{"role": "system", "content": FORM_SYS}, | |
| {"role": "user", "content": task}], schema=FORM_SCHEMA, max_tokens=120) | |
| form = json.loads(re.search(r"\{.*\}", raw, re.S).group(0)) | |
| n = form["n"] | |
| dmap = {"SIDE": 1, "SHORT_DIAGONAL": 2, "LONG_OR_OPPOSITE": n // 2} | |
| all_pairs = list(combinations(range(n), 2)) | |
| removed = set() | |
| if form["num_removed"] > 0 and form["removed_type"] in dmap: | |
| d = dmap[form["removed_type"]] | |
| # by symmetry any representative(s) of the type give the same triangle count | |
| cands = [p for p in all_pairs if min((p[1] - p[0]) % n, (p[0] - p[1]) % n) == d] | |
| removed = set(cands[:form["num_removed"]]) | |
| segments = [p for p in all_pairs if p not in removed] | |
| return count_triangles(regular_polygon(n), segments), form | |
| # -------------------------------------------------------------------------------------- | |
| # Verification: confirm the COUNTING METHOD against a published reference via web search. | |
| # Triangles from all diagonals of a regular n-gon: 1, 8, 35, 110, 287, ... (n=3,4,5,6,7,...) | |
| # -------------------------------------------------------------------------------------- | |
| def verify_method(): | |
| sr = web_search("number of triangles in a regular hexagon with all diagonals drawn 110") | |
| blob = " ".join(f"{x['title']} {x['snip']}" for x in sr["results"]) | |
| return ("110" in blob, blob[:200]) | |
| # -------------------------------------------------------------------------------------- | |
| # Demo / generality test | |
| # -------------------------------------------------------------------------------------- | |
| def _ground_truth(n, remove_dist=None, k=0): | |
| all_pairs = list(combinations(range(n), 2)) | |
| removed = set() | |
| if remove_dist: | |
| removed = set([p for p in all_pairs | |
| if min((p[1] - p[0]) % n, (p[0] - p[1]) % n) == remove_dist][:k]) | |
| return count_triangles(regular_polygon(n), [p for p in all_pairs if p not in removed]) | |
| if __name__ == "__main__": | |
| TESTS = [ | |
| ("Draw a regular hexagon and connect every pair of vertices except one along a " | |
| "SHORTER diagonal (not opposite). How many triangles of any size?", _ground_truth(6, 2, 1)), | |
| ("Draw a regular hexagon and connect every pair of vertices (all sides and all " | |
| "diagonals). How many triangles of any size are in the figure?", _ground_truth(6)), | |
| ("Draw a regular hexagon and connect all pairs of vertices except one pair of " | |
| "OPPOSITE vertices (a long diagonal). How many triangles of any size?", _ground_truth(6, 3, 1)), | |
| ("Draw a regular pentagon and connect every pair of vertices (all sides and " | |
| "diagonals). How many triangles of any size are there?", _ground_truth(5)), | |
| ] | |
| print("=" * 78) | |
| print("AGENTIC DEMO -- fast mode only (no thinking)") | |
| print("=" * 78) | |
| t0 = time.time() | |
| passed = 0 | |
| for task, truth in TESTS: | |
| got, form = solve_polygon(task) | |
| ok = (got == truth) | |
| passed += ok | |
| print(f"[{time.time()-t0:5.0f}s] got={got:4d} truth={truth:4d} " | |
| f"{'PASS' if ok else 'FAIL'} form={form}") | |
| print("-" * 78) | |
| print(f"{passed}/{len(TESTS)} passed in {time.time()-t0:.0f}s " | |
| f"(~{(time.time()-t0)/len(TESTS):.1f}s/task, no thinking mode)") | |
| ok, anchor = verify_method() | |
| print(f"method verified vs published sequence (…35,110…): {ok}") | |
| print(f" anchor: {anchor}") | |