1.3: Fix files parameter in agent.py — attachments now visible to LLM

Previously respond() accepted files but silently dropped them.
Now when files are attached:
- Images are base64-encoded as image_url content parts
- Text files are read and inlined as text content parts
- The last user message is converted to multipart format

Follows the same encoding pattern used in tools/image.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cora-start
PeninsulaInd 2026-02-17 09:58:44 -06:00
parent 9002fc08d2
commit ed751d843b
1 changed files with 59 additions and 0 deletions

View File

@ -2,10 +2,12 @@
from __future__ import annotations from __future__ import annotations
import base64
import json import json
import logging import logging
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path
from .config import Config from .config import Config
from .db import Database from .db import Database
@ -16,6 +18,49 @@ log = logging.getLogger(__name__)
MAX_TOOL_ITERATIONS = 5 MAX_TOOL_ITERATIONS = 5
_IMAGE_MIME = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
}
def _build_file_content_parts(files: list[str]) -> list[dict]:
"""Encode file attachments as content parts for the LLM message.
Images base64 image_url parts; text files inline text parts.
"""
parts: list[dict] = []
for file_path in files:
p = Path(file_path).resolve()
if not p.exists():
parts.append({"type": "text", "text": f"[File not found: {file_path}]"})
continue
suffix = p.suffix.lower()
if suffix in _IMAGE_MIME:
try:
data = base64.b64encode(p.read_bytes()).decode("utf-8")
mime = _IMAGE_MIME[suffix]
parts.append({
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{data}"},
})
except Exception as e:
parts.append({"type": "text", "text": f"[Error reading image {p.name}: {e}]"})
else:
try:
text = p.read_text(encoding="utf-8", errors="replace")
if len(text) > 10000:
text = text[:10000] + "\n... (truncated)"
parts.append({"type": "text", "text": f"[File: {p.name}]\n{text}"})
except Exception as e:
parts.append({"type": "text", "text": f"[Error reading {p.name}: {e}]"})
return parts
class Agent: class Agent:
def __init__(self, config: Config, db: Database, llm: LLMAdapter): def __init__(self, config: Config, db: Database, llm: LLMAdapter):
@ -73,6 +118,20 @@ class Agent:
system_prompt, history, self.config.memory.max_context_messages system_prompt, history, self.config.memory.max_context_messages
) )
# If files are attached, replace the last user message with multipart content
if files:
file_parts = _build_file_content_parts(files)
if file_parts:
# Find the last user message and convert to multipart
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
text_content = messages[i]["content"]
messages[i]["content"] = [
{"type": "text", "text": text_content},
*file_parts,
]
break
# Agent loop: LLM call → tool execution → repeat # Agent loop: LLM call → tool execution → repeat
seen_tool_calls: set[str] = set() # track (name, args_json) to prevent duplicates seen_tool_calls: set[str] = set() # track (name, args_json) to prevent duplicates