278 lines
10 KiB
Python
278 lines
10 KiB
Python
"""Agent 可调用的专业软件任务工具。"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from uuid import UUID, uuid4
|
|
|
|
from core.software_contracts import (
|
|
get_contracts,
|
|
get_contract,
|
|
node_available_slots,
|
|
supported_capabilities,
|
|
)
|
|
from core.software_jobs import (
|
|
SoftwareJobError,
|
|
create_job,
|
|
get_job,
|
|
get_job_request,
|
|
list_jobs,
|
|
request_job_cancel,
|
|
revise_job,
|
|
)
|
|
from core.software_nodes import list_nodes
|
|
|
|
from .base import Tool
|
|
|
|
|
|
def _contract_property_schema(name: str) -> dict:
|
|
schemas = [
|
|
item.submission_schema()["properties"][name]
|
|
for item in get_contracts().values()
|
|
]
|
|
unique = {json.dumps(item, ensure_ascii=False, sort_keys=True): item for item in schemas}
|
|
values = list(unique.values())
|
|
return values[0] if len(values) == 1 else {"anyOf": values}
|
|
|
|
|
|
class _SoftwareJobTool(Tool):
|
|
def __init__(self, user_id: UUID, task_id: UUID, **kwargs) -> None:
|
|
super().__init__(**kwargs)
|
|
self.user_id = user_id
|
|
self.task_id = task_id
|
|
|
|
|
|
class SoftwareCapabilityListTool(_SoftwareJobTool):
|
|
name = "software_capability_list"
|
|
description = "List professional software capabilities available through managed Windows nodes."
|
|
parameters = {"type": "object", "properties": {}, "additionalProperties": False}
|
|
|
|
def execute(self) -> str:
|
|
nodes = list_nodes()
|
|
items = [{
|
|
"capability": item,
|
|
"display_name": get_contract(item).display_name,
|
|
"available_nodes": sum(
|
|
1
|
|
for node in nodes
|
|
if node["status"] == "online"
|
|
and item in (node.get("capabilities") or [])
|
|
and node_available_slots(item, node.get("runtime") or {}) > 0
|
|
),
|
|
} for item in sorted(supported_capabilities())]
|
|
return json.dumps({"capabilities": items}, ensure_ascii=False)
|
|
|
|
|
|
class SoftwareJobSubmitTool(_SoftwareJobTool):
|
|
name = "software_job_submit"
|
|
description = (
|
|
"Submit a managed professional-software job using registered artifacts and a "
|
|
"capability contract. Call register_artifact first for workspace files. A successful "
|
|
"submission is the terminal action for the current run: report the queued job_id once "
|
|
"and end the turn. Completion delivery and artifact publication happen automatically "
|
|
"in a later system-managed message."
|
|
)
|
|
@staticmethod
|
|
def _parameters() -> dict:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"capability": {
|
|
"type": "string", "enum": sorted(supported_capabilities())
|
|
},
|
|
"inputs": _contract_property_schema("inputs"),
|
|
"operation": _contract_property_schema("operation"),
|
|
"outputs": _contract_property_schema("outputs"),
|
|
"completion_action": {
|
|
"type": "string",
|
|
"enum": ["report", "analyze"],
|
|
"default": "report",
|
|
"description": (
|
|
"Use report to notify with output files only. Use analyze when the "
|
|
"user also asked for interpretation, conclusions, or data analysis."
|
|
),
|
|
},
|
|
"idempotency_key": {
|
|
"type": "string",
|
|
"description": (
|
|
"Stable unique key for this exact submission; omit to generate one."
|
|
),
|
|
},
|
|
},
|
|
"required": ["capability", "inputs", "operation", "outputs"],
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
# Compatibility for callers inspecting the class; Tool.schema below is always fresh.
|
|
parameters = _parameters()
|
|
|
|
@property
|
|
def schema(self) -> dict:
|
|
value = super().schema
|
|
value["function"]["parameters"] = self._parameters()
|
|
return value
|
|
|
|
def execute(
|
|
self,
|
|
capability: str,
|
|
inputs: list[dict] | None = None,
|
|
operation: dict | None = None,
|
|
outputs: list[dict] | None = None,
|
|
completion_action: str = "report",
|
|
idempotency_key: str = "",
|
|
) -> str:
|
|
try:
|
|
if not inputs or operation is None or not outputs:
|
|
return "[Error] inputs, operation, and outputs are required for a software job"
|
|
canonical_inputs = []
|
|
for item in inputs:
|
|
if not isinstance(item, dict):
|
|
return "[Error] each input must be an object"
|
|
try:
|
|
artifact_id = str(UUID(str(item.get("artifact_id") or "")))
|
|
except ValueError:
|
|
return "[Error] every artifact_id must be a UUID; call register_artifact first"
|
|
canonical = {**item, "artifact_id": artifact_id}
|
|
canonical_inputs.append(canonical)
|
|
normalized_request = {
|
|
"schema_version": get_contract(capability).request_schema[
|
|
"properties"
|
|
]["schema_version"]["const"],
|
|
"inputs": canonical_inputs,
|
|
"operation": operation,
|
|
"outputs": outputs,
|
|
}
|
|
job, created = create_job(
|
|
self.user_id,
|
|
self.task_id,
|
|
idempotency_key=idempotency_key.strip() or str(uuid4()),
|
|
capability=capability,
|
|
completion_action=completion_action,
|
|
request=normalized_request,
|
|
)
|
|
return json.dumps({
|
|
**job,
|
|
"created": created,
|
|
"completion_delivery": "automatic",
|
|
"next_action": "end_turn_after_reporting_queued_job_id",
|
|
}, ensure_ascii=False)
|
|
except (SoftwareJobError, ValueError) as exc:
|
|
return f"[Error] {exc}"
|
|
|
|
|
|
class SoftwareJobStatusTool(_SoftwareJobTool):
|
|
name = "software_job_status"
|
|
description = (
|
|
"Check one software job, or list recent jobs in the current task when job_id is omitted. "
|
|
"A succeeded job returns output_dir as the authoritative starting directory; "
|
|
"inspect or search within that directory to analyze its files."
|
|
)
|
|
parameters = {
|
|
"type": "object",
|
|
"properties": {"job_id": {"type": "string"}},
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
def execute(self, job_id: str = "") -> str:
|
|
try:
|
|
if job_id.strip():
|
|
item = get_job(self.user_id, UUID(job_id.strip()))
|
|
if item is None or item["task_id"] != str(self.task_id):
|
|
return "[Error] software job not found"
|
|
item["editable_request"] = get_job_request(
|
|
self.user_id, self.task_id, UUID(job_id.strip())
|
|
)
|
|
return json.dumps(item, ensure_ascii=False)
|
|
return json.dumps(
|
|
{"results": list_jobs(self.user_id, task_id=self.task_id, limit=20)},
|
|
ensure_ascii=False,
|
|
)
|
|
except ValueError:
|
|
return "[Error] invalid job_id"
|
|
|
|
|
|
class SoftwareJobReviseTool(_SoftwareJobTool):
|
|
name = "software_job_revise"
|
|
description = (
|
|
"Create a new professional-software job from a prior job in the current task. "
|
|
"Reuse the prior registered inputs, provide a complete replacement operation and "
|
|
"outputs, and leave the prior job and artifacts unchanged. Call software_job_status "
|
|
"first when the prior editable_request is not already in context. A successful revision "
|
|
"is the terminal action for the current run; completion and artifacts are delivered "
|
|
"automatically in a later system-managed message."
|
|
)
|
|
|
|
@staticmethod
|
|
def _parameters() -> dict:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"source_job_id": {"type": "string", "format": "uuid"},
|
|
"operation": _contract_property_schema("operation"),
|
|
"outputs": _contract_property_schema("outputs"),
|
|
"idempotency_key": {
|
|
"type": "string",
|
|
"description": (
|
|
"Stable unique key for this exact revision; omit to generate one."
|
|
),
|
|
},
|
|
},
|
|
"required": ["source_job_id", "operation", "outputs"],
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
parameters = _parameters()
|
|
|
|
@property
|
|
def schema(self) -> dict:
|
|
value = super().schema
|
|
value["function"]["parameters"] = self._parameters()
|
|
return value
|
|
|
|
def execute(
|
|
self,
|
|
source_job_id: str,
|
|
operation: dict | None = None,
|
|
outputs: list[dict] | None = None,
|
|
idempotency_key: str = "",
|
|
) -> str:
|
|
try:
|
|
if operation is None or not outputs:
|
|
return "[Error] operation and outputs are required for a software revision"
|
|
job, created = revise_job(
|
|
self.user_id,
|
|
self.task_id,
|
|
UUID(source_job_id.strip()),
|
|
idempotency_key=idempotency_key.strip() or str(uuid4()),
|
|
operation=operation,
|
|
outputs=outputs,
|
|
)
|
|
return json.dumps({
|
|
**job,
|
|
"created": created,
|
|
"completion_delivery": "automatic",
|
|
"next_action": "end_turn_after_reporting_queued_job_id",
|
|
}, ensure_ascii=False)
|
|
except (SoftwareJobError, ValueError) as exc:
|
|
return f"[Error] {exc}"
|
|
|
|
|
|
class SoftwareJobCancelTool(_SoftwareJobTool):
|
|
name = "software_job_cancel"
|
|
description = "Request cancellation of a software job in the current task after the user asks to stop it."
|
|
parameters = {
|
|
"type": "object",
|
|
"properties": {"job_id": {"type": "string"}},
|
|
"required": ["job_id"],
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
def execute(self, job_id: str) -> str:
|
|
try:
|
|
item = get_job(self.user_id, UUID(job_id.strip()))
|
|
if item is None or item["task_id"] != str(self.task_id):
|
|
return "[Error] software job not found"
|
|
job, _ = request_job_cancel(self.user_id, UUID(job_id.strip()))
|
|
return json.dumps(job, ensure_ascii=False)
|
|
except (ValueError, SoftwareJobError) as exc:
|
|
return f"[Error] {exc}"
|