feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..config import Settings
|
||||
from ..errors import ToolInputError
|
||||
from .lessons import LessonStore
|
||||
from .loop import TraceCallback, react_loop
|
||||
from .state import AgentState, get_state
|
||||
|
||||
logger = logging.getLogger("devii.agentic.controller")
|
||||
|
||||
SUB_AGENT_SYSTEM_PROMPT = (
|
||||
"You are a focused sub-agent spawned to complete a single scoped task on this "
|
||||
"DevPlace platform. Begin with a plan() call. Investigate, then act with the most specific "
|
||||
"tools available. If you change anything, confirm it and call verify() before returning. "
|
||||
"Return a concise factual result string under 1500 characters."
|
||||
)
|
||||
NO_DELEGATE = "delegate"
|
||||
|
||||
|
||||
class AgenticController:
|
||||
def __init__(self, lessons: LessonStore, settings: Settings) -> None:
|
||||
self._lessons = lessons
|
||||
self._settings = settings
|
||||
self._llm: Any = None
|
||||
self._dispatcher: Any = None
|
||||
self._tools: list[dict[str, Any]] = []
|
||||
self._on_trace: Optional[TraceCallback] = None
|
||||
self._cost_tracker: Any = None
|
||||
self._chunk_store: Any = None
|
||||
|
||||
def bind(
|
||||
self,
|
||||
llm: Any,
|
||||
dispatcher: Any,
|
||||
tools: list[dict[str, Any]],
|
||||
on_trace: Optional[TraceCallback],
|
||||
cost_tracker: Any = None,
|
||||
chunk_store: Any = None,
|
||||
) -> None:
|
||||
self._llm = llm
|
||||
self._dispatcher = dispatcher
|
||||
self._tools = tools
|
||||
self._on_trace = on_trace
|
||||
self._cost_tracker = cost_tracker
|
||||
self._chunk_store = chunk_store
|
||||
|
||||
async def dispatch(self, name: str, arguments: dict[str, Any]) -> str:
|
||||
handlers = {
|
||||
"plan": self._plan,
|
||||
"reflect": self._reflect,
|
||||
"recall": self._recall,
|
||||
"forget_lessons": self._forget,
|
||||
"verify": self._verify,
|
||||
"delegate": self._delegate,
|
||||
}
|
||||
handler = handlers.get(name)
|
||||
if handler is None:
|
||||
raise ToolInputError(f"Unknown agentic tool: {name}")
|
||||
return await handler(arguments)
|
||||
|
||||
async def _plan(self, arguments: dict[str, Any]) -> str:
|
||||
goal = str(arguments.get("goal", "")).strip()
|
||||
steps = arguments.get("steps") or []
|
||||
if not goal:
|
||||
raise ToolInputError("plan requires a goal.")
|
||||
if not isinstance(steps, list) or not steps:
|
||||
raise ToolInputError("plan requires a non-empty steps list.")
|
||||
confidence = float(arguments.get("confidence", 0.8) or 0.8)
|
||||
state = get_state()
|
||||
if state is not None:
|
||||
state.plan = {
|
||||
"goal": goal,
|
||||
"steps": steps,
|
||||
"success_criteria": arguments.get("success_criteria", ""),
|
||||
"confidence": confidence,
|
||||
}
|
||||
advice = ""
|
||||
if confidence < 0.6:
|
||||
advice = "Confidence is below 0.6 - gather more context or recall() past lessons before executing."
|
||||
return json.dumps(
|
||||
{"status": "success", "plan_recorded": True, "step_count": len(steps), "advice": advice},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
async def _reflect(self, arguments: dict[str, Any]) -> str:
|
||||
observation = str(arguments.get("observation", "")).strip()
|
||||
conclusion = str(arguments.get("conclusion", "")).strip()
|
||||
next_action = str(arguments.get("next_action", "")).strip()
|
||||
if not (observation and conclusion and next_action):
|
||||
raise ToolInputError("reflect requires observation, conclusion, and next_action.")
|
||||
tags = str(arguments.get("tags", "") or "").strip()
|
||||
record = self._lessons.add(observation, conclusion, next_action, tags)
|
||||
state = get_state()
|
||||
if state is not None:
|
||||
state.reflections.append(
|
||||
{"observation": observation, "conclusion": conclusion, "next_action": next_action}
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"lesson_uid": record["uid"],
|
||||
"total_lessons": self._lessons.count(),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
async def _recall(self, arguments: dict[str, Any]) -> str:
|
||||
query = str(arguments.get("query", "")).strip()
|
||||
if not query:
|
||||
raise ToolInputError("recall requires a query.")
|
||||
k = int(arguments.get("k", self._settings.recall_top_k) or self._settings.recall_top_k)
|
||||
hits = self._lessons.search(query, k=k)
|
||||
return json.dumps({"status": "success", "count": len(hits), "lessons": hits}, ensure_ascii=False)
|
||||
|
||||
async def _forget(self, arguments: dict[str, Any]) -> str:
|
||||
query = str(arguments.get("query", "") or "").strip()
|
||||
if query:
|
||||
removed = 0
|
||||
for hit in self._lessons.search(query, k=50):
|
||||
uid = hit.get("uid")
|
||||
if uid and self._lessons.delete(uid):
|
||||
removed += 1
|
||||
else:
|
||||
removed = self._lessons.clear()
|
||||
return json.dumps(
|
||||
{"status": "success", "forgotten": removed, "remaining": self._lessons.count()},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
async def _verify(self, arguments: dict[str, Any]) -> str:
|
||||
summary = str(arguments.get("summary", "")).strip()
|
||||
if not summary:
|
||||
raise ToolInputError("verify requires a summary of what was confirmed.")
|
||||
confirmed = arguments.get("confirmed", True)
|
||||
if isinstance(confirmed, str):
|
||||
confirmed = confirmed.strip().lower() not in ("", "false", "no", "0")
|
||||
state = get_state()
|
||||
if state is not None and confirmed:
|
||||
state.verified = True
|
||||
return json.dumps(
|
||||
{"status": "success", "verified": bool(confirmed), "summary": summary}, ensure_ascii=False
|
||||
)
|
||||
|
||||
async def _delegate(self, arguments: dict[str, Any]) -> str:
|
||||
task = str(arguments.get("task", "")).strip()
|
||||
if not task:
|
||||
raise ToolInputError("delegate requires a task description.")
|
||||
if self._llm is None or self._dispatcher is None:
|
||||
raise ToolInputError("Delegation is not available in this context.")
|
||||
allowed = arguments.get("allowed_tools")
|
||||
if allowed:
|
||||
allowed_set = {str(name) for name in allowed}
|
||||
tools = [
|
||||
tool
|
||||
for tool in self._tools
|
||||
if tool["function"]["name"] in allowed_set and tool["function"]["name"] != NO_DELEGATE
|
||||
]
|
||||
else:
|
||||
tools = [tool for tool in self._tools if tool["function"]["name"] != NO_DELEGATE]
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SUB_AGENT_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": task},
|
||||
]
|
||||
sub_state = AgentState()
|
||||
result = await react_loop(
|
||||
llm=self._llm,
|
||||
dispatcher=self._dispatcher,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
state=sub_state,
|
||||
settings=self._settings,
|
||||
max_iterations=self._settings.delegate_max_iterations,
|
||||
plan_required=self._settings.plan_required,
|
||||
verify_required=self._settings.verify_required,
|
||||
on_trace=self._on_trace,
|
||||
cost_tracker=self._cost_tracker,
|
||||
chunk_store=self._chunk_store,
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"iterations": sub_state.iteration,
|
||||
"verified": sub_state.verified,
|
||||
"reflections": len(sub_state.reflections),
|
||||
"result": result,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
Reference in New Issue
Block a user