203 lines
10 KiB
Python
203 lines
10 KiB
Python
"""Runtime and global execution-mode service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from opc.plugins.office_ui.snapshot_builder import build_collab_sync, build_snapshot
|
|
|
|
from .context import OfficeServiceContext
|
|
from .models import ServiceError, ServiceEvent, ServiceResult
|
|
from .session import SessionService
|
|
|
|
|
|
class RuntimeService:
|
|
def __init__(self, context: OfficeServiceContext, session_service: SessionService) -> None:
|
|
self.context = context
|
|
self.session_service = session_service
|
|
|
|
async def mode_show(self) -> ServiceResult:
|
|
active_org = ""
|
|
if self.context.mode_state.exec_mode == "org" and self.context.get_active_saved_org_name is not None:
|
|
active_org = await self.context.get_active_saved_org_name()
|
|
return ServiceResult({
|
|
"mode": self.context.mode_state.exec_mode,
|
|
"profile": self.context.mode_state.company_profile,
|
|
"org_id": active_org,
|
|
"preferred_agent": self.context.mode_state.task_preferred_agent,
|
|
})
|
|
|
|
async def status(self, *, project_id: str, limit: int = 50) -> ServiceResult:
|
|
engine = await self.context.engine_for_project(project_id)
|
|
store = getattr(engine, "store", None)
|
|
payload: dict[str, Any] = {
|
|
"project_id": project_id,
|
|
"mode": self.context.mode_state.exec_mode,
|
|
"profile": self.context.mode_state.company_profile,
|
|
"preferred_agent": self.context.mode_state.task_preferred_agent,
|
|
"active_tasks": [],
|
|
"runtime_sessions": [],
|
|
"external_sessions": [],
|
|
"checkpoints": [],
|
|
}
|
|
if not self.context.store_is_ready(store):
|
|
payload["available"] = False
|
|
payload["reason"] = "store_not_ready"
|
|
return ServiceResult(payload)
|
|
from opc.core.models import TaskStatus
|
|
|
|
terminal = {TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED}
|
|
tasks = await store.get_tasks(project_id=project_id) if hasattr(store, "get_tasks") else []
|
|
payload["active_tasks"] = [
|
|
{
|
|
"task_id": getattr(task, "id", ""),
|
|
"title": getattr(task, "title", ""),
|
|
"status": getattr(getattr(task, "status", None), "value", getattr(task, "status", "")),
|
|
"session_id": getattr(task, "session_id", ""),
|
|
"assigned_to": getattr(task, "assigned_to", ""),
|
|
}
|
|
for task in tasks
|
|
if getattr(task, "status", None) not in terminal
|
|
][:limit]
|
|
if hasattr(store, "list_runtime_sessions"):
|
|
payload["runtime_sessions"] = await store.list_runtime_sessions(project_id=project_id, limit=limit)
|
|
if hasattr(store, "list_external_sessions"):
|
|
payload["external_sessions"] = await store.list_external_sessions(project_id=project_id, limit=limit)
|
|
if hasattr(store, "get_pending_checkpoints"):
|
|
payload["checkpoints"] = await store.get_pending_checkpoints(project_id=project_id)
|
|
payload["checkpoints"] = payload["checkpoints"][:limit]
|
|
return ServiceResult(payload)
|
|
|
|
async def mode_set(
|
|
self,
|
|
*,
|
|
mode: str,
|
|
profile: str = "corporate",
|
|
preferred_agent: str | None = None,
|
|
org_id: str | None = None,
|
|
sync_config: bool = True,
|
|
) -> ServiceResult:
|
|
new_mode = self.session_service.normalize_exec_mode(mode)
|
|
normalized_org_id = self.session_service.normalize_org_id(org_id)
|
|
if new_mode == "org":
|
|
profile = "custom"
|
|
if sync_config and normalized_org_id and self.context.load_active_org_config:
|
|
if not self.context.load_active_org_config(normalized_org_id):
|
|
raise ServiceError("org_not_found", "org_not_found", {"org_id": normalized_org_id})
|
|
if self.context.set_active_saved_org_name:
|
|
await self.context.set_active_saved_org_name(normalized_org_id)
|
|
else:
|
|
normalized_org_id = ""
|
|
profile = self.session_service.normalize_company_profile(profile)
|
|
if new_mode == "company" and profile == "custom":
|
|
profile = "corporate"
|
|
agent = self.session_service.normalize_preferred_agent(
|
|
preferred_agent if preferred_agent is not None else self.context.mode_state.task_preferred_agent,
|
|
default=self.context.mode_state.task_preferred_agent,
|
|
)
|
|
self.context.mode_state.exec_mode = new_mode
|
|
self.context.mode_state.company_profile = profile
|
|
self.context.mode_state.task_preferred_agent = agent
|
|
if self.context.agent_store:
|
|
await self.context.agent_store.set_server_state("exec_mode", new_mode)
|
|
await self.context.agent_store.set_server_state("company_profile", profile)
|
|
await self.context.agent_store.set_server_state("task_preferred_agent", agent)
|
|
if getattr(self.context.engine, "org_engine", None) and self.context.agent_store:
|
|
await self.context.agent_store.load_preset("custom" if new_mode == "org" else profile, self.context.engine.org_engine)
|
|
snapshot = await build_snapshot(
|
|
self.context.engine,
|
|
self.context.agent_store,
|
|
self.context.chat_store,
|
|
self.context.event_adapter,
|
|
)
|
|
snapshot["exec_mode"] = new_mode
|
|
snapshot["company_profile"] = profile
|
|
snapshot["task_preferred_agent"] = agent
|
|
collab = await build_collab_sync(
|
|
self.context.engine,
|
|
self.context.agent_store,
|
|
self.context.chat_store,
|
|
self.context.event_adapter,
|
|
exec_mode=new_mode,
|
|
)
|
|
payload = {"mode": new_mode, "profile": profile, "org_id": normalized_org_id, "preferred_agent": agent}
|
|
return ServiceResult(payload, [ServiceEvent("snapshot", snapshot), ServiceEvent("collab_sync_push", collab)])
|
|
|
|
async def run_task(self, *, project_id: str, task_id: str) -> ServiceResult:
|
|
engine = await self.context.engine_for_project(project_id)
|
|
task = await engine.store.get_task(task_id) if getattr(engine, "store", None) else None
|
|
if not task:
|
|
raise ServiceError("task_not_found", "task_not_found", {"task_id": task_id})
|
|
prompt = f"{getattr(task, 'title', '')}\n{getattr(task, 'description', '')}".strip()
|
|
return await self.session_service.send(
|
|
project_id=project_id,
|
|
task_id=task_id,
|
|
content=prompt,
|
|
)
|
|
|
|
async def checkpoints(self, *, project_id: str, limit: int = 50) -> ServiceResult:
|
|
engine = await self.context.engine_for_project(project_id)
|
|
store = getattr(engine, "store", None)
|
|
checkpoints = await store.get_pending_checkpoints(project_id=project_id) if store and hasattr(store, "get_pending_checkpoints") else []
|
|
return ServiceResult({"project_id": project_id, "checkpoints": checkpoints[-limit:]})
|
|
|
|
async def logs(self, *, project_id: str, task_id: str, limit: int = 100) -> ServiceResult:
|
|
engine = await self.context.engine_for_project(project_id)
|
|
store = getattr(engine, "store", None)
|
|
task = await store.get_task(task_id) if store else None
|
|
if not task:
|
|
raise ServiceError("task_not_found", "task_not_found", {"task_id": task_id})
|
|
metadata = dict(getattr(task, "metadata", {}) or {})
|
|
transcript = await store.get_session_transcript(task.session_id) if getattr(task, "session_id", None) else []
|
|
runtime_sessions = []
|
|
runtime_events: list[dict[str, Any]] = []
|
|
if hasattr(store, "list_runtime_sessions"):
|
|
runtime_sessions = await store.list_runtime_sessions(project_id=project_id, task_id=task_id, limit=limit)
|
|
if runtime_sessions and hasattr(store, "list_runtime_events"):
|
|
for session in runtime_sessions:
|
|
runtime_id = str(session.get("runtime_session_id", "") or "")
|
|
if runtime_id:
|
|
runtime_events.extend(await store.list_runtime_events(runtime_id, limit=limit))
|
|
enriched_events = [self._runtime_event_payload(event) for event in runtime_events[-limit:]]
|
|
return ServiceResult({
|
|
"project_id": project_id,
|
|
"task_id": task_id,
|
|
"target": {
|
|
"task_id": task_id,
|
|
"session_id": str(getattr(task, "session_id", "") or ""),
|
|
"title": str(getattr(task, "title", "") or ""),
|
|
"status": str(getattr(getattr(task, "status", None), "value", getattr(task, "status", "")) or ""),
|
|
"role_id": str(metadata.get("role_id") or getattr(task, "assigned_to", "") or ""),
|
|
"agent_id": str(metadata.get("agent_id") or metadata.get("preferred_agent") or ""),
|
|
"work_item_id": str(
|
|
metadata.get("work_item_id")
|
|
or metadata.get("linked_work_item_id")
|
|
or ""
|
|
),
|
|
},
|
|
"transcript": transcript[-limit:],
|
|
"runtime_sessions": runtime_sessions,
|
|
"runtime_events": enriched_events,
|
|
})
|
|
|
|
@staticmethod
|
|
def _runtime_event_payload(event: Any) -> dict[str, Any]:
|
|
if isinstance(event, dict):
|
|
payload = dict(event)
|
|
elif hasattr(event, "model_dump"):
|
|
payload = dict(event.model_dump())
|
|
else:
|
|
payload = dict(getattr(event, "__dict__", {}) or {})
|
|
event_type = str(payload.get("event_type") or payload.get("type") or "")
|
|
raw_payload = payload.get("payload")
|
|
if isinstance(raw_payload, dict):
|
|
tool_name = str(raw_payload.get("tool_name") or raw_payload.get("name") or "")
|
|
summary = str(raw_payload.get("summary") or raw_payload.get("result_summary") or raw_payload.get("text") or "")
|
|
else:
|
|
tool_name = ""
|
|
summary = ""
|
|
display_parts = [part for part in (event_type, tool_name, summary) if part]
|
|
payload["display_text"] = " | ".join(display_parts)
|
|
payload["event_type"] = event_type
|
|
return payload
|