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,132 @@
|
|
|
1
|
+
-- Colony World Model SQLite schema
|
|
2
|
+
-- All tables prefixed with wm_ to avoid collisions with existing schema.
|
|
3
|
+
|
|
4
|
+
-- Base entity table (all types)
|
|
5
|
+
CREATE TABLE IF NOT EXISTS wm_entities (
|
|
6
|
+
id TEXT PRIMARY KEY, -- we-<timestamp>-<random7>
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
entity_type TEXT NOT NULL
|
|
9
|
+
CHECK(entity_type IN (
|
|
10
|
+
'person', 'company', 'project', 'product',
|
|
11
|
+
'location', 'event', 'concept'
|
|
12
|
+
)),
|
|
13
|
+
aliases TEXT NOT NULL DEFAULT '[]', -- JSON array
|
|
14
|
+
external_ids TEXT NOT NULL DEFAULT '{}', -- JSON object
|
|
15
|
+
properties TEXT NOT NULL DEFAULT '{}', -- JSON object
|
|
16
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
17
|
+
first_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
18
|
+
last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
19
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
20
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_wm_entities_type
|
|
24
|
+
ON wm_entities(entity_type);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_wm_entities_name
|
|
26
|
+
ON wm_entities(name COLLATE NOCASE);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_wm_entities_confidence
|
|
28
|
+
ON wm_entities(confidence DESC);
|
|
29
|
+
|
|
30
|
+
-- Full-text search index on name + aliases
|
|
31
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS wm_entities_fts
|
|
32
|
+
USING fts5(
|
|
33
|
+
id UNINDEXED,
|
|
34
|
+
name,
|
|
35
|
+
aliases,
|
|
36
|
+
content='wm_entities',
|
|
37
|
+
content_rowid='rowid'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- Triggers to keep FTS in sync
|
|
41
|
+
CREATE TRIGGER IF NOT EXISTS wm_entities_fts_insert
|
|
42
|
+
AFTER INSERT ON wm_entities BEGIN
|
|
43
|
+
INSERT INTO wm_entities_fts(rowid, id, name, aliases)
|
|
44
|
+
VALUES (new.rowid, new.id, new.name, new.aliases);
|
|
45
|
+
END;
|
|
46
|
+
|
|
47
|
+
CREATE TRIGGER IF NOT EXISTS wm_entities_fts_update
|
|
48
|
+
AFTER UPDATE ON wm_entities BEGIN
|
|
49
|
+
INSERT INTO wm_entities_fts(wm_entities_fts, rowid, id, name, aliases)
|
|
50
|
+
VALUES ('delete', old.rowid, old.id, old.name, old.aliases);
|
|
51
|
+
INSERT INTO wm_entities_fts(rowid, id, name, aliases)
|
|
52
|
+
VALUES (new.rowid, new.id, new.name, new.aliases);
|
|
53
|
+
END;
|
|
54
|
+
|
|
55
|
+
CREATE TRIGGER IF NOT EXISTS wm_entities_fts_delete
|
|
56
|
+
AFTER DELETE ON wm_entities BEGIN
|
|
57
|
+
INSERT INTO wm_entities_fts(wm_entities_fts, rowid, id, name, aliases)
|
|
58
|
+
VALUES ('delete', old.rowid, old.id, old.name, old.aliases);
|
|
59
|
+
END;
|
|
60
|
+
|
|
61
|
+
-- Relationships
|
|
62
|
+
CREATE TABLE IF NOT EXISTS wm_relationships (
|
|
63
|
+
id TEXT PRIMARY KEY, -- wr-<timestamp>-<random7>
|
|
64
|
+
source_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
65
|
+
target_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
66
|
+
relationship_type TEXT NOT NULL,
|
|
67
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
68
|
+
valid_from TEXT, -- ISO8601; NULL = unknown start
|
|
69
|
+
valid_to TEXT, -- ISO8601; NULL = still active
|
|
70
|
+
properties TEXT NOT NULL DEFAULT '{}', -- JSON
|
|
71
|
+
source_observation_id TEXT,
|
|
72
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
73
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_source
|
|
77
|
+
ON wm_relationships(source_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_target
|
|
79
|
+
ON wm_relationships(target_id);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_type
|
|
81
|
+
ON wm_relationships(relationship_type);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_active
|
|
83
|
+
ON wm_relationships(valid_to)
|
|
84
|
+
WHERE valid_to IS NULL;
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_temporal
|
|
86
|
+
ON wm_relationships(valid_from, valid_to);
|
|
87
|
+
|
|
88
|
+
-- Observations (provenance trail)
|
|
89
|
+
CREATE TABLE IF NOT EXISTS wm_observations (
|
|
90
|
+
id TEXT PRIMARY KEY, -- wo-<timestamp>-<random7>
|
|
91
|
+
entity_id TEXT REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
92
|
+
relationship_id TEXT REFERENCES wm_relationships(id) ON DELETE CASCADE,
|
|
93
|
+
observation TEXT NOT NULL,
|
|
94
|
+
source TEXT,
|
|
95
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_wm_obs_entity
|
|
99
|
+
ON wm_observations(entity_id, created_at DESC);
|
|
100
|
+
|
|
101
|
+
-- Merge proposals
|
|
102
|
+
CREATE TABLE IF NOT EXISTS wm_merge_proposals (
|
|
103
|
+
id TEXT PRIMARY KEY, -- mp-<timestamp>-<random7>
|
|
104
|
+
candidate_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
105
|
+
existing_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
106
|
+
match_confidence REAL NOT NULL,
|
|
107
|
+
match_reason TEXT NOT NULL,
|
|
108
|
+
evidence TEXT NOT NULL DEFAULT '{}', -- JSON
|
|
109
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
110
|
+
CHECK(status IN ('pending', 'approved', 'rejected', 'expired')),
|
|
111
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
112
|
+
resolved_at TEXT
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
-- Merge audit log
|
|
116
|
+
CREATE TABLE IF NOT EXISTS wm_merge_log (
|
|
117
|
+
id TEXT PRIMARY KEY, -- ma-<timestamp>-<random7>
|
|
118
|
+
surviving_id TEXT NOT NULL,
|
|
119
|
+
retired_id TEXT NOT NULL,
|
|
120
|
+
relationships_repointed INTEGER NOT NULL DEFAULT 0,
|
|
121
|
+
properties_updated INTEGER NOT NULL DEFAULT 0,
|
|
122
|
+
executed_by TEXT NOT NULL, -- "auto" | "owner_approved"
|
|
123
|
+
merge_proposal_id TEXT,
|
|
124
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
-- Schema version tracking
|
|
128
|
+
CREATE TABLE IF NOT EXISTS wm_schema_migrations (
|
|
129
|
+
version TEXT PRIMARY KEY,
|
|
130
|
+
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
131
|
+
description TEXT
|
|
132
|
+
);
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""World Model primary store interface.
|
|
2
|
+
|
|
3
|
+
SQLite is the canonical typed-observation backend. Neo4j and PostgreSQL are
|
|
4
|
+
legacy entity/relationship adapters without typed-observation support.
|
|
5
|
+
All callers must use this interface and never access the backend directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
|
13
|
+
from collections import deque
|
|
14
|
+
|
|
15
|
+
from .config import WorldModelConfig
|
|
16
|
+
from .constants import CAUSAL_RELATIONSHIP_TYPES
|
|
17
|
+
from .entities import BaseEntity
|
|
18
|
+
from .relationships import WorldRelationship
|
|
19
|
+
from .sqlite.backend import SQLiteBackend
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_causal(rel_type: str) -> bool:
|
|
25
|
+
return str(rel_type or "").strip().upper() in CAUSAL_RELATIONSHIP_TYPES
|
|
26
|
+
|
|
27
|
+
# Optional inline belief-maintenance hook (item 7): called as
|
|
28
|
+
# cb(entity_id, key, old_value, new_value, old_conf, new_conf) when a
|
|
29
|
+
# property UPDATE changes an existing value, so the superseded value gets an
|
|
30
|
+
# audit row. Set at boot by the server; never raises into the write path.
|
|
31
|
+
_property_audit_hook = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_property_audit_hook(cb) -> None:
|
|
35
|
+
global _property_audit_hook
|
|
36
|
+
_property_audit_hook = cb
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class GraphNeighborhoodResult:
|
|
41
|
+
center: BaseEntity
|
|
42
|
+
reachable: List[BaseEntity]
|
|
43
|
+
edges: List[WorldRelationship]
|
|
44
|
+
hop_counts: Dict[str, int] # entity_id → min hops from center
|
|
45
|
+
truncated: bool = False # True if max_nodes reached
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WorldModelStats:
|
|
50
|
+
total_entities: int
|
|
51
|
+
entities_by_type: Dict[str, int]
|
|
52
|
+
total_relationships: int
|
|
53
|
+
active_relationships: int
|
|
54
|
+
total_observations: int
|
|
55
|
+
merge_proposals_pending: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WorldModelStore:
|
|
59
|
+
"""Primary interface to the Colony World Model entity graph.
|
|
60
|
+
|
|
61
|
+
SQLite is selected by default. Explicit legacy Neo4j/Postgres configurations
|
|
62
|
+
may fall back to SQLite when credentials or drivers are missing. Typed
|
|
63
|
+
observation methods reject an actual legacy backend rather than claiming
|
|
64
|
+
equivalent provenance, correction, temporal-view or erasure behavior.
|
|
65
|
+
All methods are async. Callers MUST NOT access the backing store directly.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, config: Optional[WorldModelConfig] = None) -> None:
|
|
69
|
+
self._config = config or WorldModelConfig()
|
|
70
|
+
self._backend: Optional[SQLiteBackend] = None
|
|
71
|
+
|
|
72
|
+
async def connect(self) -> None:
|
|
73
|
+
"""Initialize and connect to the storage backend."""
|
|
74
|
+
if self._config.backend == "neo4j":
|
|
75
|
+
try:
|
|
76
|
+
from apsimo.world_model.neo4j.backend import Neo4jBackend
|
|
77
|
+
neo4j_uri = self._config.neo4j_uri
|
|
78
|
+
neo4j_db = self._config.neo4j_database
|
|
79
|
+
neo4j_user = os.environ.get("NEO4J_USER", "neo4j")
|
|
80
|
+
neo4j_pass = os.environ.get("NEO4J_PASSWORD", "")
|
|
81
|
+
if neo4j_uri and neo4j_pass:
|
|
82
|
+
self._backend = Neo4jBackend(
|
|
83
|
+
uri=neo4j_uri,
|
|
84
|
+
database=neo4j_db,
|
|
85
|
+
username=neo4j_user,
|
|
86
|
+
password=neo4j_pass,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
logger.warning("NEO4J_URI or NEO4J_PASSWORD not set — falling back to sqlite")
|
|
90
|
+
self._backend = SQLiteBackend(self._config.sqlite_path)
|
|
91
|
+
except ImportError:
|
|
92
|
+
logger.warning("neo4j driver not installed — falling back to sqlite")
|
|
93
|
+
self._backend = SQLiteBackend(self._config.sqlite_path)
|
|
94
|
+
elif self._config.backend == "postgres":
|
|
95
|
+
try:
|
|
96
|
+
from apsimo.world_model.postgres.backend import PostgresBackend
|
|
97
|
+
pg_conn = os.environ.get("WORLD_MODEL_PG_CONNECTION", "")
|
|
98
|
+
if pg_conn:
|
|
99
|
+
self._backend = PostgresBackend(pg_conn)
|
|
100
|
+
else:
|
|
101
|
+
logger.warning("WORLD_MODEL_PG_CONNECTION not set — falling back to sqlite")
|
|
102
|
+
self._backend = SQLiteBackend(self._config.sqlite_path)
|
|
103
|
+
except ImportError:
|
|
104
|
+
logger.warning("asyncpg not installed — falling back to sqlite")
|
|
105
|
+
self._backend = SQLiteBackend(self._config.sqlite_path)
|
|
106
|
+
elif self._config.backend == "sqlite":
|
|
107
|
+
self._backend = SQLiteBackend(self._config.sqlite_path)
|
|
108
|
+
else:
|
|
109
|
+
logger.warning("WorldModel backend '%s' not supported — defaulting to sqlite", self._config.backend)
|
|
110
|
+
self._backend = SQLiteBackend(self._config.sqlite_path)
|
|
111
|
+
await self._backend.connect()
|
|
112
|
+
|
|
113
|
+
async def close(self) -> None:
|
|
114
|
+
if self._backend:
|
|
115
|
+
await self._backend.close()
|
|
116
|
+
|
|
117
|
+
async def __aenter__(self) -> "WorldModelStore":
|
|
118
|
+
await self.connect()
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
async def __aexit__(self, *args) -> None:
|
|
122
|
+
await self.close()
|
|
123
|
+
|
|
124
|
+
# ── Entity reads ──────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async def get_entity(
|
|
127
|
+
self,
|
|
128
|
+
entity_id: str,
|
|
129
|
+
min_confidence: float = 0.30,
|
|
130
|
+
) -> Optional[BaseEntity]:
|
|
131
|
+
"""Fetch a single entity by ID.
|
|
132
|
+
|
|
133
|
+
Returns None if the entity does not exist or falls below min_confidence.
|
|
134
|
+
"""
|
|
135
|
+
entity = await self._backend.get_entity(entity_id)
|
|
136
|
+
if entity and entity.confidence >= min_confidence:
|
|
137
|
+
return entity
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
async def find_entities(
|
|
141
|
+
self,
|
|
142
|
+
query: str,
|
|
143
|
+
entity_type: Optional[str] = None,
|
|
144
|
+
min_confidence: float = 0.30,
|
|
145
|
+
limit: int = 20,
|
|
146
|
+
) -> List[BaseEntity]:
|
|
147
|
+
"""Full-text search across entity names and aliases."""
|
|
148
|
+
return await self._backend.find_entities(
|
|
149
|
+
query=query,
|
|
150
|
+
entity_type=entity_type,
|
|
151
|
+
min_confidence=min_confidence,
|
|
152
|
+
limit=limit,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def get_entity_by_external_id(
|
|
156
|
+
self,
|
|
157
|
+
key: str,
|
|
158
|
+
value: str,
|
|
159
|
+
) -> Optional[BaseEntity]:
|
|
160
|
+
"""Look up an entity by external ID key/value pair."""
|
|
161
|
+
return await self._backend.get_entity_by_external_id(key, value)
|
|
162
|
+
|
|
163
|
+
# ── Graph traversal ───────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
async def get_neighborhood(
|
|
166
|
+
self,
|
|
167
|
+
entity_id: str,
|
|
168
|
+
max_hops: int = 2,
|
|
169
|
+
relationship_types: Optional[List[str]] = None,
|
|
170
|
+
min_confidence: float = 0.30,
|
|
171
|
+
max_nodes: int = 200,
|
|
172
|
+
) -> GraphNeighborhoodResult:
|
|
173
|
+
"""BFS neighborhood traversal up to max_hops from entity_id.
|
|
174
|
+
|
|
175
|
+
MUST NOT exceed max_hops = 5 regardless of caller input.
|
|
176
|
+
"""
|
|
177
|
+
max_hops = min(max_hops, self._config.max_graph_hops)
|
|
178
|
+
center = await self._backend.get_entity(entity_id)
|
|
179
|
+
if not center:
|
|
180
|
+
return GraphNeighborhoodResult(
|
|
181
|
+
center=None, reachable=[], edges=[], hop_counts={}, truncated=False
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
visited: Dict[str, int] = {entity_id: 0}
|
|
185
|
+
frontier = deque([entity_id])
|
|
186
|
+
reachable: List[BaseEntity] = []
|
|
187
|
+
edges: List[WorldRelationship] = []
|
|
188
|
+
truncated = False
|
|
189
|
+
|
|
190
|
+
while frontier:
|
|
191
|
+
current_id = frontier.popleft()
|
|
192
|
+
current_hop = visited[current_id]
|
|
193
|
+
if current_hop >= max_hops:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
neighbors = await self._backend.get_neighbors(
|
|
197
|
+
current_id, min_confidence=min_confidence,
|
|
198
|
+
relationship_types=relationship_types,
|
|
199
|
+
)
|
|
200
|
+
for neighbor, rel in neighbors:
|
|
201
|
+
# Query-only causal guard (H2.5, belt-and-braces over the
|
|
202
|
+
# backend exclusion): an untyped traversal never returns or
|
|
203
|
+
# walks causal edges; only explicitly typed queries do.
|
|
204
|
+
if not relationship_types and _is_causal(rel.relationship_type):
|
|
205
|
+
continue
|
|
206
|
+
edges.append(rel)
|
|
207
|
+
if neighbor.id not in visited:
|
|
208
|
+
hop = current_hop + 1
|
|
209
|
+
visited[neighbor.id] = hop
|
|
210
|
+
reachable.append(neighbor)
|
|
211
|
+
if len(reachable) >= max_nodes:
|
|
212
|
+
truncated = True
|
|
213
|
+
break
|
|
214
|
+
frontier.append(neighbor.id)
|
|
215
|
+
if truncated:
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
hop_counts = {eid: h for eid, h in visited.items() if eid != entity_id}
|
|
219
|
+
return GraphNeighborhoodResult(
|
|
220
|
+
center=center,
|
|
221
|
+
reachable=reachable,
|
|
222
|
+
edges=edges,
|
|
223
|
+
hop_counts=hop_counts,
|
|
224
|
+
truncated=truncated,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def find_path(
|
|
228
|
+
self,
|
|
229
|
+
source_id: str,
|
|
230
|
+
target_id: str,
|
|
231
|
+
max_hops: int = 5,
|
|
232
|
+
min_confidence: float = 0.30,
|
|
233
|
+
) -> Optional[List[WorldRelationship]]:
|
|
234
|
+
"""Find shortest path between two entities via BFS.
|
|
235
|
+
|
|
236
|
+
Returns ordered list of relationships, or None if no path exists.
|
|
237
|
+
"""
|
|
238
|
+
max_hops = min(max_hops, self._config.max_graph_hops)
|
|
239
|
+
if source_id == target_id:
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
# BFS with path tracking
|
|
243
|
+
visited: Dict[str, Optional[tuple]] = {source_id: None} # id → (prev_id, rel)
|
|
244
|
+
frontier = deque([(source_id, 0)])
|
|
245
|
+
|
|
246
|
+
while frontier:
|
|
247
|
+
current_id, hops = frontier.popleft()
|
|
248
|
+
if hops >= max_hops:
|
|
249
|
+
continue
|
|
250
|
+
neighbors = await self._backend.get_neighbors(
|
|
251
|
+
current_id, min_confidence=min_confidence
|
|
252
|
+
)
|
|
253
|
+
for neighbor, rel in neighbors:
|
|
254
|
+
# Query-only causal guard: untyped path-finding never walks
|
|
255
|
+
# causal edges (see get_neighborhood).
|
|
256
|
+
if _is_causal(rel.relationship_type):
|
|
257
|
+
continue
|
|
258
|
+
if neighbor.id not in visited:
|
|
259
|
+
visited[neighbor.id] = (current_id, rel)
|
|
260
|
+
if neighbor.id == target_id:
|
|
261
|
+
# Reconstruct path
|
|
262
|
+
path = []
|
|
263
|
+
node = target_id
|
|
264
|
+
while visited[node] is not None:
|
|
265
|
+
prev_id, edge = visited[node]
|
|
266
|
+
path.append(edge)
|
|
267
|
+
node = prev_id
|
|
268
|
+
path.reverse()
|
|
269
|
+
return path
|
|
270
|
+
frontier.append((neighbor.id, hops + 1))
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
async def find_common_neighbors(
|
|
274
|
+
self,
|
|
275
|
+
entity_id_a: str,
|
|
276
|
+
entity_id_b: str,
|
|
277
|
+
max_hops: int = 1,
|
|
278
|
+
) -> List[BaseEntity]:
|
|
279
|
+
"""Return entities reachable from both A and B within max_hops."""
|
|
280
|
+
result_a = await self.get_neighborhood(entity_id_a, max_hops=max_hops)
|
|
281
|
+
result_b = await self.get_neighborhood(entity_id_b, max_hops=max_hops)
|
|
282
|
+
ids_a = {e.id for e in result_a.reachable}
|
|
283
|
+
ids_b = {e.id for e in result_b.reachable}
|
|
284
|
+
shared_ids = ids_a & ids_b
|
|
285
|
+
return [e for e in result_a.reachable if e.id in shared_ids]
|
|
286
|
+
|
|
287
|
+
# ── Relationship reads ────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
async def query_relationships(
|
|
290
|
+
self,
|
|
291
|
+
source_id: Optional[str] = None,
|
|
292
|
+
target_id: Optional[str] = None,
|
|
293
|
+
relationship_type: Optional[str] = None,
|
|
294
|
+
target_types: Optional[List[str]] = None,
|
|
295
|
+
active_only: bool = False,
|
|
296
|
+
min_confidence: float = 0.30,
|
|
297
|
+
limit: int = 100,
|
|
298
|
+
) -> List[WorldRelationship]:
|
|
299
|
+
"""Query relationships with flexible filtering.
|
|
300
|
+
|
|
301
|
+
Query-only causal guard (H2.5): when ``relationship_type`` is None
|
|
302
|
+
(an untyped, generic read) causal edges are excluded — enforced in
|
|
303
|
+
every backend and re-checked here so no backend drift can leak them.
|
|
304
|
+
Only explicitly typed queries (and /world/causal/*) return them.
|
|
305
|
+
"""
|
|
306
|
+
rels = await self._backend.query_relationships(
|
|
307
|
+
source_id=source_id,
|
|
308
|
+
target_id=target_id,
|
|
309
|
+
relationship_type=relationship_type,
|
|
310
|
+
target_types=target_types,
|
|
311
|
+
active_only=active_only,
|
|
312
|
+
min_confidence=min_confidence,
|
|
313
|
+
limit=limit,
|
|
314
|
+
)
|
|
315
|
+
if not relationship_type:
|
|
316
|
+
rels = [r for r in rels if not _is_causal(r.relationship_type)]
|
|
317
|
+
return rels
|
|
318
|
+
|
|
319
|
+
async def query_at_time(
|
|
320
|
+
self,
|
|
321
|
+
entity_id: str,
|
|
322
|
+
as_of: str,
|
|
323
|
+
relationship_types: Optional[List[str]] = None,
|
|
324
|
+
) -> List[WorldRelationship]:
|
|
325
|
+
"""Return relationships active at a specific point in time.
|
|
326
|
+
Untyped calls exclude causal edges (query-only guard, H2.5)."""
|
|
327
|
+
rels = await self._backend.query_at_time(
|
|
328
|
+
entity_id=entity_id, as_of=as_of, relationship_types=relationship_types
|
|
329
|
+
)
|
|
330
|
+
if not relationship_types:
|
|
331
|
+
rels = [r for r in rels if not _is_causal(r.relationship_type)]
|
|
332
|
+
return rels
|
|
333
|
+
|
|
334
|
+
# ── Entity writes ─────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
|
|
337
|
+
"""Insert or update an entity. Returns the surviving entity."""
|
|
338
|
+
result = await self._backend.upsert_entity(entity)
|
|
339
|
+
self._emit_change("entity_upsert", entity_id=getattr(result, "id", None))
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
async def update_entity_property(
|
|
343
|
+
self,
|
|
344
|
+
entity_id: str,
|
|
345
|
+
property_key: str,
|
|
346
|
+
property_value: Any,
|
|
347
|
+
confidence: float,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Update a legacy descriptive property if its confidence is higher.
|
|
350
|
+
|
|
351
|
+
This is not current operational truth. New operational consumers use
|
|
352
|
+
record_property_observation/property_state with explicit provenance.
|
|
353
|
+
"""
|
|
354
|
+
# Capture the pre-update value so a supersession can be audited
|
|
355
|
+
# (item 7 inline hook). Best-effort; never blocks the write.
|
|
356
|
+
old_value, old_conf = None, 0.0
|
|
357
|
+
if _property_audit_hook is not None:
|
|
358
|
+
try:
|
|
359
|
+
prior = await self._backend.get_entity(entity_id)
|
|
360
|
+
if prior is not None:
|
|
361
|
+
props = getattr(prior, "properties", {}) or {}
|
|
362
|
+
old_value = props.get(property_key,
|
|
363
|
+
getattr(prior, property_key, None))
|
|
364
|
+
old_conf = float(props.get(f"_conf_{property_key}",
|
|
365
|
+
prior.confidence) or 0.0)
|
|
366
|
+
except Exception:
|
|
367
|
+
old_value = None
|
|
368
|
+
await self._backend.update_entity_property(
|
|
369
|
+
entity_id, property_key, property_value, confidence
|
|
370
|
+
)
|
|
371
|
+
if (_property_audit_hook is not None and old_value not in (None, "")
|
|
372
|
+
and str(old_value) != str(property_value)
|
|
373
|
+
and confidence >= old_conf):
|
|
374
|
+
try:
|
|
375
|
+
_property_audit_hook(entity_id, property_key, old_value,
|
|
376
|
+
property_value, old_conf, confidence)
|
|
377
|
+
except Exception:
|
|
378
|
+
logger.debug("property audit hook failed", exc_info=True)
|
|
379
|
+
self._emit_change("entity_property_update", entity_id=entity_id, property_key=property_key)
|
|
380
|
+
|
|
381
|
+
async def reinforce_entity(self, entity_id: str) -> None:
|
|
382
|
+
"""Record a repeat mention without raising evidential confidence.
|
|
383
|
+
|
|
384
|
+
Familiarity updates last_seen/count. No-op on unsupported backends.
|
|
385
|
+
"""
|
|
386
|
+
fn = getattr(self._backend, "reinforce_entity", None)
|
|
387
|
+
if fn is None:
|
|
388
|
+
logger.debug("reinforce_entity unsupported by %s backend",
|
|
389
|
+
type(self._backend).__name__)
|
|
390
|
+
return
|
|
391
|
+
await fn(entity_id)
|
|
392
|
+
|
|
393
|
+
async def add_entity_alias(self, entity_id: str, alias: str) -> None:
|
|
394
|
+
"""Add an alias to an entity's alias list if not already present."""
|
|
395
|
+
await self._backend.add_entity_alias(entity_id, alias)
|
|
396
|
+
self._emit_change("entity_alias_added", entity_id=entity_id)
|
|
397
|
+
|
|
398
|
+
# ── Relationship writes ───────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
async def upsert_relationship(
|
|
401
|
+
self, relationship: WorldRelationship
|
|
402
|
+
) -> WorldRelationship:
|
|
403
|
+
"""Insert or update a relationship."""
|
|
404
|
+
result = await self._backend.upsert_relationship(relationship)
|
|
405
|
+
self._emit_change(
|
|
406
|
+
"relationship_upsert",
|
|
407
|
+
relationship_id=getattr(result, "id", None),
|
|
408
|
+
)
|
|
409
|
+
return result
|
|
410
|
+
|
|
411
|
+
async def close_relationship(
|
|
412
|
+
self, relationship_id: str, valid_to: str
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Mark a relationship as ended by setting valid_to."""
|
|
415
|
+
await self._backend.close_relationship(relationship_id, valid_to)
|
|
416
|
+
self._emit_change("relationship_closed", relationship_id=relationship_id)
|
|
417
|
+
|
|
418
|
+
# ── Event emission ────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
@staticmethod
|
|
421
|
+
def _emit_change(change_type: str, **payload: Any) -> None:
|
|
422
|
+
"""Broadcast a world_model_changed event. Best-effort — never raises."""
|
|
423
|
+
try:
|
|
424
|
+
from apsimo.events.broadcaster import emit as _emit
|
|
425
|
+
_emit(
|
|
426
|
+
"world_model_changed",
|
|
427
|
+
{"change_type": change_type, **{k: v for k, v in payload.items() if v is not None}},
|
|
428
|
+
)
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# ── Observations ─────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
async def record_property_observation(self, **kwargs):
|
|
435
|
+
"""Append one scoped assertion; producers must supply canonical evidence.
|
|
436
|
+
|
|
437
|
+
SQLite is the qualified observation backend in this release. Other
|
|
438
|
+
adapters remain valid for legacy entities/edges and report unsupported
|
|
439
|
+
rather than substituting old unqualified property values.
|
|
440
|
+
"""
|
|
441
|
+
from .observations import observation
|
|
442
|
+
put = getattr(self._backend, 'put_property_observation', None)
|
|
443
|
+
if put is None:
|
|
444
|
+
raise NotImplementedError('typed_world_observations_require_sqlite')
|
|
445
|
+
data = observation(**kwargs)
|
|
446
|
+
if await self.get_entity(data['entity_id'], min_confidence=0) is None:
|
|
447
|
+
raise ValueError('world_observation_entity_not_found')
|
|
448
|
+
result = await put(data)
|
|
449
|
+
self._emit_change('property_observed', entity_id=data['entity_id'], observation_id=data['observation_id'])
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
async def property_state(self, entity_id, property_key, *, subject_person_id,
|
|
453
|
+
viewer_scope, shareability, as_of=None, source_ledger=None):
|
|
454
|
+
from .observations import project, iso
|
|
455
|
+
reader = getattr(self._backend, 'current_property_observations', None)
|
|
456
|
+
if reader is None:
|
|
457
|
+
raise NotImplementedError('typed_world_observations_require_sqlite')
|
|
458
|
+
moment = iso(as_of if as_of is not None else datetime.now(timezone.utc).isoformat())
|
|
459
|
+
rows = await reader(entity_id, property_key, subject_person_id=subject_person_id,
|
|
460
|
+
viewer_scope=viewer_scope, shareability=shareability, as_of=moment)
|
|
461
|
+
from .source_reports import validate_reports
|
|
462
|
+
rows = validate_reports(rows, source_ledger)
|
|
463
|
+
return project(rows, entity_id=entity_id, property_key=property_key,
|
|
464
|
+
subject_person_id=subject_person_id, viewer_scope=viewer_scope, shareability=shareability,
|
|
465
|
+
as_of=moment, coverage_limited=len(rows) >= 2001)
|
|
466
|
+
|
|
467
|
+
async def erase_property_evidence(self, evidence_refs, *, subject_person_id):
|
|
468
|
+
eraser = getattr(self._backend, 'erase_property_evidence', None)
|
|
469
|
+
if eraser is None:
|
|
470
|
+
raise NotImplementedError('typed_world_observations_require_sqlite')
|
|
471
|
+
removed = await eraser(evidence_refs, subject_person_id=subject_person_id)
|
|
472
|
+
if removed:
|
|
473
|
+
self._emit_change('property_evidence_invalidated', observation_ids=removed)
|
|
474
|
+
return removed
|
|
475
|
+
|
|
476
|
+
async def property_views(self, entity_ids, *, subject_person_id, viewer_scope, shareability,
|
|
477
|
+
as_of=None, source_ledger=None, limit=8):
|
|
478
|
+
"""Bounded scoped facts about already-relevant entities for turn context."""
|
|
479
|
+
keys = getattr(self._backend, 'property_keys', None)
|
|
480
|
+
if keys is None:
|
|
481
|
+
raise NotImplementedError('typed_world_observations_require_sqlite')
|
|
482
|
+
limit = max(1, min(int(limit), 32))
|
|
483
|
+
result = []
|
|
484
|
+
for entity_id in list(dict.fromkeys(entity_ids))[:8]:
|
|
485
|
+
properties = await keys(entity_id, subject_person_id=subject_person_id, viewer_scope=viewer_scope,
|
|
486
|
+
shareability=shareability, limit=limit)
|
|
487
|
+
for key in properties:
|
|
488
|
+
result.append(await self.property_state(entity_id, key, subject_person_id=subject_person_id,
|
|
489
|
+
viewer_scope=viewer_scope, shareability=shareability, as_of=as_of, source_ledger=source_ledger))
|
|
490
|
+
if len(result) >= limit:
|
|
491
|
+
return result
|
|
492
|
+
return result
|
|
493
|
+
|
|
494
|
+
async def add_observation(
|
|
495
|
+
self,
|
|
496
|
+
entity_id: Optional[str],
|
|
497
|
+
relationship_id: Optional[str],
|
|
498
|
+
observation: str,
|
|
499
|
+
source: str,
|
|
500
|
+
) -> str:
|
|
501
|
+
"""Record a raw observation string for an entity or relationship."""
|
|
502
|
+
return await self._backend.add_observation(
|
|
503
|
+
entity_id, relationship_id, observation, source
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# ── Maintenance ───────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
async def prune(self, ttl_days: Optional[int] = None,
|
|
509
|
+
max_confidence: Optional[float] = None) -> Dict[str, Any]:
|
|
510
|
+
"""Remove low-confidence entities that haven't been seen within the
|
|
511
|
+
TTL (config: low_confidence_entity_ttl_days / min_confidence_for_query
|
|
512
|
+
by default). High-confidence entities are never pruned regardless of
|
|
513
|
+
age. Returns {"status", "pruned", "cutoff", "max_confidence"};
|
|
514
|
+
backends without a prune primitive report skipped instead of lying."""
|
|
515
|
+
backend_prune = getattr(self._backend, "prune_entities", None)
|
|
516
|
+
if backend_prune is None:
|
|
517
|
+
return {"status": "skipped",
|
|
518
|
+
"reason": f"{type(self._backend).__name__} has no prune"}
|
|
519
|
+
ttl = int(ttl_days if ttl_days is not None
|
|
520
|
+
else self._config.low_confidence_entity_ttl_days)
|
|
521
|
+
ceiling = float(max_confidence if max_confidence is not None
|
|
522
|
+
else self._config.min_confidence_for_query)
|
|
523
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=ttl)
|
|
524
|
+
).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
525
|
+
pruned = await backend_prune(cutoff, ceiling)
|
|
526
|
+
if pruned:
|
|
527
|
+
logger.info("world model pruned %d stale low-confidence entities "
|
|
528
|
+
"(last_seen < %s, confidence < %.2f)",
|
|
529
|
+
pruned, cutoff, ceiling)
|
|
530
|
+
return {"status": "ok", "pruned": pruned, "cutoff": cutoff,
|
|
531
|
+
"max_confidence": ceiling}
|
|
532
|
+
|
|
533
|
+
# ── Stats ─────────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
async def get_stats(self) -> WorldModelStats:
|
|
536
|
+
"""Return summary statistics for the world model graph."""
|
|
537
|
+
raw = await self._backend.get_stats()
|
|
538
|
+
return WorldModelStats(
|
|
539
|
+
total_entities=raw["total_entities"],
|
|
540
|
+
entities_by_type=raw["entities_by_type"],
|
|
541
|
+
total_relationships=raw["total_relationships"],
|
|
542
|
+
active_relationships=raw["active_relationships"],
|
|
543
|
+
total_observations=raw["total_observations"],
|
|
544
|
+
merge_proposals_pending=raw["merge_proposals_pending"],
|
|
545
|
+
)
|