File size: 8,853 Bytes
33c0287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""The tool loop the transformers model runs for `chat()`, free of torch so it can be tested.

The service runs the same loop over vLLM (app.py): the brain sees the caller's files by
reference, calls tools, reads what they return -- and, once a tool has read an image, the
image itself -- then answers. Both brief the brain with the same prompts.
"""

from __future__ import annotations

import json
import os
import re
from collections.abc import Callable, Sequence
from typing import Any

try:  # the interfaze_lite package, and the HF repo as trust_remote_code imports it
    from .prompts import NUDGE, SYSTEM_PROMPT
except ImportError:  # the files run flat, from a checkout
    from prompts import NUDGE, SYSTEM_PROMPT  # type: ignore

_FILE = {"type": "string", "description": "The file's reference id from All File References, e.g. ref-0."}

TOOLS: list[dict[str, Any]] = [
    {
        "type": "function",
        "function": {
            "name": "ocr",
            "description": "Read an image, PDF or Word document the user supplied: its full text, "
            "with every line's position.",
            "parameters": {
                "type": "object",
                "properties": {
                    "file_ref_id": _FILE,
                    "page_range": {
                        "type": "array",
                        "items": {"type": "integer"},
                        "description": "First and last page to read, 1-based, for a long PDF.",
                    },
                },
                "required": ["file_ref_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "stt",
            "description": "Transcribe an audio file, optionally labelling who said what.",
            "parameters": {
                "type": "object",
                "properties": {
                    "file_ref_id": _FILE,
                    "by_speaker": {"type": "boolean", "description": "Split the transcript by speaker."},
                },
                "required": ["file_ref_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "object_detection",
            "description": "Find objects in a photo: a box and label for each.",
            "parameters": {
                "type": "object",
                "properties": {
                    "file_ref_id": _FILE,
                    "prompts": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "What to find, as simple noun phrases, e.g. ['person', 'car'].",
                    },
                },
                "required": ["file_ref_id", "prompts"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "gui_detection",
            "description": "Find interactive elements in a UI screenshot -- buttons, inputs, links -- "
            "each named by its visible label.",
            "parameters": {
                "type": "object",
                "properties": {
                    "file_ref_id": _FILE,
                    "prompts": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Elements to find; leave out for every interactive element.",
                    },
                },
                "required": ["file_ref_id"],
            },
        },
    },
]

# The model reads this much of a tool result; the caller gets all of it.
MAX_CONTEXT_CHARS = 120_000

_TOOL_CALL = re.compile(r"<tool_call>(.*?)</tool_call>", re.S)
_FUNCTION = re.compile(r"<function=([^>\s]+)>(.*?)</function>", re.S)
_PARAMETER = re.compile(r"<parameter=([^>\s]+)>(.*?)</parameter>", re.S)


def tool_calls(text: str) -> list[dict[str, Any]]:
    """The tool calls in a brain turn, in either form the chat template produces.

    `<function=ocr><parameter=file_ref_id>ref-0</parameter></function>` is what this
    brain's template emits; `{"name": ..., "arguments": {...}}` is the older JSON form.
    Both sit inside <tool_call> tags. A parameter that reads as JSON -- a list, a number
    -- is taken as that value.
    """
    calls = []
    for body in _TOOL_CALL.findall(text or ""):
        function = _FUNCTION.search(body)
        if function:
            arguments = {}
            for name, value in _PARAMETER.findall(function.group(2)):
                value = value.strip()
                try:
                    arguments[name] = json.loads(value)
                except json.JSONDecodeError:
                    arguments[name] = value
            calls.append({"name": function.group(1), "arguments": arguments})
            continue
        try:
            obj = json.loads(body.strip())
            arguments = obj.get("arguments") or {}
            if isinstance(arguments, str):
                arguments = json.loads(arguments)
            calls.append({"name": obj["name"], "arguments": arguments})
        except (json.JSONDecodeError, KeyError, TypeError, AttributeError):
            continue
    return calls


def visible(text: str) -> str:
    """A turn's answer, without any tool-call markup or thinking."""
    text = _TOOL_CALL.sub("", text or "")
    return re.sub(r"<think>.*?</think>", "", text, flags=re.S).strip()


def run(
    messages: list[dict],
    files: Sequence[str],
    *,
    generate: Callable[..., str],
    tools: dict[str, Callable[[str, dict], tuple[dict, dict]]],
    show_images: Callable[[list[dict], dict[str, str]], list[dict]],
    max_tool_steps: int = 6,
) -> dict[str, Any]:
    """Answer `messages`, calling `tools` on `files` as the brain asks.

    `generate(convo, tools=..., keep_markup=...)` is one brain turn. Each tool takes the
    file and the call's arguments and returns what the caller gets and what the model
    reads. Returns `{"content", "precontext": [{name, result}]}`.
    """
    refs = {f"ref-{i}": str(f) for i, f in enumerate(files)}
    system = SYSTEM_PROMPT
    if refs:
        system += "\n\nAll File References:\n" + "\n".join(
            f"- {ref}: {os.path.basename(path.split('?')[0]) or path}" for ref, path in refs.items()
        )
    convo = [{"role": "system", "content": system}, *messages]
    precontext: list[dict[str, Any]] = []
    used = nudged = shown = False

    for _ in range(max_tool_steps):
        reply = generate(convo, tools=TOOLS if refs else None, keep_markup=True)
        calls = tool_calls(reply)
        if not calls:
            # Attached but unread: an answer by eye invents the text. One reminder, as in the service.
            if refs and not used and not nudged:
                nudged = True
                convo = [*convo, {"role": "user", "content": NUDGE}]
                continue
            return {"content": visible(reply), "precontext": precontext}

        convo.append(
            {
                "role": "assistant",
                "content": "",
                "tool_calls": [
                    {"type": "function", "function": {"name": c["name"], "arguments": c["arguments"]}}
                    for c in calls
                ],
            }
        )
        for call in calls:
            full, seen = _dispatch(call, refs, tools)
            used = True  # A failed call too: it goes back to be corrected, not nudged, as in the service.
            if "error" not in seen:
                precontext.append({"name": call["name"], "result": full})
            convo.append({"role": "tool", "content": json.dumps(seen, ensure_ascii=False)})
        if precontext and not shown:
            convo, shown = show_images(convo, refs), True

    return {"content": visible(generate(convo, tools=None, keep_markup=False)), "precontext": precontext}


def _dispatch(call: dict, refs: dict[str, str], tools: dict) -> tuple[dict, dict]:
    tool = tools.get(call["name"])
    if tool is None:
        error = {"error": f"unknown tool {call['name']!r}; available: {sorted(tools)}"}
        return error, error
    ref = str(call["arguments"].get("file_ref_id") or "")
    # Only the caller's own files: a made-up URL is refused, never fetched.
    path = refs.get(ref) or next((p for p in refs.values() if p == ref), None)
    if path is None:
        error = {"error": f"unknown file reference {ref!r}; use one of {sorted(refs)}"}
        return error, error
    try:
        full, seen = tool(path, call["arguments"])
    except Exception as exc:
        error = {"error": f"{call['name']} failed: {type(exc).__name__}: {exc}"}
        return error, error
    text = seen.get("extracted_text")
    if isinstance(text, str) and len(text) > MAX_CONTEXT_CHARS:
        seen = {**seen, "extracted_text": text[:MAX_CONTEXT_CHARS], "truncated": True}
    return full, seen