128 lines
5.0 KiB
Python
128 lines
5.0 KiB
Python
"""Build the program message consumed by the runtime ``fetch_oss_file`` tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, Protocol
|
|
from urllib.parse import quote, urlparse
|
|
|
|
from arr_ingestion.contracts import ArtifactRef, ROLE_CONTRACTS
|
|
from arr_processing.contracts import ProcessingRequest, canonical_json_bytes
|
|
from arr_processing.errors import ProcessingError
|
|
from arr_storage.aliyun_oss_v2 import AliyunOssConfig
|
|
from arr_storage.contracts import valid_object_key
|
|
|
|
|
|
PROGRAM_INPUT_VERSION = "arr-opera-daily-program-input-3"
|
|
|
|
|
|
class ProcessingSourceResolver(Protocol):
|
|
def source_for_attempt(self, job_id: str, attempt_no: int) -> ArtifactRef:
|
|
...
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OssProcessingMessageBuilder:
|
|
"""Resolve one ARR-owned source to its public-read HTTPS object URL.
|
|
|
|
The Agent receives an attachment-shaped descriptor containing the exact URL
|
|
that ``fetch_oss_file`` must use, plus non-secret provenance and integrity
|
|
fields. AccessKey, Secret, local paths and signed URLs never enter the
|
|
message.
|
|
"""
|
|
|
|
config: AliyunOssConfig
|
|
sources: ProcessingSourceResolver
|
|
|
|
def message(self, request: ProcessingRequest) -> str:
|
|
source = self.sources.source_for_attempt(request.job_id, request.attempt_no)
|
|
self._validate_source(request, source)
|
|
endpoint_host = self._endpoint_host()
|
|
if request.submission_grant is None:
|
|
raise ProcessingError(
|
|
"PROCESSING_SUBMISSION_GRANT_MISSING",
|
|
"direct result submission grant is unavailable",
|
|
)
|
|
payload: Dict[str, Any] = {
|
|
"contract_version": PROGRAM_INPUT_VERSION,
|
|
"job_id": request.job_id,
|
|
"attempt_no": request.attempt_no,
|
|
"source_file_id": request.source_file_id,
|
|
"processor_version": request.processor_version,
|
|
"rule_set_sha256": request.rule_set_sha256,
|
|
"oss_attachments": [
|
|
{
|
|
"id": request.source_file_id,
|
|
"name": source.original_filename,
|
|
"content_type": source.mime_type,
|
|
"size": source.byte_size,
|
|
"sha256": source.sha256,
|
|
"current_or_history": "current",
|
|
"source_ref": {"source": "oss_attachments", "index": 0},
|
|
"oss": {
|
|
"bucket": self.config.bucket,
|
|
"endpoint": endpoint_host,
|
|
"object_key": source.object_key,
|
|
"url": self._public_object_url(
|
|
endpoint_host,
|
|
source.object_key,
|
|
),
|
|
},
|
|
}
|
|
],
|
|
"oss_inline_images": [],
|
|
"attachment_fetch_policy": {
|
|
"tool_name": "fetch_oss_file",
|
|
"required_before_skill_call": True,
|
|
"applies_to": ["oss_attachments"],
|
|
"output_field": "current_attachments",
|
|
},
|
|
"result_submission": {
|
|
"tool_name": "arr_submit_processing_result",
|
|
"submission_grant": request.submission_grant,
|
|
"job_id": request.job_id,
|
|
"attempt_no": request.attempt_no,
|
|
"payload_source": "structured-result.json",
|
|
"required_after_successful_skill_call": True,
|
|
},
|
|
}
|
|
return canonical_json_bytes(payload).decode("utf-8")
|
|
|
|
@staticmethod
|
|
def _validate_source(request: ProcessingRequest, source: ArtifactRef) -> None:
|
|
expected_mime = ROLE_CONTRACTS["source_xml"][2]
|
|
if (
|
|
not isinstance(source, ArtifactRef)
|
|
or source.role != "source_xml"
|
|
or source.file_kind != "opera_xml"
|
|
or source.mime_type != expected_mime
|
|
or source.original_filename != "source.xml"
|
|
or not valid_object_key(source.object_key)
|
|
or request.job_id not in source.object_key.split("/")
|
|
):
|
|
raise ProcessingError(
|
|
"PROCESSING_SOURCE_INVALID",
|
|
"processing source object is unavailable",
|
|
)
|
|
|
|
def _endpoint_host(self) -> str:
|
|
expected_host = f"oss-{self.config.region}.aliyuncs.com"
|
|
if self.config.endpoint:
|
|
parsed = urlparse(self.config.endpoint)
|
|
if (
|
|
parsed.scheme != "https"
|
|
or parsed.hostname != expected_host
|
|
or parsed.username is not None
|
|
or parsed.password is not None
|
|
or parsed.port is not None
|
|
):
|
|
raise ProcessingError(
|
|
"PROCESSING_SOURCE_INVALID",
|
|
"public OSS endpoint is unavailable",
|
|
)
|
|
return expected_host
|
|
|
|
def _public_object_url(self, endpoint_host: str, object_key: str) -> str:
|
|
encoded_key = quote(object_key, safe="/")
|
|
return f"https://{self.config.bucket}.{endpoint_host}/{encoded_key}"
|