|
@@ -1,12 +1,17 @@
|
|
|
|
|
+from core_domain import ChatCompletionRequestContract, ChatMessageContract
|
|
|
|
|
+from core_shared import JSONValue
|
|
|
|
|
+
|
|
|
from app.db.models import AgentDefinition, AgentRun, AgentVersion
|
|
from app.db.models import AgentDefinition, AgentRun, AgentVersion
|
|
|
from app.domain.repositories import (
|
|
from app.domain.repositories import (
|
|
|
AgentDefinitionRepository,
|
|
AgentDefinitionRepository,
|
|
|
AgentRunRepository,
|
|
AgentRunRepository,
|
|
|
AgentVersionRepository,
|
|
AgentVersionRepository,
|
|
|
)
|
|
)
|
|
|
|
|
+from app.infrastructure.model_gateway_client import ModelGatewayClient, ModelGatewayClientError
|
|
|
from app.schemas.agent import (
|
|
from app.schemas.agent import (
|
|
|
AgentCreateRequest,
|
|
AgentCreateRequest,
|
|
|
AgentRunCreateRequest,
|
|
AgentRunCreateRequest,
|
|
|
|
|
+ AgentRunExecuteRequest,
|
|
|
AgentRunStatusUpdateRequest,
|
|
AgentRunStatusUpdateRequest,
|
|
|
AgentStatusUpdateRequest,
|
|
AgentStatusUpdateRequest,
|
|
|
AgentVersionCreateRequest,
|
|
AgentVersionCreateRequest,
|
|
@@ -20,10 +25,12 @@ class AgentApplicationService:
|
|
|
agent_repository: AgentDefinitionRepository,
|
|
agent_repository: AgentDefinitionRepository,
|
|
|
agent_version_repository: AgentVersionRepository,
|
|
agent_version_repository: AgentVersionRepository,
|
|
|
agent_run_repository: AgentRunRepository,
|
|
agent_run_repository: AgentRunRepository,
|
|
|
|
|
+ model_gateway_client: ModelGatewayClient | None = None,
|
|
|
) -> None:
|
|
) -> None:
|
|
|
self.agent_repository = agent_repository
|
|
self.agent_repository = agent_repository
|
|
|
self.agent_version_repository = agent_version_repository
|
|
self.agent_version_repository = agent_version_repository
|
|
|
self.agent_run_repository = agent_run_repository
|
|
self.agent_run_repository = agent_run_repository
|
|
|
|
|
+ self.model_gateway_client = model_gateway_client
|
|
|
|
|
|
|
|
def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
|
|
def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
|
|
|
return self.agent_repository.create(
|
|
return self.agent_repository.create(
|
|
@@ -128,6 +135,107 @@ class AgentApplicationService:
|
|
|
error_message=payload.error_message,
|
|
error_message=payload.error_message,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ def execute_agent_run(
|
|
|
|
|
+ self,
|
|
|
|
|
+ *,
|
|
|
|
|
+ agent_run_id: str,
|
|
|
|
|
+ payload: AgentRunExecuteRequest,
|
|
|
|
|
+ ) -> AgentRun | None:
|
|
|
|
|
+ agent_run = self.agent_run_repository.get_by_id(
|
|
|
|
|
+ tenant_id=payload.tenant_id,
|
|
|
|
|
+ agent_run_id=agent_run_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ if agent_run is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ agent_version = self.agent_version_repository.get_by_id(
|
|
|
|
|
+ tenant_id=payload.tenant_id,
|
|
|
|
|
+ agent_version_id=agent_run.agent_version_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ if agent_version is None:
|
|
|
|
|
+ return self.agent_run_repository.update_status(
|
|
|
|
|
+ agent_run_id=agent_run.id,
|
|
|
|
|
+ status="failed",
|
|
|
|
|
+ worker_key=payload.worker_key,
|
|
|
|
|
+ error_code="agent_version_missing",
|
|
|
|
|
+ error_message=f"agent version not found: {agent_run.agent_version_id}",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ self.agent_run_repository.update_status(
|
|
|
|
|
+ agent_run_id=agent_run.id,
|
|
|
|
|
+ status="running",
|
|
|
|
|
+ worker_key=payload.worker_key,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ messages = self._build_chat_messages(agent_run=agent_run, agent_version=agent_version)
|
|
|
|
|
+ if payload.dry_run:
|
|
|
|
|
+ return self.agent_run_repository.update_status(
|
|
|
|
|
+ agent_run_id=agent_run.id,
|
|
|
|
|
+ status="completed",
|
|
|
|
|
+ worker_key=payload.worker_key,
|
|
|
|
|
+ output_text=self._build_dry_run_output(
|
|
|
|
|
+ agent_run=agent_run,
|
|
|
|
|
+ agent_version=agent_version,
|
|
|
|
|
+ ),
|
|
|
|
|
+ output_json={
|
|
|
|
|
+ "dry_run": True,
|
|
|
|
|
+ "agent_version_id": agent_version.id,
|
|
|
|
|
+ "message_count": len(messages),
|
|
|
|
|
+ "messages": [message.model_dump(mode="json") for message in messages],
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if self.model_gateway_client is None:
|
|
|
|
|
+ return self.agent_run_repository.update_status(
|
|
|
|
|
+ agent_run_id=agent_run.id,
|
|
|
|
|
+ status="failed",
|
|
|
|
|
+ worker_key=payload.worker_key,
|
|
|
|
|
+ error_code="model_gateway_missing",
|
|
|
|
|
+ error_message="model gateway client is not configured",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = self.model_gateway_client.create_chat_completion(
|
|
|
|
|
+ ChatCompletionRequestContract(
|
|
|
|
|
+ model=self._read_optional_string(agent_version.model_config_json, "model"),
|
|
|
|
|
+ temperature=self._read_optional_float(
|
|
|
|
|
+ agent_version.model_config_json,
|
|
|
|
|
+ "temperature",
|
|
|
|
|
+ ),
|
|
|
|
|
+ max_tokens=self._read_optional_int(agent_version.model_config_json, "max_tokens"),
|
|
|
|
|
+ messages=messages,
|
|
|
|
|
+ metadata_json={
|
|
|
|
|
+ "tenant_id": agent_run.tenant_id,
|
|
|
|
|
+ "agent_id": agent_run.agent_id,
|
|
|
|
|
+ "agent_version_id": agent_version.id,
|
|
|
|
|
+ "agent_run_id": agent_run.id,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ except ModelGatewayClientError as exc:
|
|
|
|
|
+ return self.agent_run_repository.update_status(
|
|
|
|
|
+ agent_run_id=agent_run.id,
|
|
|
|
|
+ status="failed",
|
|
|
|
|
+ worker_key=payload.worker_key,
|
|
|
|
|
+ error_code="model_gateway_error",
|
|
|
|
|
+ error_message=str(exc),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return self.agent_run_repository.update_status(
|
|
|
|
|
+ agent_run_id=agent_run.id,
|
|
|
|
|
+ status="completed",
|
|
|
|
|
+ worker_key=payload.worker_key,
|
|
|
|
|
+ output_text=response.content,
|
|
|
|
|
+ output_json={
|
|
|
|
|
+ "dry_run": False,
|
|
|
|
|
+ "agent_version_id": agent_version.id,
|
|
|
|
|
+ "model": response.model,
|
|
|
|
|
+ "finish_reason": response.finish_reason,
|
|
|
|
|
+ "usage_json": response.usage_json,
|
|
|
|
|
+ "raw_response_json": response.raw_response_json,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
def _resolve_agent_version(
|
|
def _resolve_agent_version(
|
|
|
self,
|
|
self,
|
|
|
*,
|
|
*,
|
|
@@ -144,3 +252,50 @@ class AgentApplicationService:
|
|
|
tenant_id=tenant_id,
|
|
tenant_id=tenant_id,
|
|
|
agent_id=agent_id,
|
|
agent_id=agent_id,
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ def _build_chat_messages(
|
|
|
|
|
+ self,
|
|
|
|
|
+ *,
|
|
|
|
|
+ agent_run: AgentRun,
|
|
|
|
|
+ agent_version: AgentVersion,
|
|
|
|
|
+ ) -> list[ChatMessageContract]:
|
|
|
|
|
+ messages = [
|
|
|
|
|
+ ChatMessageContract(role="system", content=agent_version.system_prompt),
|
|
|
|
|
+ ]
|
|
|
|
|
+ if agent_version.goal:
|
|
|
|
|
+ messages.append(ChatMessageContract(role="system", content=f"Goal: {agent_version.goal}"))
|
|
|
|
|
+ if agent_run.input_text:
|
|
|
|
|
+ messages.append(ChatMessageContract(role="user", content=agent_run.input_text))
|
|
|
|
|
+ if agent_run.input_json:
|
|
|
|
|
+ messages.append(
|
|
|
|
|
+ ChatMessageContract(
|
|
|
|
|
+ role="user",
|
|
|
|
|
+ content=f"Structured input: {agent_run.input_json}",
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ return messages
|
|
|
|
|
+
|
|
|
|
|
+ def _build_dry_run_output(self, *, agent_run: AgentRun, agent_version: AgentVersion) -> str:
|
|
|
|
|
+ input_preview = agent_run.input_text or str(agent_run.input_json or {})
|
|
|
|
|
+ return (
|
|
|
|
|
+ f"[dry-run] Agent role={agent_version.role} "
|
|
|
|
|
+ f"version={agent_version.version_no} received: {input_preview}"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _read_optional_string(self, payload: dict[str, JSONValue], key: str) -> str | None:
|
|
|
|
|
+ value = payload.get(key)
|
|
|
|
|
+ if isinstance(value, str) and value:
|
|
|
|
|
+ return value
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _read_optional_float(self, payload: dict[str, JSONValue], key: str) -> float | None:
|
|
|
|
|
+ value = payload.get(key)
|
|
|
|
|
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
|
|
|
+ return float(value)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _read_optional_int(self, payload: dict[str, JSONValue], key: str) -> int | None:
|
|
|
|
|
+ value = payload.get(key)
|
|
|
|
|
+ if isinstance(value, int) and not isinstance(value, bool):
|
|
|
|
|
+ return value
|
|
|
|
|
+ return None
|