|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
|
|
|
|
|
import httpx
|
|
import httpx
|
|
|
from agent import FileSystemArtifactStore, FileSystemTaskStore, FileSystemTraceStore
|
|
from agent import FileSystemArtifactStore, FileSystemTaskStore, FileSystemTraceStore
|
|
|
|
|
+from sqlalchemy.engine import make_url
|
|
|
|
|
|
|
|
from script_build_host.adapters import (
|
|
from script_build_host.adapters import (
|
|
|
DatabaseFirstPromptSource,
|
|
DatabaseFirstPromptSource,
|
|
@@ -16,6 +17,10 @@ from script_build_host.adapters import (
|
|
|
FileKnowledgeRetrievalAdapter,
|
|
FileKnowledgeRetrievalAdapter,
|
|
|
FilePersonaSource,
|
|
FilePersonaSource,
|
|
|
HttpDecodeRetrievalAdapter,
|
|
HttpDecodeRetrievalAdapter,
|
|
|
|
|
+ LegacyExternalRetrievalAdapter,
|
|
|
|
|
+ LegacyLlmKnowledgeRetrievalAdapter,
|
|
|
|
|
+ LegacyOpenRouterClient,
|
|
|
|
|
+ LegacyVectorDecodeRetrievalAdapter,
|
|
|
PatternRetrievalAdapter,
|
|
PatternRetrievalAdapter,
|
|
|
SafeHttpClient,
|
|
SafeHttpClient,
|
|
|
SafeImageAdapter,
|
|
SafeImageAdapter,
|
|
@@ -41,6 +46,7 @@ from script_build_host.infrastructure.manifests import SettingsRuntimeManifestPr
|
|
|
from script_build_host.infrastructure.outbound import OutboundPolicy
|
|
from script_build_host.infrastructure.outbound import OutboundPolicy
|
|
|
from script_build_host.infrastructure.ownership import FencedCommandGate, OwnerLease
|
|
from script_build_host.infrastructure.ownership import FencedCommandGate, OwnerLease
|
|
|
from script_build_host.infrastructure.raw_artifacts import FileRawArtifactStore
|
|
from script_build_host.infrastructure.raw_artifacts import FileRawArtifactStore
|
|
|
|
|
+from script_build_host.infrastructure.socks_tunnel import ManagedSocks4aTunnel
|
|
|
from script_build_host.infrastructure.trace_verifier import FileSystemTraceStoreVerifier
|
|
from script_build_host.infrastructure.trace_verifier import FileSystemTraceStoreVerifier
|
|
|
from script_build_host.repositories import (
|
|
from script_build_host.repositories import (
|
|
|
LegacySqlAlchemyInputReader,
|
|
LegacySqlAlchemyInputReader,
|
|
@@ -74,17 +80,25 @@ class ProductionHost:
|
|
|
runtime_manifest: SettingsRuntimeManifestProvider
|
|
runtime_manifest: SettingsRuntimeManifestProvider
|
|
|
trace_store: Any
|
|
trace_store: Any
|
|
|
trace_store_verifier: DurableTraceStoreVerifier
|
|
trace_store_verifier: DurableTraceStoreVerifier
|
|
|
|
|
+ read_database_tunnel: ManagedSocks4aTunnel | None = None
|
|
|
require_trace_attachments: bool = False
|
|
require_trace_attachments: bool = False
|
|
|
_closed: bool = False
|
|
_closed: bool = False
|
|
|
|
|
|
|
|
async def validate_startup(self) -> None:
|
|
async def validate_startup(self) -> None:
|
|
|
"""Resolve configured endpoints and sources before serving traffic."""
|
|
"""Resolve configured endpoints and sources before serving traffic."""
|
|
|
|
|
|
|
|
- await self.runtime_manifest.load()
|
|
|
|
|
- await self.trace_store_verifier.verify(
|
|
|
|
|
- self.trace_store,
|
|
|
|
|
- require_attachments=self.require_trace_attachments,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ if self.read_database_tunnel is not None:
|
|
|
|
|
+ await self.read_database_tunnel.start()
|
|
|
|
|
+ try:
|
|
|
|
|
+ await self.runtime_manifest.load()
|
|
|
|
|
+ await self.trace_store_verifier.verify(
|
|
|
|
|
+ self.trace_store,
|
|
|
|
|
+ require_attachments=self.require_trace_attachments,
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ if self.read_database_tunnel is not None:
|
|
|
|
|
+ await self.read_database_tunnel.stop()
|
|
|
|
|
+ raise
|
|
|
|
|
|
|
|
async def close(self) -> None:
|
|
async def close(self) -> None:
|
|
|
if self._closed:
|
|
if self._closed:
|
|
@@ -92,6 +106,8 @@ class ProductionHost:
|
|
|
self._closed = True
|
|
self._closed = True
|
|
|
await self.http_client.aclose()
|
|
await self.http_client.aclose()
|
|
|
await self.database.dispose()
|
|
await self.database.dispose()
|
|
|
|
|
+ if self.read_database_tunnel is not None:
|
|
|
|
|
+ await self.read_database_tunnel.stop()
|
|
|
|
|
|
|
|
|
|
|
|
|
def compose_production_host(
|
|
def compose_production_host(
|
|
@@ -113,16 +129,40 @@ def compose_production_host(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
decode_path: Path | None = None
|
|
decode_path: Path | None = None
|
|
|
- if not settings.decode_endpoint:
|
|
|
|
|
|
|
+ vector_decode = (
|
|
|
|
|
+ not settings.decode_endpoint
|
|
|
|
|
+ and settings.decode_raw_root is not None
|
|
|
|
|
+ and (settings.decode_index_root / "manifest.json").is_file()
|
|
|
|
|
+ and (settings.decode_index_root / "meta.jsonl").is_file()
|
|
|
|
|
+ and (settings.decode_index_root / "embeddings.npy").is_file()
|
|
|
|
|
+ )
|
|
|
|
|
+ if not settings.decode_endpoint and not vector_decode:
|
|
|
if settings.environment.lower() != "test":
|
|
if settings.environment.lower() != "test":
|
|
|
- raise RuntimeError("SCRIPT_BUILD_DECODE_ENDPOINT is required in production")
|
|
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ "SCRIPT_BUILD_DECODE_ENDPOINT or a complete local vector index is required"
|
|
|
|
|
+ )
|
|
|
decode_path = _one_json_source(settings.decode_index_root, "decode")
|
|
decode_path = _one_json_source(settings.decode_index_root, "decode")
|
|
|
knowledge_path = _one_json_source(settings.knowledge_root, "knowledge")
|
|
knowledge_path = _one_json_source(settings.knowledge_root, "knowledge")
|
|
|
|
|
|
|
|
|
|
+ read_database_tunnel: ManagedSocks4aTunnel | None = None
|
|
|
|
|
+ proxy = settings.read_socks_proxy()
|
|
|
|
|
+ if proxy is not None:
|
|
|
|
|
+ direct_read_url = make_url(settings.direct_read_dsn())
|
|
|
|
|
+ if not direct_read_url.host:
|
|
|
|
|
+ raise RuntimeError("the direct read database URL has no host")
|
|
|
|
|
+ read_database_tunnel = ManagedSocks4aTunnel(
|
|
|
|
|
+ proxy_host=proxy[0],
|
|
|
|
|
+ proxy_port=proxy[1],
|
|
|
|
|
+ target_host=direct_read_url.host,
|
|
|
|
|
+ target_port=direct_read_url.port or 3306,
|
|
|
|
|
+ listen_host=settings.read_database_tunnel_host,
|
|
|
|
|
+ listen_port=settings.read_database_tunnel_port,
|
|
|
|
|
+ )
|
|
|
database = create_database_sessions(settings)
|
|
database = create_database_sessions(settings)
|
|
|
outbound_policy = OutboundPolicy(
|
|
outbound_policy = OutboundPolicy(
|
|
|
- frozenset(settings.outbound_allowed_hosts),
|
|
|
|
|
- frozenset(settings.outbound_allowed_ports),
|
|
|
|
|
|
|
+ allowed_hosts=frozenset(settings.outbound_allowed_hosts),
|
|
|
|
|
+ allowed_ports=frozenset(settings.outbound_allowed_ports),
|
|
|
|
|
+ allowed_http_hosts=frozenset(settings.outbound_allowed_http_hosts),
|
|
|
)
|
|
)
|
|
|
http_client = httpx.AsyncClient(
|
|
http_client = httpx.AsyncClient(
|
|
|
transport=runtime.http_transport,
|
|
transport=runtime.http_transport,
|
|
@@ -134,6 +174,16 @@ def compose_production_host(
|
|
|
timeout_seconds=settings.request_timeout_seconds,
|
|
timeout_seconds=settings.request_timeout_seconds,
|
|
|
max_response_bytes=settings.max_response_bytes,
|
|
max_response_bytes=settings.max_response_bytes,
|
|
|
)
|
|
)
|
|
|
|
|
+ openrouter: LegacyOpenRouterClient | None = None
|
|
|
|
|
+ if settings.openrouter_api_key is not None and settings.embedding_endpoint:
|
|
|
|
|
+ openrouter = LegacyOpenRouterClient(
|
|
|
|
|
+ safe_http,
|
|
|
|
|
+ api_key=settings.openrouter_api_key.get_secret_value(),
|
|
|
|
|
+ embedding_endpoint=settings.embedding_endpoint,
|
|
|
|
|
+ chat_endpoint=settings.openrouter_chat_endpoint,
|
|
|
|
|
+ embedding_model=settings.embedding_model,
|
|
|
|
|
+ embedding_dimension=settings.embedding_dimension,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
retrieval_adapters: dict[str, Any] = {}
|
|
retrieval_adapters: dict[str, Any] = {}
|
|
|
if settings.pattern_endpoint:
|
|
if settings.pattern_endpoint:
|
|
@@ -148,16 +198,44 @@ def compose_production_host(
|
|
|
settings.decode_endpoint,
|
|
settings.decode_endpoint,
|
|
|
safe_http,
|
|
safe_http,
|
|
|
)
|
|
)
|
|
|
|
|
+ elif vector_decode and settings.decode_raw_root is not None:
|
|
|
|
|
+ retrieval_adapters["decode"] = LegacyVectorDecodeRetrievalAdapter(
|
|
|
|
|
+ index_root=settings.decode_index_root,
|
|
|
|
|
+ raw_root=settings.decode_raw_root,
|
|
|
|
|
+ openrouter=openrouter,
|
|
|
|
|
+ min_score=settings.decode_min_score,
|
|
|
|
|
+ )
|
|
|
elif decode_path is not None:
|
|
elif decode_path is not None:
|
|
|
retrieval_adapters["decode"] = FileDecodeRetrievalAdapter(decode_path)
|
|
retrieval_adapters["decode"] = FileDecodeRetrievalAdapter(decode_path)
|
|
|
else:
|
|
else:
|
|
|
raise AssertionError("decode configuration was validated before resource creation")
|
|
raise AssertionError("decode configuration was validated before resource creation")
|
|
|
- if settings.external_endpoint:
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ settings.xhs_search_endpoint
|
|
|
|
|
+ and settings.xhs_detail_endpoint
|
|
|
|
|
+ and settings.zhihu_search_endpoint
|
|
|
|
|
+ ):
|
|
|
|
|
+ retrieval_adapters["external"] = LegacyExternalRetrievalAdapter(
|
|
|
|
|
+ xhs_search_endpoint=settings.xhs_search_endpoint,
|
|
|
|
|
+ xhs_detail_endpoint=settings.xhs_detail_endpoint,
|
|
|
|
|
+ zhihu_search_endpoint=settings.zhihu_search_endpoint,
|
|
|
|
|
+ http=safe_http,
|
|
|
|
|
+ openrouter=openrouter,
|
|
|
|
|
+ image_model=settings.external_image_model,
|
|
|
|
|
+ )
|
|
|
|
|
+ elif settings.external_endpoint:
|
|
|
retrieval_adapters["external"] = ExternalRetrievalAdapter(
|
|
retrieval_adapters["external"] = ExternalRetrievalAdapter(
|
|
|
settings.external_endpoint,
|
|
settings.external_endpoint,
|
|
|
safe_http,
|
|
safe_http,
|
|
|
)
|
|
)
|
|
|
- retrieval_adapters["knowledge"] = FileKnowledgeRetrievalAdapter(knowledge_path)
|
|
|
|
|
|
|
+ retrieval_adapters["knowledge"] = (
|
|
|
|
|
+ LegacyLlmKnowledgeRetrievalAdapter(
|
|
|
|
|
+ knowledge_path,
|
|
|
|
|
+ openrouter=openrouter,
|
|
|
|
|
+ model=settings.knowledge_model,
|
|
|
|
|
+ )
|
|
|
|
|
+ if openrouter is not None
|
|
|
|
|
+ else FileKnowledgeRetrievalAdapter(knowledge_path)
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
snapshots = SqlAlchemyInputSnapshotRepository(database.write)
|
|
snapshots = SqlAlchemyInputSnapshotRepository(database.write)
|
|
|
bindings = SqlAlchemyMissionBindingRepository(database.write)
|
|
bindings = SqlAlchemyMissionBindingRepository(database.write)
|
|
@@ -187,14 +265,17 @@ def compose_production_host(
|
|
|
knowledge_path=knowledge_path,
|
|
knowledge_path=knowledge_path,
|
|
|
)
|
|
)
|
|
|
data_root = settings.agent_data_root.resolve()
|
|
data_root = settings.agent_data_root.resolve()
|
|
|
|
|
+ fresh_local_outputs = settings.environment.lower() == "test"
|
|
|
|
|
+ output_reads = database.write if fresh_local_outputs else database.read
|
|
|
raw_artifacts = FileRawArtifactStore(data_root)
|
|
raw_artifacts = FileRawArtifactStore(data_root)
|
|
|
candidate_workspaces = SqlAlchemyCandidateWorkspaceRepository(database.write, artifacts)
|
|
candidate_workspaces = SqlAlchemyCandidateWorkspaceRepository(database.write, artifacts)
|
|
|
legacy_projection = LegacyDetailProjectionService(
|
|
legacy_projection = LegacyDetailProjectionService(
|
|
|
- database.read,
|
|
|
|
|
|
|
+ output_reads,
|
|
|
runtime.build_authorizer,
|
|
runtime.build_authorizer,
|
|
|
topic_reader=legacy_input,
|
|
topic_reader=legacy_input,
|
|
|
bindings=bindings,
|
|
bindings=bindings,
|
|
|
snapshots=snapshots,
|
|
snapshots=snapshots,
|
|
|
|
|
+ include_legacy_decision_traces=not fresh_local_outputs,
|
|
|
)
|
|
)
|
|
|
final_uow = SqlAlchemyFinalPublicationUnitOfWork(
|
|
final_uow = SqlAlchemyFinalPublicationUnitOfWork(
|
|
|
database.final,
|
|
database.final,
|
|
@@ -257,19 +338,23 @@ def compose_production_host(
|
|
|
final_publications=final_publications,
|
|
final_publications=final_publications,
|
|
|
legacy_projection=legacy_projection,
|
|
legacy_projection=legacy_projection,
|
|
|
legacy_api=LegacyScriptBuildApiService(
|
|
legacy_api=LegacyScriptBuildApiService(
|
|
|
- database.read, database.write, runtime.build_authorizer
|
|
|
|
|
|
|
+ output_reads,
|
|
|
|
|
+ database.write,
|
|
|
|
|
+ runtime.build_authorizer,
|
|
|
|
|
+ include_legacy_logs=not fresh_local_outputs,
|
|
|
),
|
|
),
|
|
|
http_command_journal=HttpCommandJournal(database.write),
|
|
http_command_journal=HttpCommandJournal(database.write),
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
host = ProductionHost(
|
|
host = ProductionHost(
|
|
|
- composition,
|
|
|
|
|
- database,
|
|
|
|
|
- http_client,
|
|
|
|
|
- runtime_manifest,
|
|
|
|
|
- trace_store,
|
|
|
|
|
- trace_store_verifier,
|
|
|
|
|
- settings.enable_image_understanding,
|
|
|
|
|
|
|
+ composition=composition,
|
|
|
|
|
+ database=database,
|
|
|
|
|
+ http_client=http_client,
|
|
|
|
|
+ runtime_manifest=runtime_manifest,
|
|
|
|
|
+ trace_store=trace_store,
|
|
|
|
|
+ trace_store_verifier=trace_store_verifier,
|
|
|
|
|
+ read_database_tunnel=read_database_tunnel,
|
|
|
|
|
+ require_trace_attachments=settings.enable_image_understanding,
|
|
|
)
|
|
)
|
|
|
composition.app.state.production_host = host
|
|
composition.app.state.production_host = host
|
|
|
composition.app.router.add_event_handler("startup", host.validate_startup)
|
|
composition.app.router.add_event_handler("startup", host.validate_startup)
|