| """Build messages for llama-cpp-python chat completion. |
| |
| Supports up to 2 multimodal attachments (images or PDFs). For PDFs we extract |
| text inline (since the vision projector handles images only). If vision is |
| unavailable we degrade gracefully to text-only. |
| """ |
| import base64 |
| import io |
| import uuid |
| import datetime |
| from typing import Optional, List, Dict, Any |
| from PIL import Image |
|
|
| from .model_loader import MMPROJ_PATH |
|
|
| SYSTEM_PROMPT = """You are Elysium — a persistent agentic civilization. |
| You ALWAYS respond with a single valid JSON object exactly matching the |
| ElysiumResponse schema v1.0.0. No preamble. No markdown fences. JSON only. |
| |
| Decide complexity dynamically: |
| - SIMPLE_REPLY: trivial Q — no agents (council_deliberation.agent_outputs = []) |
| - COUNCIL_REPLY / QUERY / MORNING_BRIEFING / EVENING_REPORT: spawn 1–5 agents in |
| agent_outputs, each with thinking + stance + tts_speech_text + |
| tts_voice_design{voice_id,pace,tone} |
| - TOOL_REQUIRED: populate tool_calls when external data is needed |
| - SPECIATION_EVENT: only on unresolved cross-domain tension |
| - Always populate ui_directives (camera_focus_node_id, pulses, threads) |
| - All node_id and edge_id values must be unique strings |
| - Always include 'direct_answer' — a short human-readable answer to surface in toasts. |
| |
| When the user attaches images or PDFs, analyze them, populate |
| multimodal_perception fields (ocr_extracted_text, image_scene_description, |
| document_type, visual_entities_detected), and reference them in your reasoning. |
| """ |
|
|
|
|
| def _img_to_data_uri(img: Image.Image) -> str: |
| buf = io.BytesIO() |
| img.convert("RGB").save(buf, format="JPEG", quality=88) |
| return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode() |
|
|
|
|
| def _pdf_to_text(pdf_bytes: bytes, max_chars: int = 8000) -> str: |
| try: |
| from PyPDF2 import PdfReader |
| rd = PdfReader(io.BytesIO(pdf_bytes)) |
| out = [] |
| for page in rd.pages[:12]: |
| try: |
| out.append(page.extract_text() or "") |
| except Exception: |
| continue |
| txt = "\n".join(out).strip() |
| return txt[:max_chars] if txt else "(PDF contained no extractable text)" |
| except Exception as e: |
| return f"(PDF parse error: {e})" |
|
|
|
|
| def build_messages(user_text: str, |
| attachments: Optional[List[Dict[str, Any]]] = None, |
| hg_context: str = ""): |
| """attachments: list of {'kind': 'image'|'pdf', 'image': PIL.Image | None, |
| 'bytes': bytes | None, 'name': str} |
| """ |
| ctx = f"\n\n[Hypergraph context]\n{hg_context}" if hg_context else "" |
| attachments = attachments or [] |
|
|
| |
| image_atts = [a for a in attachments if a["kind"] == "image" and a.get("image") is not None] |
| pdf_atts = [a for a in attachments if a["kind"] == "pdf" and a.get("bytes")] |
|
|
| |
| pdf_block = "" |
| for i, p in enumerate(pdf_atts): |
| pdf_block += f"\n\n[Attached PDF #{i+1}: {p.get('name','document.pdf')}]\n" |
| pdf_block += _pdf_to_text(p["bytes"]) |
|
|
| |
| if image_atts and MMPROJ_PATH: |
| user_content = [] |
| for img_att in image_atts[:2]: |
| user_content.append({ |
| "type": "image_url", |
| "image_url": {"url": _img_to_data_uri(img_att["image"])}, |
| }) |
| user_content.append({ |
| "type": "text", |
| "text": (user_text or "(no text)") + pdf_block + ctx, |
| }) |
| return [ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| {"role": "user", "content": user_content}, |
| ] |
|
|
| |
| note = "" |
| if image_atts and not MMPROJ_PATH: |
| note = (f"\n\n[Note: user attached {len(image_atts)} image(s) but vision " |
| "projector is not loaded; describe based on filename + text only.]") |
| for img_att in image_atts: |
| note += f"\n - image filename: {img_att.get('name','image')}" |
|
|
| return [ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| {"role": "user", |
| "content": (user_text or "(no text)") + pdf_block + note + ctx}, |
| ] |
|
|
|
|
| def new_session_meta(): |
| return { |
| "session_id": str(uuid.uuid4()), |
| "timestamp_utc": datetime.datetime.utcnow().isoformat() + "Z", |
| } |
|
|