#!/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}")