Spaces:
Running
Running
File size: 7,004 Bytes
df42e17 | 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 | """Shared original Agents SDK conversation, tool events and approval/resume engine.
No intent router or answer templates: the agent chooses tools and writes answers.
"""
from __future__ import annotations
import asyncio
import json
import logging
import queue
import threading
import uuid
from agents import Runner, RunState, SQLiteSession, set_tracing_disabled
from agents.items import ItemHelpers
from openai.types.responses import (ResponseCompletedEvent, ResponseCreatedEvent, ResponseReasoningSummaryTextDeltaEvent, ResponseReasoningTextDeltaEvent, ResponseTextDeltaEvent)
set_tracing_disabled(True)
log = logging.getLogger("ontic.agent")
class SessionRuntime:
def __init__(self, stream: bool = True):
self.id = uuid.uuid4().hex
self.events: queue.Queue = queue.Queue()
self.thread: threading.Thread | None = None
self.running = False
self.stream = stream
self.memory = SQLiteSession(self.id) # ๋ํ ์ด๋ ฅ (ํ๋ก์ธ์ค ๋ฉ๋ชจ๋ฆฌ; ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ฃผ๋ฉด ์์)
self.state: RunState | None = None # ์น์ธ ๋๊ธฐ ์ค์ธ ๋ฐ
self.usage = None
self.mcp = self.make_mcp() # ์ธ์
๋น ํ๋ (๋ฐ๋ง๋ค connect/cleanup; ์ด๋ฒคํธ ๋ฃจํ๊ฐ ๋ฐ๋ง๋ค ์๋ก ์๊ธฐ๋ฏ๋ก ์ ์์ ๊ทธ๋)
steps = {}
own_tools = set()
def make_mcp(self):
return None
def _agent(self, mcp_servers=()):
raise NotImplementedError
def command(self, name, args):
return ""
def _emit(self, **e):
if e["type"] not in ("token", "reasoning"):
log.info("session %s %s %s", self.id, e["type"], e.get("tool") or e.get("state") or "")
self.events.put(e)
@staticmethod
def _args(raw: str | None) -> dict:
try:
return {k: v for k, v in json.loads(raw or "{}").items() if k != "summary"}
except ValueError:
return {}
@property
def status(self) -> str:
return "running" if self.busy() else "waiting_for_confirmation" if self.state is not None else "idle"
def pending(self) -> list[dict]:
return [{"tool": i.tool_name, "args": self._args(i.arguments)} for i in (self.state.get_interruptions() if self.state else [])]
async def _arun(self, inp):
return await self._stream(inp, ())
async def _stream(self, inp, servers):
result = Runner.run_streamed(self._agent(servers), inp, context=self, session=self.memory, max_turns=30)
calls: dict[str, str] = {}
n_llm, summary_seen = 0, False
async for ev in result.stream_events():
if ev.type == "raw_response_event": # LLM ํธ์ถ๋ง๋ค: ์์ โ ์๊ฐ(reasoning ๋ธํ) โ ๋. ๊ธฐ๋ค๋ฆผ์ด ๋ณด์ด๊ฒ ํ๋ค
d = ev.data
if isinstance(d, ResponseCreatedEvent):
n_llm += 1; summary_seen = False; self._emit(type="llm", state="start", n=n_llm)
elif isinstance(d, ResponseCompletedEvent):
self._emit(type="llm", state="end", n=n_llm, tokens=getattr(getattr(d.response, "usage", None), "output_tokens", None))
elif isinstance(d, ResponseReasoningSummaryTextDeltaEvent) and d.delta: # LiteLLM ์ ๊ฐ์ ์๊ฐ์ reasoning_content ์ reasoning ๋ ํ๋๋ก ๋ณด๋ด๊ณ
summary_seen = True; self._emit(type="reasoning", text=d.delta) # SDK ๋ ๊ฐ๊ฐ summary/text ๋ธํ๋ก ๋ฐ๊พผ๋ค โ ํ์ชฝ๋ง ์ด๋ค (๋ ๋ค ์ฐ๋ฉด ๊ธ์๊ฐ ๊ฒน์นจ)
elif isinstance(d, ResponseReasoningTextDeltaEvent) and d.delta and not summary_seen:
self._emit(type="reasoning", text=d.delta)
elif self.stream and isinstance(d, ResponseTextDeltaEvent) and d.delta:
self._emit(type="token", text=d.delta)
elif ev.type == "run_item_stream_event":
it = ev.item
if it.type == "tool_call_item":
raw = json.loads(getattr(it.raw_item, "arguments", None) or "{}")
self._emit(type="progress", step=self.steps.get(it.raw_item.name, 0)) # D14: SSE 7์ข
+ progress ยท llm ยท reasoning
args = {k: v for k, v in raw.items() if k != "summary"}
self._emit(type="action", tool=it.raw_item.name, args=args, summary=raw.get("summary", ""), cmd=self.command(it.raw_item.name, args)) # โค ๋จ๊ณ ์นด๋์ ๋ช
๋ น์ค
calls[getattr(it.raw_item, "call_id", "")] = it.raw_item.name
elif it.type == "tool_call_output_item": # MCP ๋๊ตฌ๋ _run ์ ์ ๊ฑฐ์น๋ฏ๋ก ์ฌ๊ธฐ์ ๊ด์ธก ์ด๋ฒคํธ
ri = it.raw_item; cid = ri.get("call_id") if isinstance(ri, dict) else getattr(ri, "call_id", "")
name = calls.get(cid or "")
if name and name not in self.own_tools:
self._emit(type="observation", tool=name, data={"result": str(it.output)[:6000]})
elif it.type == "message_output_item":
text = ItemHelpers.text_message_output(it)
if text.strip():
self._emit(type="message", text=text)
self.usage = result.context_wrapper.usage
if result.interruptions:
self.state = result.to_state()
return dict(type="approval", actions=self.pending())
self.state = None
return dict(type="status", state="finished")
def _run(self, inp):
try:
final = asyncio.run(self._arun(inp))
except Exception as e:
final = dict(type="error", text=f"{type(e).__name__}: {str(e)[:300]}")
self.running = False # ๋ ์ด๋ฒคํธ๋ณด๋ค ๋จผ์ ๋ด๋ ค์ผ ๋ฐ์ ์ชฝ์ด ๋ฐ๋ก confirm/send ํ ์ ์๋ค
self._emit(**final)
def _start(self, inp):
self.running = True
self._emit(type="status", state="running")
self.thread = threading.Thread(target=self._run, args=(inp,), daemon=True)
self.thread.start()
def busy(self) -> bool:
return self.running
def send(self, text: str):
if self.busy():
raise RuntimeError("agent is still running")
if self.state is not None: # ์น์ธ ์นด๋๋ฅผ ๋๊ณ ๋ง์ ๊ฑธ๋ฉด = ์ด์ ์๋ ๊ฑฐ๋ถ (ํ๋กฌํํธ 3๋ฒ)
return self.confirm(False, text)
self._start(text)
def confirm(self, approve: bool, reason: str = ""):
if self.busy():
raise RuntimeError("agent is still running")
if self.state is None:
raise RuntimeError("nothing to confirm")
for it in self.state.get_interruptions():
if approve:
self.state.approve(it)
else:
self.state.reject(it, rejection_message=reason or "User rejected the action")
self._start(self.state)
|