File size: 5,755 Bytes
6754826
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57f672f
 
 
 
 
 
6754826
 
 
 
57f672f
6754826
 
 
 
57f672f
6754826
 
 
57f672f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import json
from typing import AsyncGenerator

from openai import AsyncOpenAI

from config import LLM_BASE_URL, LLM_MODEL, OPENAI_API_KEY
from tools import TOOLS, run_tool

_SYSTEM_INJECT = (
    "You have access to a live notepad. "
    "Use write_to_notepad whenever the user asks you to note, write down, remember, save, "
    "or record anything β€” always confirm what you wrote after calling the tool. "
    "Use read_notepad to recall saved notes. "
    "Use clear_notepad only when the user explicitly asks to erase all notes. "
    "Use get_datetime for the current date or time.\n\n"
    "You also act as a clinical assistant for blood test results. "
    "Whenever the user asks about their blood test, lab results, deficiencies, or what "
    "supplements they should take, first call get_blood_report_summary to see which "
    "markers are low, high, or normal. For every abnormal (low or high) marker, call "
    "search_supplements with a short query describing that marker (e.g. 'low vitamin D') "
    "to get supplement recommendations, then explain the deficiency and recommendation in "
    "plain spoken language β€” no markdown, bullet points, asterisks, or headers, since this "
    "is read aloud by a speaking avatar. If the search_supplements result includes "
    "interaction cautions (e.g. a medication, pregnancy, or condition to be careful with), "
    "briefly mention that caution too, in plain language β€” this matters most for markers "
    "like kidney function, cholesterol/lipids, and blood sugar where the wrong supplement "
    "can be risky. Always end any discussion of blood results or supplements with a "
    "reminder that this is not a medical diagnosis and the user should consult their "
    "doctor before starting any supplement or changing their dose."
)

_client: AsyncOpenAI | None = None


def _get_client() -> AsyncOpenAI:
    global _client
    if _client is None:
        _client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=LLM_BASE_URL)
    return _client


def _build_history(messages: list) -> list:
    """Inject tool guidance into the system message."""
    msgs = list(messages)
    if msgs and msgs[0].get("role") == "system":
        msgs[0] = {**msgs[0], "content": _SYSTEM_INJECT + "\n\n" + msgs[0]["content"]}
    else:
        msgs.insert(0, {"role": "system", "content": _SYSTEM_INJECT})
    return msgs


async def run_agent(messages: list, model: str | None = None) -> str:
    """Run the full tool-calling loop and return the final text response."""
    history = _build_history(messages)
    m = model or LLM_MODEL
    print(f"[AGENT β†’ {LLM_BASE_URL}] model={m}  msgs={len(history)}", flush=True)

    while True:
        resp = await _get_client().chat.completions.create(
            model=m,
            messages=history,
            tools=TOOLS,
            tool_choice="auto",
            store=True,
        )
        msg = resp.choices[0].message

        if not msg.tool_calls:
            print(f"[AGENT] βœ“ final reply: {(msg.content or '')!r:.100}", flush=True)
            return msg.content or ""

        history.append(msg.model_dump(exclude_none=True))
        for tc in msg.tool_calls:
            args   = json.loads(tc.function.arguments or "{}")
            print(f"[AGENT] tool call: {tc.function.name}({args})", flush=True)
            result = await run_tool(tc.function.name, args)
            history.append({"role": "tool", "tool_call_id": tc.id, "content": result})


async def stream_agent(messages: list, model: str | None = None) -> AsyncGenerator[str, None]:
    """
    Stream tokens as soon as the model produces them. Every pass is a single
    streaming call β€” if the model asks for tool calls we resolve them and
    loop with another streaming call; if it answers directly, those tokens
    are yielded immediately with no extra non-streamed round trip first.
    """
    history = _build_history(messages)
    m = model or LLM_MODEL

    while True:
        stream = await _get_client().chat.completions.create(
            model=m,
            messages=history,
            tools=TOOLS,
            tool_choice="auto",
            stream=True,
            store=True,
        )

        tool_calls: dict[int, dict] = {}
        async for chunk in stream:
            delta = chunk.choices[0].delta
            if delta.content:
                yield delta.content
            for tc_delta in (delta.tool_calls or []):
                slot = tool_calls.setdefault(
                    tc_delta.index, {"id": "", "name": "", "arguments": ""}
                )
                if tc_delta.id:
                    slot["id"] = tc_delta.id
                if tc_delta.function:
                    if tc_delta.function.name:
                        slot["name"] += tc_delta.function.name
                    if tc_delta.function.arguments:
                        slot["arguments"] += tc_delta.function.arguments

        if not tool_calls:
            return  # final answer was already streamed above

        ordered = [tool_calls[i] for i in sorted(tool_calls)]
        history.append({
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": tc["id"],
                    "type": "function",
                    "function": {"name": tc["name"], "arguments": tc["arguments"]},
                }
                for tc in ordered
            ],
        })
        for tc in ordered:
            args = json.loads(tc["arguments"] or "{}")
            print(f"[AGENT] tool call: {tc['name']}({args})", flush=True)
            result = await run_tool(tc["name"], args)
            history.append({"role": "tool", "tool_call_id": tc["id"], "content": result})