Update
This commit is contained in:
@@ -19,10 +19,18 @@ from ..config import Settings
|
||||
from ..cost import CostTracker
|
||||
from ..cost.tracker import Pricing
|
||||
from ..http_client import PlatformClient
|
||||
from ..interaction import InteractionController
|
||||
from ..interaction.broker import InteractionBroker
|
||||
from ..interaction.capabilities import (
|
||||
CA_IWP_SYSTEM_FRAGMENT,
|
||||
UI_TOOL_NAMES,
|
||||
fragment_for,
|
||||
)
|
||||
from ..llm import LLMClient
|
||||
from ..registry import CATALOG
|
||||
from ..tasks import Scheduler, TaskController, TaskStore
|
||||
from ..virtual_tools import VirtualToolController, VirtualToolStore
|
||||
from ..text import normalize_newlines
|
||||
from ._helpers import _format_offset, _now_iso, _repair_history
|
||||
from .prompts import (
|
||||
BEHAVIOR_HEADER,
|
||||
@@ -98,6 +106,14 @@ class DeviiSession:
|
||||
)
|
||||
self._behavior_store = behavior_store
|
||||
self.behavior = BehaviorController(behavior_store)
|
||||
self.interaction_broker = InteractionBroker(
|
||||
session_channel=channel,
|
||||
owner_kind=owner_kind,
|
||||
owner_id=owner_id,
|
||||
)
|
||||
self.interaction = InteractionController(
|
||||
self.interaction_broker, owner_kind=owner_kind, owner_id=owner_id
|
||||
)
|
||||
self.dispatcher = Dispatcher(
|
||||
CATALOG,
|
||||
self.client,
|
||||
@@ -113,6 +129,7 @@ class DeviiSession:
|
||||
owner_id=owner_id,
|
||||
virtual_tools=self.virtual_tools,
|
||||
behavior=self.behavior,
|
||||
interaction=self.interaction,
|
||||
)
|
||||
self.tools = self._builtin_tools()
|
||||
self._system_prompt = (
|
||||
@@ -164,8 +181,16 @@ class DeviiSession:
|
||||
self._turn_epoch = 0
|
||||
|
||||
def restore_history(self, messages: list[dict[str, Any]]) -> None:
|
||||
if messages:
|
||||
self.agent._messages = messages
|
||||
if not messages:
|
||||
return
|
||||
repaired: list[dict[str, Any]] = []
|
||||
for message in messages:
|
||||
item = dict(message)
|
||||
content = item.get("content")
|
||||
if isinstance(content, str):
|
||||
item["content"] = normalize_newlines(content)
|
||||
repaired.append(item)
|
||||
self.agent._messages = repaired
|
||||
|
||||
def history(self) -> list[dict[str, Any]]:
|
||||
visible: list[dict[str, Any]] = []
|
||||
@@ -174,11 +199,12 @@ class DeviiSession:
|
||||
content = message.get("content")
|
||||
if role not in ("user", "assistant") or not content:
|
||||
continue
|
||||
if (
|
||||
role == "user"
|
||||
and isinstance(content, str)
|
||||
and content.startswith(INTERNAL_PREFIXES)
|
||||
):
|
||||
if not isinstance(content, str):
|
||||
continue
|
||||
content = normalize_newlines(content).strip()
|
||||
if not content:
|
||||
continue
|
||||
if role == "user" and content.startswith(INTERNAL_PREFIXES):
|
||||
continue
|
||||
visible.append({"role": role, "content": content})
|
||||
return visible
|
||||
@@ -228,6 +254,10 @@ class DeviiSession:
|
||||
self._disconnected.clear()
|
||||
self.avatar.bind(self._avatar_request)
|
||||
self.browser.bind(self._client_request)
|
||||
self.interaction_broker.bind(
|
||||
emit=self._emit_interaction,
|
||||
wait_site=self._interaction_wait,
|
||||
)
|
||||
self.ensure_scheduler_started()
|
||||
if self._buffer:
|
||||
pending = self._buffer
|
||||
@@ -248,6 +278,14 @@ class DeviiSession:
|
||||
self._disconnected.set()
|
||||
self.avatar.unbind()
|
||||
self.browser.unbind()
|
||||
open_id = self.interaction_broker.open_id()
|
||||
if open_id:
|
||||
self.interaction_broker.resolve(
|
||||
open_id,
|
||||
status="cancelled",
|
||||
values={},
|
||||
error="Browser disconnected.",
|
||||
)
|
||||
logger.info(
|
||||
"Session %s/%s detached (%d conns)",
|
||||
self.owner_kind,
|
||||
@@ -457,7 +495,13 @@ class DeviiSession:
|
||||
for s in schemas
|
||||
if s.get("function", {}).get("name") in DOCS_TOOLS
|
||||
]
|
||||
return schemas
|
||||
allowed = set(self.interaction_broker.channel_context().tools)
|
||||
return [
|
||||
s
|
||||
for s in schemas
|
||||
if s.get("function", {}).get("name") not in UI_TOOL_NAMES
|
||||
or s.get("function", {}).get("name") in allowed
|
||||
]
|
||||
|
||||
def _refresh_tools(self) -> None:
|
||||
if self.channel == "docs":
|
||||
@@ -471,7 +515,14 @@ class DeviiSession:
|
||||
return self._system_prompt
|
||||
body = self._behavior_store.text().strip()
|
||||
section = BEHAVIOR_HEADER if not body else f"{BEHAVIOR_HEADER}\n{body}"
|
||||
return f"{self._system_prompt}\n\n{self._clock_line()}\n\n{section}"
|
||||
channel_block = fragment_for(self.interaction_broker.channel_context())
|
||||
return (
|
||||
f"{self._system_prompt}\n\n"
|
||||
f"{CA_IWP_SYSTEM_FRAGMENT}\n\n"
|
||||
f"{channel_block}\n\n"
|
||||
f"{self._clock_line()}\n\n"
|
||||
f"{section}"
|
||||
)
|
||||
|
||||
def _clock_line(self) -> str:
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -745,6 +796,144 @@ class DeviiSession:
|
||||
if len(self._buffer) > 100:
|
||||
self._buffer = self._buffer[-100:]
|
||||
|
||||
async def _emit_interaction(self, payload: dict[str, Any]) -> None:
|
||||
await self._emit(payload, buffer=False)
|
||||
|
||||
async def _interaction_wait(
|
||||
self, interaction_id: str, frame: dict[str, Any], timeout: float
|
||||
) -> Any:
|
||||
loop = asyncio.get_event_loop()
|
||||
deadline_seconds = timeout if timeout and timeout > 0 else BROWSER_REQUEST_DEADLINE_SECONDS
|
||||
if timeout and timeout > 0:
|
||||
deadline_seconds = max(timeout, 1.0)
|
||||
else:
|
||||
deadline_seconds = max(BROWSER_REQUEST_DEADLINE_SECONDS, 300.0)
|
||||
deadline = loop.time() + deadline_seconds
|
||||
request_id = f"ix:{interaction_id}"
|
||||
future: asyncio.Future = loop.create_future()
|
||||
self._pending[request_id] = future
|
||||
wire = {
|
||||
"type": "interaction",
|
||||
"id": request_id,
|
||||
"interaction_id": interaction_id,
|
||||
"args": frame,
|
||||
}
|
||||
try:
|
||||
while not future.done():
|
||||
remaining = deadline - loop.time()
|
||||
if remaining <= 0:
|
||||
raise asyncio.TimeoutError()
|
||||
try:
|
||||
await asyncio.wait_for(self._connected.wait(), timeout=remaining)
|
||||
except asyncio.TimeoutError as exc:
|
||||
raise asyncio.TimeoutError() from exc
|
||||
ws = self._pick_target()
|
||||
if ws is None:
|
||||
continue
|
||||
try:
|
||||
await self._send_to(ws, wire)
|
||||
except Exception:
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
drop = asyncio.create_task(self._disconnected.wait())
|
||||
try:
|
||||
await asyncio.wait(
|
||||
{future, drop},
|
||||
timeout=max(0.0, deadline - loop.time()),
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
finally:
|
||||
drop.cancel()
|
||||
return future.result()
|
||||
finally:
|
||||
self._pending.pop(request_id, None)
|
||||
|
||||
def resolve_interaction(self, interaction_id: str, payload: Any) -> None:
|
||||
request_id = str(interaction_id or "")
|
||||
if not request_id.startswith("ix:"):
|
||||
request_id = f"ix:{request_id}"
|
||||
future = self._pending.get(request_id)
|
||||
if future is not None and not future.done():
|
||||
future.set_result(payload)
|
||||
self._pending.pop(request_id, None)
|
||||
bare = request_id[3:]
|
||||
if isinstance(payload, dict):
|
||||
self.interaction_broker.resolve(
|
||||
bare,
|
||||
status=str(payload.get("status") or "submitted"),
|
||||
values=payload.get("values")
|
||||
if isinstance(payload.get("values"), dict)
|
||||
else {},
|
||||
error=payload.get("error"),
|
||||
meta=payload.get("meta")
|
||||
if isinstance(payload.get("meta"), dict)
|
||||
else None,
|
||||
)
|
||||
return
|
||||
bare = request_id[3:] if request_id.startswith("ix:") else request_id
|
||||
self.interaction_broker.resolve(
|
||||
bare,
|
||||
status=str((payload or {}).get("status") or "submitted")
|
||||
if isinstance(payload, dict)
|
||||
else "error",
|
||||
values=(payload or {}).get("values")
|
||||
if isinstance(payload, dict)
|
||||
else {},
|
||||
error=(payload or {}).get("error") if isinstance(payload, dict) else None,
|
||||
meta=(payload or {}).get("meta") if isinstance(payload, dict) else None,
|
||||
)
|
||||
|
||||
async def try_answer_interaction(self, text: str) -> bool:
|
||||
open_id = self.interaction_broker.open_id()
|
||||
if not open_id:
|
||||
return False
|
||||
pending = self._pending.get(f"ix:{open_id}")
|
||||
if pending is not None and pending.done():
|
||||
return False
|
||||
outcome = self.interaction_broker.answer_text(text)
|
||||
if not outcome or not outcome.get("handled"):
|
||||
return False
|
||||
if outcome.get("status") == "error":
|
||||
result = {
|
||||
"status": "cancelled",
|
||||
"interaction_id": open_id,
|
||||
"values": {},
|
||||
"meta": {
|
||||
"adapter": "site-chat",
|
||||
"via": "text_non_answer",
|
||||
"degraded": True,
|
||||
},
|
||||
"error": None,
|
||||
}
|
||||
self.resolve_interaction(open_id, result)
|
||||
await self._emit(
|
||||
{
|
||||
"type": "interaction_closed",
|
||||
"id": f"ix:{open_id}",
|
||||
"interaction_id": open_id,
|
||||
"result": result,
|
||||
},
|
||||
buffer=False,
|
||||
)
|
||||
return False
|
||||
result = outcome.get("result") or {
|
||||
"status": outcome.get("status") or "submitted",
|
||||
"interaction_id": open_id,
|
||||
"values": {},
|
||||
"meta": {"adapter": "site-chat", "via": "text", "degraded": True},
|
||||
}
|
||||
self.resolve_interaction(open_id, result)
|
||||
await self._emit(
|
||||
{
|
||||
"type": "interaction_closed",
|
||||
"id": f"ix:{open_id}",
|
||||
"interaction_id": open_id,
|
||||
"result": result,
|
||||
},
|
||||
buffer=False,
|
||||
)
|
||||
return True
|
||||
|
||||
async def _avatar_request(self, action: str, args: dict[str, Any]) -> Any:
|
||||
return await self._browser_request("avatar", action, args)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user