apsimo 1.3.0__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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Example feed spec — everything a feed instance needs, in one file.
|
|
2
|
+
# Create with: colony feeds create example-feed.yaml
|
|
3
|
+
# Only name/topic/destination/cadence + one source adapter are required.
|
|
4
|
+
|
|
5
|
+
name: rust-embedded
|
|
6
|
+
title: Rust Embedded Feed
|
|
7
|
+
topic: >-
|
|
8
|
+
Rust on microcontrollers and embedded targets: new HAL crates, RTIC and
|
|
9
|
+
Embassy releases, async-on-bare-metal techniques, and real hardware
|
|
10
|
+
benchmarks. The goal is catching tools and techniques worth adopting.
|
|
11
|
+
|
|
12
|
+
audience: personal # personal | group (group adds community-input handling)
|
|
13
|
+
|
|
14
|
+
destination:
|
|
15
|
+
kind: deliver # deliver | command | file
|
|
16
|
+
deliver: origin # harness delivery target: origin, platform:chat_id, ...
|
|
17
|
+
# kind: command # pipe the brief to a shell command instead:
|
|
18
|
+
# send_command: "python3 ~/.hermes/scripts/my-group-send.py 42"
|
|
19
|
+
# read_back: 'read_chat chat="My Group" limit=100' # group community input
|
|
20
|
+
# owner_deliver: origin # where discovery proposals go (defaults to deliver)
|
|
21
|
+
|
|
22
|
+
# llm: # optional but recommended: pin so global provider
|
|
23
|
+
# provider: my-provider # changes can never drift-skip these jobs
|
|
24
|
+
# model: my/model
|
|
25
|
+
|
|
26
|
+
cadence:
|
|
27
|
+
collect: "every 240m"
|
|
28
|
+
distill: "every 120m"
|
|
29
|
+
# digest: "0 8 * * *" # optional daily rollup
|
|
30
|
+
# alerts: "every 60m" # optional between-brief P0 pushes
|
|
31
|
+
# discovery: "every 480m" # optional new-source research agent
|
|
32
|
+
|
|
33
|
+
sources:
|
|
34
|
+
x_searches:
|
|
35
|
+
- '(embassy OR RTIC OR "embedded-hal") rust'
|
|
36
|
+
key_accounts: [rustembedded]
|
|
37
|
+
github_search: "embedded rust hal OR rtic OR embassy"
|
|
38
|
+
github_keywords: [embedded, hal, rtic, embassy, no_std]
|
|
39
|
+
hf_keywords: [] # empty = adapter disabled
|
|
40
|
+
forum_urls: [] # Discourse forums (fetched as <url>.json)
|
|
41
|
+
forum_title_keywords: []
|
|
42
|
+
arxiv:
|
|
43
|
+
categories: [cs.SE]
|
|
44
|
+
query_terms: [embedded, "real-time"]
|
|
45
|
+
keywords: [embedded, microcontroller]
|
|
46
|
+
rss:
|
|
47
|
+
- {name: rust-blog, url: "https://blog.rust-lang.org/feed.xml"}
|
|
48
|
+
|
|
49
|
+
registry: # tiered source registry used by promotion rules
|
|
50
|
+
- name: rust-embedded-wg
|
|
51
|
+
handle: "@rustembedded"
|
|
52
|
+
category: working_group
|
|
53
|
+
weight: P0
|
|
54
|
+
reason: Official embedded WG — every announcement matters.
|
|
55
|
+
platforms: [x]
|
|
56
|
+
|
|
57
|
+
scoring:
|
|
58
|
+
keyword_categories: # generic keyword->points buckets
|
|
59
|
+
- {keywords: [rtic, embassy, embedded-hal, no_std], points: 15}
|
|
60
|
+
- {keywords: [benchmark, latency, interrupt], points: 10}
|
|
61
|
+
thresholds: {p0: 40, p1: 25, p2: 15}
|
|
62
|
+
|
|
63
|
+
brief:
|
|
64
|
+
framing: "What changes for embedded Rust projects because of this?"
|
|
65
|
+
standard_cap: 5
|
|
66
|
+
# context_files: # extra files the distill agent reads if present
|
|
67
|
+
# - {path: ~/.hermes/data/my-extra-context.json, label: crate download stats}
|
|
68
|
+
# extra_instructions: "" # freeform text appended to the distill prompt
|
|
69
|
+
|
|
70
|
+
privacy:
|
|
71
|
+
forbidden:
|
|
72
|
+
- "private infrastructure details (hardware counts, cluster topology, hostnames, addresses)"
|
|
73
|
+
- "names of the operator, their contacts, or internal agent/system codenames"
|
|
74
|
+
# aliases: [DGX Spark] # allowed public product names
|
|
75
|
+
# note: "" # extra privacy instruction
|
|
76
|
+
|
|
77
|
+
learning_loop: true # log unknown-source promotions for registry evolution
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Harness (Hermes) cron integration for feed instances.
|
|
2
|
+
|
|
3
|
+
All scheduling goes through the ``hermes cron`` CLI; the one thing the CLI
|
|
4
|
+
cannot do — pinning a job to an explicit provider/model so global inference
|
|
5
|
+
config drift can never silently skip a feed job — is done by editing the
|
|
6
|
+
scheduler's jobs.json directly (with a timestamped backup first).
|
|
7
|
+
|
|
8
|
+
Paths/binaries are overridable via env for non-standard installs:
|
|
9
|
+
COLONY_HERMES_BIN hermes CLI (default: ``hermes`` on PATH, then the
|
|
10
|
+
conventional venv location)
|
|
11
|
+
COLONY_HERMES_HOME harness home (default ~/.hermes)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def hermes_home() -> str:
|
|
24
|
+
return os.path.expanduser(os.environ.get("COLONY_HERMES_HOME", "~/.hermes"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def hermes_bin() -> str:
|
|
28
|
+
env = os.environ.get("COLONY_HERMES_BIN")
|
|
29
|
+
if env:
|
|
30
|
+
return os.path.expanduser(env)
|
|
31
|
+
on_path = shutil.which("hermes")
|
|
32
|
+
if on_path:
|
|
33
|
+
return on_path
|
|
34
|
+
conventional = os.path.join(hermes_home(), "hermes-agent/venv/bin/hermes")
|
|
35
|
+
return conventional if os.path.exists(conventional) else "hermes"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def jobs_json_path() -> str:
|
|
39
|
+
return os.path.join(hermes_home(), "cron/jobs.json")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def scripts_dir() -> str:
|
|
43
|
+
return os.path.join(hermes_home(), "scripts")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run(args: list[str], timeout: int = 60) -> str:
|
|
47
|
+
proc = subprocess.run([hermes_bin(), *args], capture_output=True, text=True,
|
|
48
|
+
timeout=timeout)
|
|
49
|
+
if proc.returncode != 0:
|
|
50
|
+
raise RuntimeError(f"hermes {' '.join(args[:2])} failed: "
|
|
51
|
+
f"{proc.stderr.strip() or proc.stdout.strip()}")
|
|
52
|
+
return proc.stdout
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_jobs() -> list[dict]:
|
|
56
|
+
try:
|
|
57
|
+
with open(jobs_json_path(), encoding="utf-8") as f:
|
|
58
|
+
data = json.load(f)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
return []
|
|
61
|
+
return data.get("jobs", data) if isinstance(data, dict) else data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_jobs(name_prefix: str) -> list[dict]:
|
|
65
|
+
return [j for j in _load_jobs()
|
|
66
|
+
if isinstance(j, dict) and (j.get("name") or "").startswith(name_prefix)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_job(name: str, schedule: str, *, prompt: str | None = None,
|
|
70
|
+
script: str | None = None, no_agent: bool = False,
|
|
71
|
+
deliver: str = "local") -> str:
|
|
72
|
+
"""Create a job and return its id (resolved by unique name lookup)."""
|
|
73
|
+
if find_jobs(name):
|
|
74
|
+
raise RuntimeError(f"a cron job named {name!r} already exists")
|
|
75
|
+
args = ["cron", "create", schedule, "--name", name, "--deliver", deliver]
|
|
76
|
+
if script:
|
|
77
|
+
args += ["--script", script]
|
|
78
|
+
if no_agent:
|
|
79
|
+
args += ["--no-agent"]
|
|
80
|
+
if prompt is not None:
|
|
81
|
+
args.insert(3, prompt) # positional prompt right after the schedule
|
|
82
|
+
_run(args)
|
|
83
|
+
jobs = find_jobs(name)
|
|
84
|
+
if len(jobs) != 1:
|
|
85
|
+
raise RuntimeError(f"created job {name!r} but found {len(jobs)} matches in jobs.json")
|
|
86
|
+
return jobs[0]["id"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def pin_model(job_ids: list[str], provider: str, model: str) -> None:
|
|
90
|
+
"""Pin jobs to an explicit provider/model in jobs.json (backup first)."""
|
|
91
|
+
if not (provider and model):
|
|
92
|
+
return
|
|
93
|
+
path = jobs_json_path()
|
|
94
|
+
shutil.copy2(path, f"{path}.bak-feeds-{int(time.time())}")
|
|
95
|
+
with open(path, encoding="utf-8") as f:
|
|
96
|
+
data = json.load(f)
|
|
97
|
+
jobs = data.get("jobs", data) if isinstance(data, dict) else data
|
|
98
|
+
hit = 0
|
|
99
|
+
for j in jobs:
|
|
100
|
+
if isinstance(j, dict) and j.get("id") in job_ids:
|
|
101
|
+
j["provider"] = provider
|
|
102
|
+
j["model"] = model
|
|
103
|
+
hit += 1
|
|
104
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
105
|
+
json.dump(data, f, indent=2)
|
|
106
|
+
if hit != len(job_ids):
|
|
107
|
+
raise RuntimeError(f"pinned {hit}/{len(job_ids)} jobs — jobs.json out of sync")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def remove_job(job_id: str) -> None:
|
|
111
|
+
_run(["cron", "remove", job_id])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def pause_job(job_id: str) -> None:
|
|
115
|
+
_run(["cron", "pause", job_id])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resume_job(job_id: str) -> None:
|
|
119
|
+
_run(["cron", "resume", job_id])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run_job_detached(job_id: str) -> None:
|
|
123
|
+
"""Trigger a run without blocking: agent jobs can run for many minutes."""
|
|
124
|
+
subprocess.Popen([hermes_bin(), "cron", "run", job_id],
|
|
125
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
126
|
+
start_new_session=True)
|
apsimo/feeds/manager.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Feed instance lifecycle: create, list, status, pause/resume, run, delete.
|
|
2
|
+
|
|
3
|
+
Everything a feed needs is derived from its spec at create time:
|
|
4
|
+
|
|
5
|
+
data dir <data_root>/{spec.yaml,instance.json,config.json,queue.json,seen.db,briefs/}
|
|
6
|
+
engine one shared copy of engine.py in the harness scripts dir
|
|
7
|
+
shims tiny generated per-instance scripts that point the engine at
|
|
8
|
+
this instance's config (the scheduler's --script mechanism has
|
|
9
|
+
no argument passing, so the shim IS the argument)
|
|
10
|
+
cron jobs "[feeds:<name>] <stage>" jobs, agent jobs pinned to the spec's
|
|
11
|
+
provider/model so global config drift can never skip them
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
from . import hermes_cron as hc
|
|
24
|
+
from . import template
|
|
25
|
+
from .spec import FeedSpec
|
|
26
|
+
|
|
27
|
+
ENGINE_BASENAME = "colony-feed-engine.py"
|
|
28
|
+
JOB_PREFIX = "[feeds:{name}]"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _prefix(name: str) -> str:
|
|
32
|
+
return JOB_PREFIX.format(name=name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _instance_path(name: str) -> str:
|
|
36
|
+
return os.path.expanduser(f"~/.hermes/data/feeds/{name}/instance.json")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_instance(name: str) -> dict:
|
|
40
|
+
try:
|
|
41
|
+
with open(_instance_path(name), encoding="utf-8") as f:
|
|
42
|
+
return json.load(f)
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
raise SystemExit(f"no feed instance named {name!r} (see: colony feeds list)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _engine_src() -> str:
|
|
48
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "engine.py")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _install_engine() -> str:
|
|
52
|
+
dst = os.path.join(hc.scripts_dir(), ENGINE_BASENAME)
|
|
53
|
+
os.makedirs(hc.scripts_dir(), exist_ok=True)
|
|
54
|
+
shutil.copy2(_engine_src(), dst)
|
|
55
|
+
return dst
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write_shim(name: str, stage: str, config_path: str) -> str:
|
|
59
|
+
shim = os.path.join(hc.scripts_dir(), f"feeds-{name}-{stage}.py")
|
|
60
|
+
with open(shim, "w", encoding="utf-8") as f:
|
|
61
|
+
f.write(f'''#!/usr/bin/env python3
|
|
62
|
+
# Auto-generated by `colony feeds` for instance {name!r} — do not edit.
|
|
63
|
+
import os, runpy
|
|
64
|
+
os.environ.setdefault("COLONY_FEED_CONFIG", {config_path!r})
|
|
65
|
+
os.environ.setdefault("COLONY_FEED_STAGE", {stage!r})
|
|
66
|
+
runpy.run_path(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
67
|
+
{ENGINE_BASENAME!r}), run_name="__main__")
|
|
68
|
+
''')
|
|
69
|
+
return shim
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------------------------- create
|
|
73
|
+
def create(spec_path: str, dry_run: bool = False) -> dict:
|
|
74
|
+
spec = FeedSpec.load(spec_path)
|
|
75
|
+
root = spec.data_root()
|
|
76
|
+
if os.path.exists(_instance_path(spec.name)):
|
|
77
|
+
raise SystemExit(f"feed {spec.name!r} already exists — delete it first or pick a new name")
|
|
78
|
+
|
|
79
|
+
prompts = {"distill": template.render_distill(spec)}
|
|
80
|
+
if spec.cadence.get("digest"):
|
|
81
|
+
prompts["digest"] = template.render_digest(spec)
|
|
82
|
+
if spec.cadence.get("discovery"):
|
|
83
|
+
prompts["discovery"] = template.render_discovery(spec)
|
|
84
|
+
if dry_run:
|
|
85
|
+
return {"name": spec.name, "root": root, "prompts": prompts}
|
|
86
|
+
|
|
87
|
+
os.makedirs(spec.briefs_dir, exist_ok=True)
|
|
88
|
+
os.makedirs(root, exist_ok=True)
|
|
89
|
+
shutil.copy2(os.path.expanduser(spec_path), os.path.join(root, "spec" + os.path.splitext(spec_path)[1]))
|
|
90
|
+
|
|
91
|
+
# Engine config: an explicit storage.config_path means an external
|
|
92
|
+
# (possibly hand-maintained / mutated-by-other-tools) config owns the
|
|
93
|
+
# collector; otherwise render one from the spec.
|
|
94
|
+
config_path = spec.storage.get("config_path")
|
|
95
|
+
if config_path:
|
|
96
|
+
config_path = os.path.expanduser(config_path)
|
|
97
|
+
if not os.path.exists(config_path):
|
|
98
|
+
raise SystemExit(f"storage.config_path {config_path} does not exist")
|
|
99
|
+
else:
|
|
100
|
+
config_path = os.path.join(root, "config.json")
|
|
101
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
102
|
+
json.dump(spec.engine_config(), f, indent=2)
|
|
103
|
+
|
|
104
|
+
_install_engine()
|
|
105
|
+
shims = {"collect": _write_shim(spec.name, "collect", config_path)}
|
|
106
|
+
if spec.cadence.get("alerts"):
|
|
107
|
+
shims["alert"] = _write_shim(spec.name, "alert", config_path)
|
|
108
|
+
|
|
109
|
+
deliver_agent = (spec.destination.get("deliver", "local")
|
|
110
|
+
if spec.destination.get("kind") == "deliver" else "local")
|
|
111
|
+
owner_deliver = spec.destination.get("owner_deliver", deliver_agent)
|
|
112
|
+
|
|
113
|
+
jobs: dict[str, str] = {}
|
|
114
|
+
try:
|
|
115
|
+
jobs["collect"] = hc.create_job(
|
|
116
|
+
f"{_prefix(spec.name)} collect", spec.cadence["collect"],
|
|
117
|
+
script=os.path.basename(shims["collect"]), no_agent=True)
|
|
118
|
+
jobs["distill"] = hc.create_job(
|
|
119
|
+
f"{_prefix(spec.name)} distill", spec.cadence["distill"],
|
|
120
|
+
prompt=prompts["distill"], deliver=deliver_agent)
|
|
121
|
+
if spec.cadence.get("digest"):
|
|
122
|
+
jobs["digest"] = hc.create_job(
|
|
123
|
+
f"{_prefix(spec.name)} digest", spec.cadence["digest"],
|
|
124
|
+
prompt=prompts["digest"], deliver=deliver_agent)
|
|
125
|
+
if spec.cadence.get("alerts"):
|
|
126
|
+
jobs["alert"] = hc.create_job(
|
|
127
|
+
f"{_prefix(spec.name)} alert", spec.cadence["alerts"],
|
|
128
|
+
script=os.path.basename(shims["alert"]), no_agent=True,
|
|
129
|
+
deliver=deliver_agent)
|
|
130
|
+
if spec.cadence.get("discovery"):
|
|
131
|
+
jobs["discovery"] = hc.create_job(
|
|
132
|
+
f"{_prefix(spec.name)} discovery", spec.cadence["discovery"],
|
|
133
|
+
prompt=prompts["discovery"], deliver=owner_deliver)
|
|
134
|
+
except Exception:
|
|
135
|
+
for jid in jobs.values(): # roll back partial registration
|
|
136
|
+
try:
|
|
137
|
+
hc.remove_job(jid)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
agent_jobs = [jid for stage, jid in jobs.items()
|
|
143
|
+
if stage in ("distill", "digest", "discovery")]
|
|
144
|
+
hc.pin_model(agent_jobs, spec.llm.get("provider", ""), spec.llm.get("model", ""))
|
|
145
|
+
|
|
146
|
+
instance = {"name": spec.name, "title": spec.title, "created_at": int(time.time()),
|
|
147
|
+
"spec_path": os.path.abspath(os.path.expanduser(spec_path)),
|
|
148
|
+
"config_path": config_path, "jobs": jobs, "shims": shims,
|
|
149
|
+
"paused": False}
|
|
150
|
+
with open(_instance_path(spec.name), "w", encoding="utf-8") as f:
|
|
151
|
+
json.dump(instance, f, indent=2)
|
|
152
|
+
return instance
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ----------------------------------------------------------------- lifecycle
|
|
156
|
+
def list_instances() -> list[dict]:
|
|
157
|
+
base = os.path.expanduser("~/.hermes/data/feeds")
|
|
158
|
+
out = []
|
|
159
|
+
if not os.path.isdir(base):
|
|
160
|
+
return out
|
|
161
|
+
for name in sorted(os.listdir(base)):
|
|
162
|
+
p = _instance_path(name)
|
|
163
|
+
if os.path.exists(p):
|
|
164
|
+
with open(p, encoding="utf-8") as f:
|
|
165
|
+
out.append(json.load(f))
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def status(name: str) -> dict:
|
|
170
|
+
inst = _load_instance(name)
|
|
171
|
+
spec = FeedSpec.load(inst["spec_path"]) if os.path.exists(inst["spec_path"]) else None
|
|
172
|
+
info = dict(inst)
|
|
173
|
+
if spec and os.path.exists(spec.queue_path):
|
|
174
|
+
with open(spec.queue_path, encoding="utf-8") as f:
|
|
175
|
+
q = json.load(f)
|
|
176
|
+
info["queue"] = {"collected_at": q.get("collected_at"),
|
|
177
|
+
"total_items": q.get("total_items")}
|
|
178
|
+
if spec and os.path.isdir(spec.briefs_dir):
|
|
179
|
+
briefs = sorted(os.listdir(spec.briefs_dir))
|
|
180
|
+
info["latest_brief"] = briefs[-1] if briefs else None
|
|
181
|
+
live = {j["id"]: j for j in hc.find_jobs(_prefix(name))}
|
|
182
|
+
info["jobs_live"] = {stage: (jid in live) for stage, jid in inst["jobs"].items()}
|
|
183
|
+
return info
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def pause(name: str) -> None:
|
|
187
|
+
inst = _load_instance(name)
|
|
188
|
+
for jid in inst["jobs"].values():
|
|
189
|
+
hc.pause_job(jid)
|
|
190
|
+
inst["paused"] = True
|
|
191
|
+
with open(_instance_path(name), "w", encoding="utf-8") as f:
|
|
192
|
+
json.dump(inst, f, indent=2)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def resume(name: str) -> None:
|
|
196
|
+
inst = _load_instance(name)
|
|
197
|
+
for jid in inst["jobs"].values():
|
|
198
|
+
hc.resume_job(jid)
|
|
199
|
+
inst["paused"] = False
|
|
200
|
+
with open(_instance_path(name), "w", encoding="utf-8") as f:
|
|
201
|
+
json.dump(inst, f, indent=2)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def run(name: str, stage: str) -> None:
|
|
205
|
+
"""Run one stage now. collect/alert run synchronously (output streams);
|
|
206
|
+
agent stages are triggered detached because they can run for minutes."""
|
|
207
|
+
inst = _load_instance(name)
|
|
208
|
+
if stage in ("collect", "alert"):
|
|
209
|
+
shim = inst["shims"].get(stage) or inst["shims"]["collect"]
|
|
210
|
+
env = dict(os.environ, COLONY_FEED_STAGE=stage)
|
|
211
|
+
subprocess.run([sys.executable, shim], env=env, check=True)
|
|
212
|
+
return
|
|
213
|
+
jid = inst["jobs"].get(stage)
|
|
214
|
+
if not jid:
|
|
215
|
+
raise SystemExit(f"feed {name!r} has no {stage!r} job")
|
|
216
|
+
hc.run_job_detached(jid)
|
|
217
|
+
print(f"triggered {stage} (job {jid}) — check the scheduler's output dir for results")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def delete(name: str, purge: bool = False) -> None:
|
|
221
|
+
inst = _load_instance(name)
|
|
222
|
+
for jid in inst["jobs"].values():
|
|
223
|
+
try:
|
|
224
|
+
hc.remove_job(jid)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
print(f"warning: could not remove job {jid}: {e}", file=sys.stderr)
|
|
227
|
+
for shim in inst.get("shims", {}).values():
|
|
228
|
+
try:
|
|
229
|
+
os.remove(shim)
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
pass
|
|
232
|
+
if purge:
|
|
233
|
+
shutil.rmtree(os.path.dirname(_instance_path(name)), ignore_errors=True)
|
|
234
|
+
else:
|
|
235
|
+
os.remove(_instance_path(name))
|
apsimo/feeds/spec.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Feed instance spec: schema, defaults, validation.
|
|
2
|
+
|
|
3
|
+
A spec is a plain YAML (or JSON) mapping. Everything that distinguishes one
|
|
4
|
+
feed from another lives here — topic, sources, scoring, cadence, destination,
|
|
5
|
+
privacy — so an instance is fully reproducible from its spec file.
|
|
6
|
+
|
|
7
|
+
Minimal spec::
|
|
8
|
+
|
|
9
|
+
name: rust-embedded
|
|
10
|
+
title: Rust Embedded Feed
|
|
11
|
+
topic: Rust on microcontrollers — new crates, HALs, RTIC releases.
|
|
12
|
+
destination: {kind: deliver, deliver: origin}
|
|
13
|
+
cadence: {collect: "every 240m", distill: "every 120m"}
|
|
14
|
+
sources:
|
|
15
|
+
github_search: "embedded rust hal OR rtic"
|
|
16
|
+
arxiv: {categories: [cs.SE], keywords: [embedded]}
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,63}$")
|
|
28
|
+
|
|
29
|
+
DESTINATION_KINDS = ("command", "deliver", "file")
|
|
30
|
+
|
|
31
|
+
#: Adapter keys under ``sources:`` — at least one must be configured.
|
|
32
|
+
ADAPTER_KEYS = ("x_searches", "hf_keywords", "github_search", "forum_urls", "arxiv", "rss")
|
|
33
|
+
|
|
34
|
+
DEFAULT_FRAMING = "What changes for us because of this?"
|
|
35
|
+
|
|
36
|
+
DEFAULT_SCORING = {
|
|
37
|
+
"account_whitelist": 10,
|
|
38
|
+
"engagement": {"per_100_likes": 1, "per_1000_likes": 2, "max": 10},
|
|
39
|
+
"human_curated": 20,
|
|
40
|
+
"benchmark_numbers": 15,
|
|
41
|
+
"code_release": 10,
|
|
42
|
+
"open_weights": 10,
|
|
43
|
+
"thresholds": {"p0": 50, "p1": 35, "p2": 20},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
DEFAULT_PRIVACY_FORBIDDEN = [
|
|
47
|
+
"private infrastructure details (hardware counts, cluster topology, hostnames, addresses)",
|
|
48
|
+
"names of the operator, their contacts, or internal agent/system codenames",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SpecError(ValueError):
|
|
53
|
+
"""Raised when a spec fails validation; ``.errors`` lists every problem."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, errors: list[str]):
|
|
56
|
+
self.errors = errors
|
|
57
|
+
super().__init__("invalid feed spec:\n - " + "\n - ".join(errors))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class FeedSpec:
|
|
62
|
+
"""Validated, defaulted view over a raw spec mapping."""
|
|
63
|
+
|
|
64
|
+
raw: dict = field(repr=False)
|
|
65
|
+
name: str = ""
|
|
66
|
+
title: str = ""
|
|
67
|
+
topic: str = ""
|
|
68
|
+
audience: str = "personal" # personal | group
|
|
69
|
+
destination: dict = field(default_factory=dict)
|
|
70
|
+
llm: dict = field(default_factory=dict) # {provider, model} pin
|
|
71
|
+
cadence: dict = field(default_factory=dict)
|
|
72
|
+
storage: dict = field(default_factory=dict) # per-path overrides
|
|
73
|
+
sources: dict = field(default_factory=dict)
|
|
74
|
+
registry: list = field(default_factory=list) # tiered source registry
|
|
75
|
+
scoring: dict = field(default_factory=dict)
|
|
76
|
+
brief: dict = field(default_factory=dict)
|
|
77
|
+
privacy: dict = field(default_factory=dict)
|
|
78
|
+
learning_loop: bool = True
|
|
79
|
+
budget: dict = field(default_factory=dict)
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------ load
|
|
82
|
+
@classmethod
|
|
83
|
+
def load(cls, path: str) -> "FeedSpec":
|
|
84
|
+
with open(os.path.expanduser(path), encoding="utf-8") as f:
|
|
85
|
+
text = f.read()
|
|
86
|
+
if path.endswith(".json"):
|
|
87
|
+
raw = json.loads(text)
|
|
88
|
+
else:
|
|
89
|
+
try:
|
|
90
|
+
import yaml # type: ignore
|
|
91
|
+
except ImportError as exc: # pragma: no cover
|
|
92
|
+
raise SpecError([f"PyYAML unavailable ({exc}); use a .json spec"]) from exc
|
|
93
|
+
raw = yaml.safe_load(text)
|
|
94
|
+
if not isinstance(raw, dict):
|
|
95
|
+
raise SpecError(["spec root must be a mapping"])
|
|
96
|
+
return cls.from_dict(raw)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, raw: dict) -> "FeedSpec":
|
|
100
|
+
spec = cls(raw=raw)
|
|
101
|
+
errors: list[str] = []
|
|
102
|
+
|
|
103
|
+
spec.name = str(raw.get("name", "")).strip()
|
|
104
|
+
if not _SLUG_RE.match(spec.name):
|
|
105
|
+
errors.append("name: required kebab-case slug, e.g. 'ai-inference'")
|
|
106
|
+
spec.title = str(raw.get("title") or spec.name.replace("-", " ").title())
|
|
107
|
+
spec.topic = str(raw.get("topic", "")).strip()
|
|
108
|
+
if not spec.topic:
|
|
109
|
+
errors.append("topic: required — one paragraph describing what this feed tracks and why")
|
|
110
|
+
|
|
111
|
+
spec.audience = raw.get("audience", "personal")
|
|
112
|
+
if spec.audience not in ("personal", "group"):
|
|
113
|
+
errors.append("audience: must be 'personal' or 'group'")
|
|
114
|
+
|
|
115
|
+
dest = raw.get("destination") or {}
|
|
116
|
+
kind = dest.get("kind")
|
|
117
|
+
if kind not in DESTINATION_KINDS:
|
|
118
|
+
errors.append(f"destination.kind: required, one of {DESTINATION_KINDS}")
|
|
119
|
+
elif kind == "command" and not dest.get("send_command"):
|
|
120
|
+
errors.append("destination.send_command: required for kind=command "
|
|
121
|
+
"(shell command the brief is piped to)")
|
|
122
|
+
elif kind == "deliver" and not dest.get("deliver"):
|
|
123
|
+
errors.append("destination.deliver: required for kind=deliver "
|
|
124
|
+
"(harness delivery target, e.g. 'origin' or 'platform:chat_id')")
|
|
125
|
+
spec.destination = dest
|
|
126
|
+
|
|
127
|
+
cadence = raw.get("cadence") or {}
|
|
128
|
+
for req in ("collect", "distill"):
|
|
129
|
+
if not cadence.get(req):
|
|
130
|
+
errors.append(f"cadence.{req}: required (harness schedule string, e.g. 'every 120m')")
|
|
131
|
+
spec.cadence = cadence
|
|
132
|
+
|
|
133
|
+
sources = raw.get("sources") or {}
|
|
134
|
+
if not any(sources.get(k) for k in ADAPTER_KEYS):
|
|
135
|
+
errors.append(f"sources: at least one adapter of {ADAPTER_KEYS} must be configured")
|
|
136
|
+
spec.sources = sources
|
|
137
|
+
|
|
138
|
+
spec.registry = raw.get("registry") or []
|
|
139
|
+
for i, entry in enumerate(spec.registry):
|
|
140
|
+
if not isinstance(entry, dict) or not entry.get("name"):
|
|
141
|
+
errors.append(f"registry[{i}]: each entry needs at least a 'name'")
|
|
142
|
+
elif entry.get("weight", "P1") not in ("P0", "P1", "P2"):
|
|
143
|
+
errors.append(f"registry[{i}] ({entry.get('name')}): weight must be P0/P1/P2")
|
|
144
|
+
|
|
145
|
+
spec.scoring = _merged(DEFAULT_SCORING, raw.get("scoring") or {})
|
|
146
|
+
spec.llm = raw.get("llm") or {}
|
|
147
|
+
spec.storage = raw.get("storage") or {}
|
|
148
|
+
spec.brief = raw.get("brief") or {}
|
|
149
|
+
spec.brief.setdefault("framing", DEFAULT_FRAMING)
|
|
150
|
+
spec.brief.setdefault("standard_cap", 5)
|
|
151
|
+
spec.privacy = raw.get("privacy") or {}
|
|
152
|
+
spec.privacy.setdefault("forbidden", list(DEFAULT_PRIVACY_FORBIDDEN))
|
|
153
|
+
spec.learning_loop = bool(raw.get("learning_loop", True))
|
|
154
|
+
spec.budget = raw.get("budget") or {}
|
|
155
|
+
|
|
156
|
+
if errors:
|
|
157
|
+
raise SpecError(errors)
|
|
158
|
+
return spec
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------- storage
|
|
161
|
+
def data_root(self, base: str = "~/.hermes/data/feeds") -> str:
|
|
162
|
+
root = self.storage.get("root") or f"{base}/{self.name}"
|
|
163
|
+
return os.path.expanduser(root)
|
|
164
|
+
|
|
165
|
+
def path(self, key: str, default_name: str) -> str:
|
|
166
|
+
"""Resolve a storage path, honouring per-key spec overrides."""
|
|
167
|
+
override = self.storage.get(key)
|
|
168
|
+
if override:
|
|
169
|
+
return os.path.expanduser(override)
|
|
170
|
+
return os.path.join(self.data_root(), default_name)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def queue_path(self) -> str:
|
|
174
|
+
return self.path("queue_path", "queue.json")
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def seen_db(self) -> str:
|
|
178
|
+
return self.path("seen_db", "seen.db")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def briefs_dir(self) -> str:
|
|
182
|
+
return self.path("briefs_dir", "briefs")
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def alert_path(self) -> str:
|
|
186
|
+
return self.path("alert_path", "alert.json")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def deltas_log(self) -> str:
|
|
190
|
+
return self.path("deltas_log", "briefs/_registry_deltas.log")
|
|
191
|
+
|
|
192
|
+
# -------------------------------------------------------- engine config
|
|
193
|
+
def engine_config(self) -> dict[str, Any]:
|
|
194
|
+
"""Render the self-contained collector-engine config for this feed."""
|
|
195
|
+
s = self.sources
|
|
196
|
+
arxiv = s.get("arxiv") or {}
|
|
197
|
+
sc = self.scoring
|
|
198
|
+
cfg: dict[str, Any] = {
|
|
199
|
+
"feed_name": self.name,
|
|
200
|
+
"queue_path": self.queue_path,
|
|
201
|
+
"seen_db": self.seen_db,
|
|
202
|
+
"alert_path": self.alert_path,
|
|
203
|
+
# adapters (empty list / missing = adapter disabled)
|
|
204
|
+
"x_searches": s.get("x_searches") or [],
|
|
205
|
+
"x_results_per_query": s.get("x_results_per_query", 15),
|
|
206
|
+
"key_accounts": s.get("key_accounts") or [],
|
|
207
|
+
"hf_keywords": s.get("hf_keywords") or [],
|
|
208
|
+
"github_search": s.get("github_search") or "",
|
|
209
|
+
"github_keywords": s.get("github_keywords") or [],
|
|
210
|
+
"forum_urls": s.get("forum_urls") or [],
|
|
211
|
+
"forum_title_keywords": s.get("forum_title_keywords") or [],
|
|
212
|
+
"arxiv_categories": arxiv.get("categories") or [],
|
|
213
|
+
"arxiv_query_terms": arxiv.get("query_terms") or [],
|
|
214
|
+
"arxiv_keywords": arxiv.get("keywords") or [],
|
|
215
|
+
"rss": s.get("rss") or [],
|
|
216
|
+
# scoring
|
|
217
|
+
"keyword_categories": sc.get("keyword_categories") or [],
|
|
218
|
+
"scoring": {
|
|
219
|
+
"account_whitelist": sc.get("account_whitelist", 10),
|
|
220
|
+
"engagement_per_100_likes": sc.get("engagement", {}).get("per_100_likes", 1),
|
|
221
|
+
"engagement_per_1000_likes": sc.get("engagement", {}).get("per_1000_likes", 2),
|
|
222
|
+
"engagement_max": sc.get("engagement", {}).get("max", 10),
|
|
223
|
+
"human_curated": sc.get("human_curated", 20),
|
|
224
|
+
"benchmark_numbers": sc.get("benchmark_numbers", 15),
|
|
225
|
+
"code_release": sc.get("code_release", 10),
|
|
226
|
+
"open_weights": sc.get("open_weights", 10),
|
|
227
|
+
},
|
|
228
|
+
"benchmark_regex": sc.get(
|
|
229
|
+
"benchmark_regex",
|
|
230
|
+
r"\d+\s*(tok/s|tokens?/?s|t/s|tflops?|ms|latency|throughput)",
|
|
231
|
+
),
|
|
232
|
+
"priority_thresholds": sc.get("thresholds", DEFAULT_SCORING["thresholds"]),
|
|
233
|
+
# alert delivery for the alert stage (kind=command only)
|
|
234
|
+
"send_command": (self.destination.get("send_command")
|
|
235
|
+
if self.destination.get("kind") == "command" else ""),
|
|
236
|
+
"alert_min_priority": (self.brief.get("alert_min_priority", "P0")),
|
|
237
|
+
}
|
|
238
|
+
# Legacy compatibility: a spec may point config_path at a hand-written
|
|
239
|
+
# engine config; then that file wins entirely (see manager.create).
|
|
240
|
+
return cfg
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _merged(base: dict, override: dict) -> dict:
|
|
244
|
+
out = json.loads(json.dumps(base))
|
|
245
|
+
for k, v in override.items():
|
|
246
|
+
if isinstance(v, dict) and isinstance(out.get(k), dict):
|
|
247
|
+
out[k].update(v)
|
|
248
|
+
else:
|
|
249
|
+
out[k] = v
|
|
250
|
+
return out
|