agentenv-framework 0.9.1254__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Shared pieces for the explorer's read routers: every list endpoint is
|
|
2
|
+
``DocumentStore.latest_per_id`` + ``count_distinct`` behind one factory, so no route
|
|
3
|
+
depends on a backend query language.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from agent_env.artifact.registry import canonical_type, equivalent_types
|
|
14
|
+
from agent_env.config import get_config
|
|
15
|
+
from agent_env.store import Filter, Sort
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PaginatedResponse(BaseModel):
|
|
19
|
+
"""Pagination envelope shared by every list endpoint."""
|
|
20
|
+
|
|
21
|
+
items: list[Any]
|
|
22
|
+
total: int
|
|
23
|
+
limit: int
|
|
24
|
+
offset: int
|
|
25
|
+
has_more: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def docs():
|
|
29
|
+
return get_config().get_document_store()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _id_matches(doc: dict, id_field: str, needle: str) -> bool:
|
|
33
|
+
"""Case-insensitive substring match on the entity id."""
|
|
34
|
+
value = doc.get(id_field)
|
|
35
|
+
return isinstance(value, str) and needle.lower() in value.lower()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_UNIVERSE_FILE_TYPES = {"file_artifact_universe"}
|
|
39
|
+
_UNIVERSE_ENVIRONMENT_TYPES = {"environment_universe"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ref_id_version(ref: Any) -> tuple[Optional[str], Optional[int]]:
|
|
43
|
+
if isinstance(ref, dict):
|
|
44
|
+
return ref.get("id"), ref.get("version")
|
|
45
|
+
return (ref if isinstance(ref, str) else None), None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_doc(store, collection: str, ref: Any) -> Optional[dict]:
|
|
49
|
+
"""Fetch a referenced artifact doc by (id, version); latest version if unpinned."""
|
|
50
|
+
rid, ver = _ref_id_version(ref)
|
|
51
|
+
if rid is None:
|
|
52
|
+
return None
|
|
53
|
+
if ver is not None:
|
|
54
|
+
return store.find_one(collection, Filter.of(id=rid, version=ver))
|
|
55
|
+
found = store.latest_per_id(collection, Filter.of(id=rid))
|
|
56
|
+
return found[0] if found else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _enrich_universe(doc: dict, store, collection: str) -> dict:
|
|
60
|
+
"""Resolve a universe's artifact refs into the ``files``/``services`` arrays the UI renders.
|
|
61
|
+
|
|
62
|
+
Each file's ``object_url`` is the FileArtifact's seam location, served via /objects/content.
|
|
63
|
+
"""
|
|
64
|
+
t = doc.get("type")
|
|
65
|
+
if collection == "artifacts" and isinstance(t, str):
|
|
66
|
+
t = canonical_type(t)
|
|
67
|
+
if t in _UNIVERSE_FILE_TYPES:
|
|
68
|
+
refs = doc.get("file_artifact_refs") or doc.get("file_artifact_ids") or {}
|
|
69
|
+
files = []
|
|
70
|
+
for filename, ref in refs.items():
|
|
71
|
+
fa = _resolve_doc(store, collection, ref)
|
|
72
|
+
rid, ver = _ref_id_version(ref)
|
|
73
|
+
files.append({
|
|
74
|
+
"filename": filename,
|
|
75
|
+
"artifact_id": rid,
|
|
76
|
+
"version": ver,
|
|
77
|
+
"content_type": fa.get("content_type") if fa else None,
|
|
78
|
+
"object_url": fa.get("s3_url") if fa else None,
|
|
79
|
+
})
|
|
80
|
+
doc["files"] = files
|
|
81
|
+
elif t in _UNIVERSE_ENVIRONMENT_TYPES:
|
|
82
|
+
refs = doc.get("service_artifact_refs")
|
|
83
|
+
if not refs:
|
|
84
|
+
refs = [{"id": sid} for sid in (doc.get("service_artifact_ids") or [])]
|
|
85
|
+
services = []
|
|
86
|
+
for ref in refs:
|
|
87
|
+
sa = _resolve_doc(store, collection, ref)
|
|
88
|
+
rid, ver = _ref_id_version(ref)
|
|
89
|
+
services.append({
|
|
90
|
+
"service_name": (sa.get("service_name") if sa else None) or rid,
|
|
91
|
+
"artifact_id": rid,
|
|
92
|
+
"version": ver,
|
|
93
|
+
})
|
|
94
|
+
doc["services"] = services
|
|
95
|
+
return doc
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def versioned_router(
|
|
99
|
+
*,
|
|
100
|
+
prefix: str,
|
|
101
|
+
tag: str,
|
|
102
|
+
collection: str,
|
|
103
|
+
id_field: str = "id",
|
|
104
|
+
) -> APIRouter:
|
|
105
|
+
"""List / get / versions over one ``(id, version)`` collection (artifacts, envs, tasks,
|
|
106
|
+
agents, evals)."""
|
|
107
|
+
router = APIRouter(prefix=prefix, tags=[tag])
|
|
108
|
+
|
|
109
|
+
@router.get("", response_model=PaginatedResponse)
|
|
110
|
+
def list_items(
|
|
111
|
+
limit: int = Query(50, ge=1, le=500),
|
|
112
|
+
offset: int = Query(0, ge=0),
|
|
113
|
+
type: Optional[list[str]] = Query(None, description="Filter by type; repeat to union."),
|
|
114
|
+
id: Optional[str] = Query(None, description="Substring match on the entity id."),
|
|
115
|
+
sort_by: str = Query("created_at_utc"),
|
|
116
|
+
descending: bool = Query(True),
|
|
117
|
+
) -> PaginatedResponse:
|
|
118
|
+
store = docs()
|
|
119
|
+
want_types = set(type) if type else None
|
|
120
|
+
if want_types is not None and collection == "artifacts":
|
|
121
|
+
want_types = {s for t in want_types for s in equivalent_types(t)}
|
|
122
|
+
if id or want_types is not None:
|
|
123
|
+
# Reduce to the latest version per id FIRST, then filter (by id substring
|
|
124
|
+
# and/or type) in Python. Filtering before the reduction could select a
|
|
125
|
+
# superseded version whose type still matches and report it as the live
|
|
126
|
+
# entity; reducing first means the type filter only ever sees live rows.
|
|
127
|
+
reduced = store.latest_per_id(
|
|
128
|
+
collection, Filter({}), id_field=id_field,
|
|
129
|
+
sort=Sort.by(sort_by, descending=descending),
|
|
130
|
+
)
|
|
131
|
+
matched = [
|
|
132
|
+
d for d in reduced
|
|
133
|
+
if (not id or _id_matches(d, id_field, id))
|
|
134
|
+
and (want_types is None or str(d.get("type")) in want_types)
|
|
135
|
+
]
|
|
136
|
+
total = len(matched)
|
|
137
|
+
items = matched[offset: offset + limit] if limit else matched[offset:]
|
|
138
|
+
else:
|
|
139
|
+
items = store.latest_per_id(
|
|
140
|
+
collection, Filter({}), id_field=id_field,
|
|
141
|
+
sort=Sort.by(sort_by, descending=descending), limit=limit, offset=offset,
|
|
142
|
+
)
|
|
143
|
+
# Count distinct ids, not rows — one entity (N versions) is one result.
|
|
144
|
+
total = store.count_distinct(collection, Filter({}), id_field=id_field)
|
|
145
|
+
return PaginatedResponse(
|
|
146
|
+
items=items, total=total, limit=limit, offset=offset,
|
|
147
|
+
has_more=offset + len(items) < total,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@router.get("/{entity_id}")
|
|
151
|
+
def get_item(entity_id: str, version: Optional[int] = None) -> dict:
|
|
152
|
+
store = docs()
|
|
153
|
+
if version is not None:
|
|
154
|
+
doc = store.find_one(collection, Filter.of(**{id_field: entity_id, "version": version}))
|
|
155
|
+
else:
|
|
156
|
+
found = store.latest_per_id(collection, Filter.of(**{id_field: entity_id}), id_field=id_field)
|
|
157
|
+
doc = found[0] if found else None
|
|
158
|
+
if doc is None:
|
|
159
|
+
raise HTTPException(status_code=404, detail=f"{tag} {entity_id} not found")
|
|
160
|
+
return _enrich_universe(doc, store, collection)
|
|
161
|
+
|
|
162
|
+
@router.get("/{entity_id}/versions")
|
|
163
|
+
def list_versions(
|
|
164
|
+
entity_id: str,
|
|
165
|
+
limit: int = Query(100, ge=1, le=500),
|
|
166
|
+
offset: int = Query(0, ge=0),
|
|
167
|
+
) -> list[dict]:
|
|
168
|
+
"""Every stored version of one entity, newest first. Returns a bare array (not a
|
|
169
|
+
PaginatedResponse) — clients expect a list here."""
|
|
170
|
+
filt = Filter.of(**{id_field: entity_id})
|
|
171
|
+
return docs().query(collection, filt, sort=Sort.by("version", descending=True),
|
|
172
|
+
limit=limit, offset=offset)
|
|
173
|
+
|
|
174
|
+
return router
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Read-only A2A conversation transcripts, per task instance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
from agent_env.explorer.routers.common import docs
|
|
8
|
+
from agent_env.store import Filter, Sort
|
|
9
|
+
|
|
10
|
+
# Prefix hardcoded (not imported from app) to avoid a router->app import cycle.
|
|
11
|
+
router = APIRouter(prefix="/api/v1", tags=["conversations"])
|
|
12
|
+
|
|
13
|
+
# Written by agent_env.a2a_agent.conversation_store.
|
|
14
|
+
_CONVERSATIONS_COLLECTION = "agent_env_a2a_conversations"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/task-instances/{task_instance_id}/conversations")
|
|
18
|
+
def list_conversations(task_instance_id: str) -> dict:
|
|
19
|
+
"""A2A conversations recorded for a task instance, oldest first."""
|
|
20
|
+
conversations = docs().query(
|
|
21
|
+
_CONVERSATIONS_COLLECTION,
|
|
22
|
+
Filter.of(task_instance_id=task_instance_id),
|
|
23
|
+
sort=Sort.by("created_at_utc", descending=False), # oldest first
|
|
24
|
+
)
|
|
25
|
+
return {"conversations": conversations}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Object-store byte seam: stream any object-store blob to the browser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
|
|
13
|
+
from agent_env.config import get_config
|
|
14
|
+
|
|
15
|
+
# Prefix hardcoded (not imported from app) to avoid a router->app import cycle.
|
|
16
|
+
router = APIRouter(prefix="/api/v1/objects", tags=["objects"])
|
|
17
|
+
|
|
18
|
+
_CONTENT_STREAM_CHUNK = 64 * 1024
|
|
19
|
+
|
|
20
|
+
# Types a browser runs as a document — served under CSP sandbox so a stored
|
|
21
|
+
# HTML/SVG artifact can't execute script on the explorer origin.
|
|
22
|
+
_SANDBOX_TYPES = frozenset({
|
|
23
|
+
"text/html", "application/xhtml+xml", "image/svg+xml",
|
|
24
|
+
"application/xml", "text/xml", "application/xslt+xml",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _owned_store(object_url: str):
|
|
29
|
+
"""The store serving ``object_url``, which must be one of its objects: the explorer reads
|
|
30
|
+
nothing outside the configured store."""
|
|
31
|
+
store = get_config().get_object_store_at(object_url)
|
|
32
|
+
if not store.owns(object_url):
|
|
33
|
+
raise HTTPException(status_code=400, detail="object_url is not in the configured object store")
|
|
34
|
+
if not store.get_object_key(object_url):
|
|
35
|
+
raise HTTPException(status_code=400, detail="object_url names a bucket, not an object")
|
|
36
|
+
return store
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _content_disposition(filename: str) -> str:
|
|
40
|
+
"""``inline`` with the name for the browser to save under. Header values are Latin-1, so a
|
|
41
|
+
name that is not plain ASCII goes in RFC 6266's ``filename*`` beside an ASCII fallback."""
|
|
42
|
+
fallback = "".join(c if " " <= c <= "~" and c not in '"\\' else "_" for c in filename)
|
|
43
|
+
if fallback == filename:
|
|
44
|
+
return f'inline; filename="{filename}"'
|
|
45
|
+
return f"inline; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename, safe='')}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.get("/content")
|
|
49
|
+
def object_content(object_url: str = Query(...)):
|
|
50
|
+
"""Proxy an object-store blob to the browser, resolved through the configured store."""
|
|
51
|
+
store = _owned_store(object_url)
|
|
52
|
+
meta = store.get_object_metadata_at(object_url)
|
|
53
|
+
if meta is None:
|
|
54
|
+
raise HTTPException(status_code=404, detail="no object at the given object_url")
|
|
55
|
+
|
|
56
|
+
# Stream from a temp file to bound memory on large blobs.
|
|
57
|
+
fd, tmp = tempfile.mkstemp(prefix="agentenv-content-")
|
|
58
|
+
os.close(fd)
|
|
59
|
+
try:
|
|
60
|
+
try:
|
|
61
|
+
store.download_to_file(object_url, tmp)
|
|
62
|
+
except Exception:
|
|
63
|
+
# Object existed at the metadata check above, so a read failure here is an
|
|
64
|
+
# upstream store error (permission/timeout/outage), not a missing object.
|
|
65
|
+
raise HTTPException(status_code=502, detail="object store failed to read the object")
|
|
66
|
+
response = _streamed(tmp, object_url, meta)
|
|
67
|
+
except BaseException:
|
|
68
|
+
# The response owns the file once it exists; until then nothing else will remove it.
|
|
69
|
+
os.unlink(tmp)
|
|
70
|
+
raise
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _streamed(tmp: str, object_url: str, meta) -> StreamingResponse:
|
|
75
|
+
def _stream_and_cleanup():
|
|
76
|
+
try:
|
|
77
|
+
with open(tmp, "rb") as fh:
|
|
78
|
+
while chunk := fh.read(_CONTENT_STREAM_CHUNK):
|
|
79
|
+
yield chunk
|
|
80
|
+
finally:
|
|
81
|
+
os.unlink(tmp)
|
|
82
|
+
|
|
83
|
+
filename = object_url.rstrip("/").rsplit("/", 1)[-1] or "object"
|
|
84
|
+
# Local stores don't persist a content type; fall back to the filename so both the
|
|
85
|
+
# media type and the sandbox decision below are correct on every backend.
|
|
86
|
+
content_type = meta.content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
87
|
+
headers = {
|
|
88
|
+
"X-Content-Type-Options": "nosniff", # untrusted artifact bytes must not sniff-execute
|
|
89
|
+
"Content-Disposition": _content_disposition(filename),
|
|
90
|
+
}
|
|
91
|
+
if content_type.split(";")[0].strip().lower() in _SANDBOX_TYPES:
|
|
92
|
+
headers["Content-Security-Policy"] = "sandbox"
|
|
93
|
+
# Reads return the bytes as stored, so the browser needs the encoding to decode them.
|
|
94
|
+
if meta.content_encoding:
|
|
95
|
+
headers["Content-Encoding"] = meta.content_encoding
|
|
96
|
+
if meta.size is not None:
|
|
97
|
+
headers["Content-Length"] = str(meta.size)
|
|
98
|
+
return StreamingResponse(
|
|
99
|
+
_stream_and_cleanup(),
|
|
100
|
+
media_type=content_type,
|
|
101
|
+
headers=headers,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.get("/metadata")
|
|
106
|
+
def object_metadata(object_url: str = Query(...)):
|
|
107
|
+
"""Size + content-type for an object, without transferring the bytes."""
|
|
108
|
+
store = _owned_store(object_url)
|
|
109
|
+
meta = store.get_object_metadata_at(object_url)
|
|
110
|
+
if meta is None:
|
|
111
|
+
raise HTTPException(status_code=404, detail="no object at the given object_url")
|
|
112
|
+
return {"size_bytes": meta.size, "content_type": meta.content_type}
|