feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
from core.agentscope.events.agui_codec import AgentScopeAgUiCodec, to_agui_wire_event
|
||||
from core.agentscope.events.pipeline import AgentScopeEventPipeline
|
||||
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||
from core.agentscope.events.sse import to_sse_event
|
||||
from core.agentscope.events.store import NullEventStore
|
||||
|
||||
__all__ = [
|
||||
"AgentScopeAgUiCodec",
|
||||
"AgentScopeEventPipeline",
|
||||
"RedisStreamBus",
|
||||
"NullEventStore",
|
||||
"to_agui_wire_event",
|
||||
"to_sse_event",
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
_TYPE_MAP: dict[str, str] = {
|
||||
"run.started": "RUN_STARTED",
|
||||
"run.finished": "RUN_FINISHED",
|
||||
"run.error": "RUN_ERROR",
|
||||
"step.start": "STEP_STARTED",
|
||||
"step.finish": "STEP_FINISHED",
|
||||
"text.start": "TEXT_MESSAGE_START",
|
||||
"text.delta": "TEXT_MESSAGE_CONTENT",
|
||||
"text.end": "TEXT_MESSAGE_END",
|
||||
"tool.start": "TOOL_CALL_START",
|
||||
"tool.args": "TOOL_CALL_ARGS",
|
||||
"tool.end": "TOOL_CALL_END",
|
||||
"tool.result": "TOOL_CALL_RESULT",
|
||||
"tool.error": "TOOL_CALL_ERROR",
|
||||
"state.snapshot": "STATE_SNAPSHOT",
|
||||
"messages.snapshot": "MESSAGES_SNAPSHOT",
|
||||
}
|
||||
|
||||
|
||||
def to_agui_wire_event(event: dict[str, Any]) -> dict[str, Any]:
|
||||
event_type = str(event.get("type", "")).strip()
|
||||
wire_type = _TYPE_MAP.get(event_type, event_type.upper().replace(".", "_"))
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"type": wire_type,
|
||||
}
|
||||
thread_id = event.get("threadId")
|
||||
run_id = event.get("runId")
|
||||
if isinstance(thread_id, str) and thread_id:
|
||||
payload["threadId"] = thread_id
|
||||
if isinstance(run_id, str) and run_id:
|
||||
payload["runId"] = run_id
|
||||
|
||||
data = event.get("data")
|
||||
if isinstance(data, dict):
|
||||
reserved = {"type", "threadId", "runId"}
|
||||
data_map = cast(dict[str, Any], data)
|
||||
payload.update({k: v for k, v in data_map.items() if k not in reserved})
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class AgentScopeAgUiCodec:
|
||||
def to_wire(self, event: dict[str, Any]) -> dict[str, Any]:
|
||||
return to_agui_wire_event(event)
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class CodecLike(Protocol):
|
||||
def to_wire(self, event: dict[str, Any]) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
class StoreLike(Protocol):
|
||||
async def persist(self, event: dict[str, Any]) -> None: ...
|
||||
|
||||
|
||||
class BusLike(Protocol):
|
||||
async def publish(self, *, session_id: str, event: dict[str, Any]) -> str: ...
|
||||
|
||||
|
||||
class AgentScopeEventPipeline:
|
||||
_codec: CodecLike
|
||||
_store: StoreLike
|
||||
_bus: BusLike
|
||||
|
||||
def __init__(self, *, codec: CodecLike, store: StoreLike, bus: BusLike) -> None:
|
||||
self._codec = codec
|
||||
self._store = store
|
||||
self._bus = bus
|
||||
|
||||
async def emit(self, *, session_id: str, event: dict[str, Any]) -> str:
|
||||
wire_event = self._codec.to_wire(event)
|
||||
await self._store.persist(wire_event)
|
||||
return await self._bus.publish(session_id=session_id, event=wire_event)
|
||||
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
|
||||
class RedisStreamClient(Protocol):
|
||||
def xadd(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
|
||||
def xread(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
|
||||
|
||||
class RedisStreamBus:
|
||||
_client: RedisStreamClient
|
||||
_stream_prefix: str
|
||||
_read_count: int
|
||||
_block_ms: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
client: RedisStreamClient,
|
||||
stream_prefix: str,
|
||||
read_count: int = 100,
|
||||
block_ms: int = 5000,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._stream_prefix = stream_prefix
|
||||
self._read_count = read_count
|
||||
self._block_ms = block_ms
|
||||
|
||||
async def publish(self, *, session_id: str, event: dict[str, Any]) -> str:
|
||||
payload = json.dumps(event, ensure_ascii=True, separators=(",", ":"))
|
||||
result = self._client.xadd(self._stream_name(session_id), {"event": payload})
|
||||
if inspect.isawaitable(result):
|
||||
return str(await result)
|
||||
return str(result)
|
||||
|
||||
async def read(
|
||||
self,
|
||||
*,
|
||||
session_id: str,
|
||||
last_event_id: str | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
stream = self._stream_name(session_id)
|
||||
start_id = "0-0" if last_event_id is None else last_event_id
|
||||
raw = self._client.xread(
|
||||
{stream: start_id},
|
||||
count=self._read_count,
|
||||
block=self._block_ms,
|
||||
)
|
||||
response = await raw if inspect.isawaitable(raw) else raw
|
||||
if not response:
|
||||
return []
|
||||
|
||||
first = response[0]
|
||||
if (
|
||||
not isinstance(first, tuple)
|
||||
or len(first) != 2
|
||||
or not isinstance(first[1], list)
|
||||
):
|
||||
return []
|
||||
|
||||
entries = cast(list[tuple[str, dict[str, Any]]], first[1])
|
||||
rows: list[dict[str, Any]] = []
|
||||
for entry in entries:
|
||||
if (
|
||||
not isinstance(entry, tuple)
|
||||
or len(entry) != 2
|
||||
or not isinstance(entry[0], str)
|
||||
or not isinstance(entry[1], dict)
|
||||
):
|
||||
continue
|
||||
payload_map = cast(dict[str, Any], entry[1])
|
||||
event_payload = payload_map.get("event")
|
||||
if isinstance(event_payload, bytes):
|
||||
event_payload = event_payload.decode("utf-8", errors="replace")
|
||||
if not isinstance(event_payload, str):
|
||||
continue
|
||||
try:
|
||||
decoded = json.loads(event_payload)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not isinstance(decoded, dict):
|
||||
continue
|
||||
rows.append({"id": entry[0], "event": decoded})
|
||||
return rows
|
||||
|
||||
def _stream_name(self, session_id: str) -> str:
|
||||
return f"{self._stream_prefix}:{session_id}"
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from ag_ui.core.events import BaseEvent
|
||||
from ag_ui.encoder.encoder import EventEncoder
|
||||
|
||||
_EVENT_TYPE_RE = re.compile(r"^[A-Z0-9_]+$")
|
||||
_ENCODER = EventEncoder()
|
||||
|
||||
|
||||
def to_sse_event(stream_id: str, event: dict[str, Any]) -> str:
|
||||
safe_stream_id = str(stream_id).replace("\r", "").replace("\n", "")
|
||||
try:
|
||||
event_model = BaseEvent.model_validate(event)
|
||||
event_type = event_model.type.value
|
||||
encoded_data = _ENCODER.encode(event_model)
|
||||
return f"id: {safe_stream_id}\nevent: {event_type}\n{encoded_data}"
|
||||
except Exception: # noqa: BLE001
|
||||
raw_event_type = (
|
||||
str(event.get("type", "MESSAGE")).replace("\r", "").replace("\n", "")
|
||||
)
|
||||
event_type = (
|
||||
raw_event_type if _EVENT_TYPE_RE.fullmatch(raw_event_type) else "MESSAGE"
|
||||
)
|
||||
payload = json.dumps(event, ensure_ascii=True, separators=(",", ":"))
|
||||
return f"id: {safe_stream_id}\nevent: {event_type}\ndata: {payload}\n\n"
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class EventStore(Protocol):
|
||||
async def persist(self, event: dict[str, Any]) -> None: ...
|
||||
|
||||
|
||||
class NullEventStore:
|
||||
async def persist(self, event: dict[str, Any]) -> None:
|
||||
del event
|
||||
Reference in New Issue
Block a user