feat(ai): durcissement phase F (#79) - rate limit, redaction, OpenAPI/MCP, E2E
This commit is contained in:
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
@@ -35,11 +36,14 @@ logger = logging.getLogger("obsigate.agent.loop")
|
||||
DEFAULT_MAX_ITERATIONS = 10
|
||||
# Cap the size of a tool result fed back to the model (chars).
|
||||
MAX_TOOL_RESULT_CHARS = 100_000
|
||||
# Quota: maximum tool calls executed per agent run (``BOOKSLM_MAX_TOOL_CALLS``).
|
||||
DEFAULT_MAX_TOOL_CALLS = int(os.environ.get("BOOKSLM_MAX_TOOL_CALLS", "25"))
|
||||
|
||||
# Stopping reasons
|
||||
STOP_DONE = "done"
|
||||
STOP_MAX_ITERATIONS = "max_iterations"
|
||||
STOP_CONFIRMATION_REQUIRED = "confirmation_required"
|
||||
STOP_QUOTA_EXCEEDED = "quota_exceeded"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -158,6 +162,7 @@ async def run_agent(
|
||||
llm: Callable[..., Any] | None = None,
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
max_iterations: int = DEFAULT_MAX_ITERATIONS,
|
||||
max_tool_calls: int | None = None,
|
||||
on_tool_call: Callable[[ToolCallRecord], None] | None = None,
|
||||
resume_messages: list[dict[str, Any]] | None = None,
|
||||
confirm_pending: dict[str, Any] | None = None,
|
||||
@@ -173,6 +178,8 @@ async def run_agent(
|
||||
tools: Tool schemas to expose. ``None`` exposes all in-app tools;
|
||||
pass ``[]`` to disable tool calling (plain chat).
|
||||
max_iterations: Hard cap on LLM round-trips.
|
||||
max_tool_calls: Hard cap on the total number of executed tool calls
|
||||
(quota, defaults to ``BOOKSLM_MAX_TOOL_CALLS``).
|
||||
on_tool_call: Optional callback invoked after each executed tool call.
|
||||
resume_messages: Conversation snapshot from a paused run (returned with
|
||||
a ``confirmation`` event). When set, the loop resumes from it.
|
||||
@@ -187,11 +194,20 @@ async def run_agent(
|
||||
llm = llm or provider_llm
|
||||
if tools is None:
|
||||
tools = get_tool_schemas(scope=ToolScope.IN_APP)
|
||||
quota = DEFAULT_MAX_TOOL_CALLS if max_tool_calls is None else max_tool_calls
|
||||
|
||||
convo = [dict(m) for m in (resume_messages if resume_messages is not None else messages)]
|
||||
executed: list[ToolCallRecord] = []
|
||||
|
||||
if confirm_pending:
|
||||
if quota is not None and len(executed) >= quota:
|
||||
return AgentResult(
|
||||
content="",
|
||||
messages=convo,
|
||||
tool_calls=executed,
|
||||
iterations=0,
|
||||
stopped=STOP_QUOTA_EXCEEDED,
|
||||
)
|
||||
_execute_confirmed(ctx, confirm_pending, convo, executed, on_tool_call)
|
||||
|
||||
for iteration in range(1, max_iterations + 1):
|
||||
@@ -209,6 +225,15 @@ async def run_agent(
|
||||
convo.append(_assistant_tool_message(response.content, response.tool_calls))
|
||||
|
||||
for call in response.tool_calls:
|
||||
if quota is not None and len(executed) >= quota:
|
||||
logger.warning(f"Agent reached the tool-call quota ({quota})")
|
||||
return AgentResult(
|
||||
content=response.content or "",
|
||||
messages=convo,
|
||||
tool_calls=executed,
|
||||
iterations=iteration,
|
||||
stopped=STOP_QUOTA_EXCEEDED,
|
||||
)
|
||||
try:
|
||||
result = call_tool(call.name, ctx, call.arguments)
|
||||
payload = result.data
|
||||
|
||||
Reference in New Issue
Block a user