|
|
@@ -4,6 +4,8 @@ from typing import cast
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
from core_domain import (
|
|
|
+ AgentSkillRefContract,
|
|
|
+ AgentToolRefContract,
|
|
|
ChatCompletionRequestContract,
|
|
|
ChatMessageContract,
|
|
|
MemoryCreateContract,
|
|
|
@@ -22,6 +24,8 @@ from app.domain.repositories import (
|
|
|
)
|
|
|
from app.infrastructure.model_gateway_client import ModelGatewayClient, ModelGatewayClientError
|
|
|
from app.infrastructure.memory_client import MemoryClient, MemoryClientError
|
|
|
+from app.infrastructure.skill_client import SkillServiceClient, SkillServiceClientError
|
|
|
+from app.infrastructure.tool_client import ToolServiceClient, ToolServiceClientError
|
|
|
from app.schemas.agent import (
|
|
|
AgentCreateRequest,
|
|
|
AgentRunCreateRequest,
|
|
|
@@ -41,12 +45,16 @@ class AgentApplicationService:
|
|
|
agent_run_repository: AgentRunRepository,
|
|
|
model_gateway_client: ModelGatewayClient | None = None,
|
|
|
memory_client: MemoryClient | None = None,
|
|
|
+ tool_client: ToolServiceClient | None = None,
|
|
|
+ skill_client: SkillServiceClient | None = None,
|
|
|
) -> None:
|
|
|
self.agent_repository = agent_repository
|
|
|
self.agent_version_repository = agent_version_repository
|
|
|
self.agent_run_repository = agent_run_repository
|
|
|
self.model_gateway_client = model_gateway_client
|
|
|
self.memory_client = memory_client
|
|
|
+ self.tool_client = tool_client
|
|
|
+ self.skill_client = skill_client
|
|
|
|
|
|
def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
|
|
|
return self.agent_repository.create(
|
|
|
@@ -187,12 +195,18 @@ class AgentApplicationService:
|
|
|
agent_run=agent_run,
|
|
|
agent_version=agent_version,
|
|
|
)
|
|
|
- messages = self._build_chat_messages(
|
|
|
- agent_run=agent_run,
|
|
|
- agent_version=agent_version,
|
|
|
- memory_results=memory_results,
|
|
|
- )
|
|
|
+ selected_tools = self._select_tool_refs(agent_run=agent_run, agent_version=agent_version)
|
|
|
+ selected_skills = self._select_skill_refs(agent_run=agent_run, agent_version=agent_version)
|
|
|
if payload.dry_run:
|
|
|
+ messages = self._build_chat_messages(
|
|
|
+ agent_run=agent_run,
|
|
|
+ agent_version=agent_version,
|
|
|
+ memory_results=memory_results,
|
|
|
+ capability_context=self._format_capability_plan(
|
|
|
+ selected_tools=selected_tools,
|
|
|
+ selected_skills=selected_skills,
|
|
|
+ ),
|
|
|
+ )
|
|
|
return self.agent_run_repository.update_status(
|
|
|
agent_run_id=agent_run.id,
|
|
|
status="completed",
|
|
|
@@ -206,10 +220,35 @@ class AgentApplicationService:
|
|
|
"agent_version_id": agent_version.id,
|
|
|
"message_count": len(messages),
|
|
|
"messages": [message.model_dump(mode="json") for message in messages],
|
|
|
+ "selected_tool_refs": [
|
|
|
+ tool_ref.model_dump(mode="json") for tool_ref in selected_tools
|
|
|
+ ],
|
|
|
+ "selected_skill_refs": [
|
|
|
+ skill_ref.model_dump(mode="json") for skill_ref in selected_skills
|
|
|
+ ],
|
|
|
**memory_metadata,
|
|
|
},
|
|
|
)
|
|
|
|
|
|
+ tool_invocations = self._invoke_selected_tools(
|
|
|
+ agent_run=agent_run,
|
|
|
+ selected_tools=selected_tools,
|
|
|
+ )
|
|
|
+ skill_invocations = self._invoke_selected_skills(
|
|
|
+ agent_run=agent_run,
|
|
|
+ selected_skills=selected_skills,
|
|
|
+ worker_key=payload.worker_key,
|
|
|
+ )
|
|
|
+ messages = self._build_chat_messages(
|
|
|
+ agent_run=agent_run,
|
|
|
+ agent_version=agent_version,
|
|
|
+ memory_results=memory_results,
|
|
|
+ capability_context=self._format_capability_results(
|
|
|
+ tool_invocations=tool_invocations,
|
|
|
+ skill_invocations=skill_invocations,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
if self.model_gateway_client is None:
|
|
|
return self.agent_run_repository.update_status(
|
|
|
agent_run_id=agent_run.id,
|
|
|
@@ -217,6 +256,11 @@ class AgentApplicationService:
|
|
|
worker_key=payload.worker_key,
|
|
|
error_code="model_gateway_missing",
|
|
|
error_message="model gateway client is not configured",
|
|
|
+ output_json={
|
|
|
+ "tool_invocations": tool_invocations,
|
|
|
+ "skill_invocations": skill_invocations,
|
|
|
+ **memory_metadata,
|
|
|
+ },
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
@@ -263,6 +307,8 @@ class AgentApplicationService:
|
|
|
"finish_reason": response.finish_reason,
|
|
|
"usage_json": response.usage_json,
|
|
|
"raw_response_json": response.raw_response_json,
|
|
|
+ "tool_invocations": tool_invocations,
|
|
|
+ "skill_invocations": skill_invocations,
|
|
|
**memory_metadata,
|
|
|
**memory_write_metadata,
|
|
|
},
|
|
|
@@ -320,6 +366,7 @@ class AgentApplicationService:
|
|
|
agent_run: AgentRun,
|
|
|
agent_version: AgentVersion,
|
|
|
memory_results: list[MemorySearchResultContract] | None = None,
|
|
|
+ capability_context: str | None = None,
|
|
|
) -> list[ChatMessageContract]:
|
|
|
messages = [
|
|
|
ChatMessageContract(role="system", content=agent_version.system_prompt),
|
|
|
@@ -333,6 +380,8 @@ class AgentApplicationService:
|
|
|
content=self._format_memory_context(memory_results),
|
|
|
)
|
|
|
)
|
|
|
+ if capability_context:
|
|
|
+ messages.append(ChatMessageContract(role="system", content=capability_context))
|
|
|
if agent_run.input_text:
|
|
|
messages.append(ChatMessageContract(role="user", content=agent_run.input_text))
|
|
|
if agent_run.input_json:
|
|
|
@@ -344,6 +393,263 @@ class AgentApplicationService:
|
|
|
)
|
|
|
return messages
|
|
|
|
|
|
+ def _select_tool_refs(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ agent_run: AgentRun,
|
|
|
+ agent_version: AgentVersion,
|
|
|
+ ) -> list[AgentToolRefContract]:
|
|
|
+ input_preview = self._build_input_preview(agent_run)
|
|
|
+ selected: list[AgentToolRefContract] = []
|
|
|
+ for item in agent_version.tool_refs_json:
|
|
|
+ ref = AgentToolRefContract.model_validate(item)
|
|
|
+ if (
|
|
|
+ ref.required
|
|
|
+ or self._read_bool(ref.config_json, "auto_invoke", default=False)
|
|
|
+ or self._matches_selection_keywords(ref.config_json, input_preview)
|
|
|
+ ):
|
|
|
+ selected.append(ref)
|
|
|
+ return selected
|
|
|
+
|
|
|
+ def _select_skill_refs(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ agent_run: AgentRun,
|
|
|
+ agent_version: AgentVersion,
|
|
|
+ ) -> list[AgentSkillRefContract]:
|
|
|
+ input_preview = self._build_input_preview(agent_run)
|
|
|
+ selected: list[AgentSkillRefContract] = []
|
|
|
+ for item in agent_version.skill_refs_json:
|
|
|
+ ref = AgentSkillRefContract.model_validate(item)
|
|
|
+ auto_invoke = self._read_bool(ref.config_json, "auto_invoke", default=True)
|
|
|
+ if auto_invoke or self._matches_selection_keywords(ref.config_json, input_preview):
|
|
|
+ selected.append(ref)
|
|
|
+ return selected
|
|
|
+
|
|
|
+ def _invoke_selected_tools(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ agent_run: AgentRun,
|
|
|
+ selected_tools: list[AgentToolRefContract],
|
|
|
+ ) -> list[dict[str, JSONValue]]:
|
|
|
+ invocations: list[dict[str, JSONValue]] = []
|
|
|
+ for ref in selected_tools:
|
|
|
+ if ref.tool_binding_id is None:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "skipped",
|
|
|
+ "reason": "tool_binding_id_missing",
|
|
|
+ "tool_code": ref.tool_code,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ if self.tool_client is None:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "failed",
|
|
|
+ "reason": "tool_client_missing",
|
|
|
+ "tool_binding_id": ref.tool_binding_id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ detail = self.tool_client.get_tool_binding_detail(
|
|
|
+ tenant_id=agent_run.tenant_id,
|
|
|
+ binding_id=ref.tool_binding_id,
|
|
|
+ )
|
|
|
+ if not detail.binding.enabled:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "failed",
|
|
|
+ "reason": "tool_binding_disabled",
|
|
|
+ "tool_binding_id": ref.tool_binding_id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ if detail.tool_definition.tool_type != "http":
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "skipped",
|
|
|
+ "reason": "unsupported_tool_type",
|
|
|
+ "tool_type": detail.tool_definition.tool_type,
|
|
|
+ "tool_binding_id": ref.tool_binding_id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ output_text, output_json = self.tool_client.invoke_http_tool(
|
|
|
+ detail=detail,
|
|
|
+ input_json=agent_run.input_json or {},
|
|
|
+ config_json=ref.config_json,
|
|
|
+ )
|
|
|
+ except ToolServiceClientError as exc:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "failed",
|
|
|
+ "reason": str(exc),
|
|
|
+ "tool_binding_id": ref.tool_binding_id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "completed",
|
|
|
+ "tool_binding_id": ref.tool_binding_id,
|
|
|
+ "tool_code": detail.tool_definition.code,
|
|
|
+ "output_text": output_text,
|
|
|
+ "output_json": output_json,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return invocations
|
|
|
+
|
|
|
+ def _invoke_selected_skills(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ agent_run: AgentRun,
|
|
|
+ selected_skills: list[AgentSkillRefContract],
|
|
|
+ worker_key: str | None,
|
|
|
+ ) -> list[dict[str, JSONValue]]:
|
|
|
+ invocations: list[dict[str, JSONValue]] = []
|
|
|
+ for ref in selected_skills:
|
|
|
+ if self.skill_client is None:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "failed",
|
|
|
+ "reason": "skill_client_missing",
|
|
|
+ "skill_id": ref.skill_id,
|
|
|
+ "skill_code": ref.skill_code,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ skill_id = ref.skill_id or self._resolve_skill_id_by_code(
|
|
|
+ tenant_id=agent_run.tenant_id,
|
|
|
+ skill_code=ref.skill_code,
|
|
|
+ )
|
|
|
+ if skill_id is None:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "failed",
|
|
|
+ "reason": "skill_id_missing",
|
|
|
+ "skill_code": ref.skill_code,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ try:
|
|
|
+ created_run = self.skill_client.create_skill_run(
|
|
|
+ tenant_id=agent_run.tenant_id,
|
|
|
+ skill_id=skill_id,
|
|
|
+ skill_version_id=self._read_optional_string(
|
|
|
+ ref.config_json,
|
|
|
+ "skill_version_id",
|
|
|
+ ),
|
|
|
+ installation_id=self._read_optional_string(
|
|
|
+ ref.config_json,
|
|
|
+ "installation_id",
|
|
|
+ ),
|
|
|
+ input_json=self._build_skill_input_json(agent_run=agent_run, ref=ref),
|
|
|
+ )
|
|
|
+ executed_run = self.skill_client.execute_skill_run(
|
|
|
+ tenant_id=agent_run.tenant_id,
|
|
|
+ skill_run_id=created_run.id,
|
|
|
+ worker_key=worker_key,
|
|
|
+ )
|
|
|
+ except SkillServiceClientError as exc:
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": "failed",
|
|
|
+ "reason": str(exc),
|
|
|
+ "skill_id": skill_id,
|
|
|
+ "skill_code": ref.skill_code,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
+ invocations.append(
|
|
|
+ {
|
|
|
+ "status": executed_run.status,
|
|
|
+ "skill_id": skill_id,
|
|
|
+ "skill_code": ref.skill_code,
|
|
|
+ "skill_run_id": executed_run.id,
|
|
|
+ "output_text": executed_run.output_text,
|
|
|
+ "output_json": executed_run.output_json,
|
|
|
+ "error_code": executed_run.error_code,
|
|
|
+ "error_message": executed_run.error_message,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return invocations
|
|
|
+
|
|
|
+ def _format_capability_plan(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ selected_tools: list[AgentToolRefContract],
|
|
|
+ selected_skills: list[AgentSkillRefContract],
|
|
|
+ ) -> str:
|
|
|
+ return (
|
|
|
+ "Selected capability plan before model call:\n"
|
|
|
+ f"Tools: {[item.model_dump(mode='json') for item in selected_tools]}\n"
|
|
|
+ f"Skills: {[item.model_dump(mode='json') for item in selected_skills]}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def _format_capability_results(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ tool_invocations: list[dict[str, JSONValue]],
|
|
|
+ skill_invocations: list[dict[str, JSONValue]],
|
|
|
+ ) -> str | None:
|
|
|
+ if not tool_invocations and not skill_invocations:
|
|
|
+ return None
|
|
|
+ return (
|
|
|
+ "Capability invocation results before model call:\n"
|
|
|
+ f"Tools: {tool_invocations}\n"
|
|
|
+ f"Skills: {skill_invocations}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def _build_skill_input_json(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ agent_run: AgentRun,
|
|
|
+ ref: AgentSkillRefContract,
|
|
|
+ ) -> dict[str, JSONValue]:
|
|
|
+ input_json: dict[str, JSONValue] = dict(agent_run.input_json or {})
|
|
|
+ if agent_run.input_text:
|
|
|
+ input_json.setdefault("input_text", agent_run.input_text)
|
|
|
+ configured_input = ref.config_json.get("input_json")
|
|
|
+ if isinstance(configured_input, dict):
|
|
|
+ input_json.update(
|
|
|
+ {str(item_key): item_value for item_key, item_value in configured_input.items()}
|
|
|
+ )
|
|
|
+ return input_json
|
|
|
+
|
|
|
+ def _resolve_skill_id_by_code(self, *, tenant_id: str, skill_code: str | None) -> str | None:
|
|
|
+ if skill_code is None or self.skill_client is None:
|
|
|
+ return None
|
|
|
+ try:
|
|
|
+ skills = self.skill_client.list_skills(tenant_id=tenant_id)
|
|
|
+ except SkillServiceClientError:
|
|
|
+ return None
|
|
|
+ for skill in skills:
|
|
|
+ if skill.code == skill_code:
|
|
|
+ return skill.id
|
|
|
+ return None
|
|
|
+
|
|
|
+ def _build_input_preview(self, agent_run: AgentRun) -> str:
|
|
|
+ return f"{agent_run.input_text or ''} {agent_run.input_json or {}}".lower()
|
|
|
+
|
|
|
+ def _matches_selection_keywords(
|
|
|
+ self,
|
|
|
+ config_json: dict[str, JSONValue],
|
|
|
+ input_preview: str,
|
|
|
+ ) -> bool:
|
|
|
+ keywords = config_json.get("selection_keywords")
|
|
|
+ if not isinstance(keywords, list):
|
|
|
+ return False
|
|
|
+ return any(
|
|
|
+ isinstance(keyword, str) and keyword.lower() in input_preview
|
|
|
+ for keyword in keywords
|
|
|
+ )
|
|
|
+
|
|
|
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 (
|
|
|
@@ -577,4 +883,12 @@ def build_agent_application_service(
|
|
|
base_url=settings.memory_service_url,
|
|
|
timeout_seconds=settings.memory_service_timeout_seconds,
|
|
|
),
|
|
|
+ tool_client=ToolServiceClient(
|
|
|
+ base_url=settings.tool_service_url,
|
|
|
+ timeout_seconds=settings.tool_service_timeout_seconds,
|
|
|
+ ),
|
|
|
+ skill_client=SkillServiceClient(
|
|
|
+ base_url=settings.skill_service_url,
|
|
|
+ timeout_seconds=settings.skill_service_timeout_seconds,
|
|
|
+ ),
|
|
|
)
|