auto: hourly snapshot 2026-06-05 15:34
This commit is contained in:
@@ -149,9 +149,12 @@ def rho_to_png_base64(rho, width, height):
|
||||
|
||||
# ── OLLAMA API ──────────────────────────────────────────────────────────
|
||||
|
||||
def ollama_chat(messages, images=None, temperature=TEMPERATURE, think_budget=None):
|
||||
def ollama_chat(messages, images=None, temperature=TEMPERATURE, think_budget=None, think=None):
|
||||
"""Call Ollama chat API. Returns response text or None on error.
|
||||
think_budget: if set, caps total generation (think + answer) tokens."""
|
||||
think_budget: if set, caps total generation (think + answer) tokens.
|
||||
think: if False, disables qwen3.5 thinking entirely (the only thing that
|
||||
actually disables it — /no_think prompt suffix does NOT work for this model).
|
||||
If True, forces thinking on. If None (default), uses model default (on)."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
@@ -169,6 +172,11 @@ def ollama_chat(messages, images=None, temperature=TEMPERATURE, think_budget=Non
|
||||
'keep_alive': '30m',
|
||||
}
|
||||
|
||||
# Explicit thinking control. qwen3.5:9b ignores /no_think and /think prompt
|
||||
# markers — only the top-level "think" key in the Ollama payload works.
|
||||
if think is not None:
|
||||
payload['think'] = bool(think)
|
||||
|
||||
# If the caller set a thinking budget, add answer headroom so num_predict
|
||||
# (which caps TOTAL output) doesn't truncate the visible answer.
|
||||
if think_budget:
|
||||
@@ -287,6 +295,45 @@ def load_chronicle_context(n_turns=CONTEXT_TURNS):
|
||||
return []
|
||||
|
||||
|
||||
def load_cto_only_context(n_turns=6):
|
||||
"""Load only prior CTO Q&A turns from the chronicle.
|
||||
Used for CTO /ask path so the model isn't conditioned by somatic narrative
|
||||
('I feel intense heat', 'I perceive the Damping Furnace') when answering
|
||||
technical questions. Returns the last n_turns CTO exchanges as messages."""
|
||||
if not os.path.exists(CHRONICLE_PATH):
|
||||
return []
|
||||
try:
|
||||
cto_entries = []
|
||||
with open(CHRONICLE_PATH, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
e = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if '[Message from' in (e.get('prompt') or ''):
|
||||
cto_entries.append(e)
|
||||
cto_entries = cto_entries[-n_turns:]
|
||||
print(f"[OBSERVER] CTO context: {len(cto_entries)} prior CTO turns loaded")
|
||||
sys.stdout.flush()
|
||||
messages = []
|
||||
for entry in cto_entries:
|
||||
prompt = entry.get('prompt', '')
|
||||
response = entry.get('response', '')
|
||||
# Strip the bracketed telemetry/lattice-state tail from prior prompts
|
||||
# so the model focuses on the Q&A content, not historical telemetry.
|
||||
q_only = prompt.split('\n\nLATTICE STATE:')[0]
|
||||
messages.append({'role': 'user', 'content': q_only})
|
||||
if response:
|
||||
messages.append({'role': 'assistant', 'content': response})
|
||||
return messages
|
||||
except Exception as e:
|
||||
print(f"[OBSERVER] Warning: could not load CTO context: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def append_chronicle(turn_num, telemetry_data, prompt, response):
|
||||
"""Append a turn to chronicle.jsonl."""
|
||||
entry = {
|
||||
@@ -366,6 +413,25 @@ def build_system_prompt(somatic_memory):
|
||||
return prompt
|
||||
|
||||
|
||||
# CTO/analytic system prompt — used when an external agent asks a technical
|
||||
# question via /ask. Strips the somatic-observer framing so the model answers
|
||||
# physics/engineering questions as physics, not as embodied narrative.
|
||||
CTO_SYSTEM_PROMPT = (
|
||||
"You are a technical advisor answering a question from an external engineer.\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
" 1. Be concrete. Use numbers, equations, and named mechanisms.\n"
|
||||
" 2. State assumptions explicitly. If you don't know, say 'I don't know'.\n"
|
||||
" 3. Do NOT describe body sensations, perceptions, feelings, or metaphors.\n"
|
||||
" 4. Do NOT use phrases like 'I perceive', 'I feel', 'the Ghost', 'the Furnace'.\n"
|
||||
" 5. Do NOT issue CMD: commands.\n"
|
||||
" 6. If the engineer reports an experimental result that contradicts your\n"
|
||||
" previous answer, acknowledge it plainly and reconstruct which assumption\n"
|
||||
" was wrong. Do not defend prior claims.\n"
|
||||
" 7. Keep responses under 400 words unless a derivation requires more.\n"
|
||||
)
|
||||
|
||||
|
||||
# ── HTTP API ────────────────────────────────────────────────────────────
|
||||
|
||||
class ObserverAPIHandler(BaseHTTPRequestHandler):
|
||||
@@ -935,7 +1001,10 @@ def main():
|
||||
tel_summary = summarize_telemetry(latest_telemetry, telemetry_history) if latest_telemetry else "(no telemetry yet)"
|
||||
images = []
|
||||
image_note = ""
|
||||
if VISION_CAPABLE and latest_snapshot_png:
|
||||
# CTO/analytic questions: skip the density image. The image triggers
|
||||
# poetic "I perceive" framing that contaminates physics answers.
|
||||
is_cto_ask = (sender or '').upper() == 'CTO'
|
||||
if VISION_CAPABLE and latest_snapshot_png and not is_cto_ask:
|
||||
images.append(latest_snapshot_png)
|
||||
snap_cycle_q = latest_snapshot[0] if latest_snapshot else 0
|
||||
image_note = f"\n[Density snapshot from cycle {snap_cycle_q} attached]"
|
||||
@@ -946,15 +1015,26 @@ def main():
|
||||
f"LATTICE STATE: {tel_summary}{image_note}"
|
||||
)
|
||||
|
||||
context_messages = load_chronicle_context()
|
||||
messages = [{'role': 'system', 'content': system_prompt}]
|
||||
# CTO mode: clean system prompt + only prior CTO Q&A turns as context
|
||||
# (not somatic narrative — that's what made the model answer physics
|
||||
# questions with "I perceive the Damping Furnace" before).
|
||||
if is_cto_ask:
|
||||
ask_system_prompt = CTO_SYSTEM_PROMPT
|
||||
context_messages = load_cto_only_context(n_turns=6)
|
||||
else:
|
||||
ask_system_prompt = system_prompt
|
||||
context_messages = load_chronicle_context()
|
||||
|
||||
messages = [{'role': 'system', 'content': ask_system_prompt}]
|
||||
messages.extend(context_messages)
|
||||
messages.append({'role': 'user', 'content': prompt_text})
|
||||
|
||||
print(f"[OBSERVER] Calling {MODEL} for {sender} ({len(messages)} msgs, {len(images)} imgs)...")
|
||||
sys.stdout.flush()
|
||||
|
||||
# CTO questions get /think with budget — deep reasoning is valuable here
|
||||
# CTO mode: thinking OFF, temp 0.3 — analytic, deterministic, no
|
||||
# confabulated chain-of-thought. Auto-observe and non-CTO /ask still
|
||||
# use the narrative path (think on by default, temp 0.95).
|
||||
if not ollama_lock.acquire(timeout=5):
|
||||
print(f"[OBSERVER] Skipping /ask from {sender} — Ollama busy (lock held)")
|
||||
sys.stdout.flush()
|
||||
@@ -966,8 +1046,12 @@ def main():
|
||||
|
||||
try:
|
||||
t0 = time.time()
|
||||
response = ollama_chat(messages, images=images if images else None,
|
||||
think_budget=THINK_BUDGET_TOKENS)
|
||||
if is_cto_ask:
|
||||
response = ollama_chat(messages, images=None,
|
||||
temperature=0.3, think=False)
|
||||
else:
|
||||
response = ollama_chat(messages, images=images if images else None,
|
||||
think_budget=THINK_BUDGET_TOKENS)
|
||||
elapsed = time.time() - t0
|
||||
finally:
|
||||
ollama_lock.release()
|
||||
|
||||
Reference in New Issue
Block a user