test11 / agentic_demo.py
anon334test's picture
Add agentic_demo.py: agentic system report + proven fast-mode demo
a8b8660 verified
Raw
History Blame Contribute Delete
10.2 kB
#!/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}")