hyperagents 1.2.3rc5.post3__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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retrieval module for Civilization Knowledge Layer.
|
|
3
|
+
Handles RAG, context injection, and relevance ranking.
|
|
4
|
+
"""
|
|
5
|
+
from .civilization_rag import CivilizationRAG, RAGResult, RAGConfig
|
|
6
|
+
from .context_injector import ContextInjector, InjectionMode
|
|
7
|
+
from .relevance_ranker import RelevanceRanker, RankingResult
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CivilizationRAG",
|
|
11
|
+
"RAGResult",
|
|
12
|
+
"RAGConfig",
|
|
13
|
+
"ContextInjector",
|
|
14
|
+
"InjectionMode",
|
|
15
|
+
"RelevanceRanker",
|
|
16
|
+
"RankingResult",
|
|
17
|
+
]
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RAG (Retrieval Augmented Generation) for Civilization Knowledge Layer.
|
|
3
|
+
Retrieves and formats organizational knowledge for agent consumption.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from ..schema import CivilizationNode, NodeType
|
|
12
|
+
from ..store import CivilizationStore
|
|
13
|
+
from ..ingestion.embedder import CivilizationEmbedder
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RAGConfig:
|
|
20
|
+
"""Configuration for RAG retrieval."""
|
|
21
|
+
top_k: int = 5
|
|
22
|
+
min_relevance_score: float = 0.5
|
|
23
|
+
include_metadata: bool = True
|
|
24
|
+
max_context_tokens: int = 4000
|
|
25
|
+
node_type_filters: list[NodeType] | None = None
|
|
26
|
+
tag_filters: list[str] | None = None
|
|
27
|
+
rerank: bool = True
|
|
28
|
+
expand_related: bool = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RetrievedNode:
|
|
33
|
+
"""A node retrieved by RAG with relevance metadata."""
|
|
34
|
+
node: CivilizationNode
|
|
35
|
+
score: float
|
|
36
|
+
source: str = "vector" # "vector", "keyword", "related"
|
|
37
|
+
highlights: list[str] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class RAGResult:
|
|
42
|
+
"""Result of a RAG query."""
|
|
43
|
+
query: str
|
|
44
|
+
retrieved_nodes: list[RetrievedNode] = field(default_factory=list)
|
|
45
|
+
context: str = ""
|
|
46
|
+
metadata: dict = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def node_count(self) -> int:
|
|
50
|
+
return len(self.retrieved_nodes)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def has_results(self) -> bool:
|
|
54
|
+
return len(self.retrieved_nodes) > 0
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def top_node(self) -> CivilizationNode | None:
|
|
58
|
+
if self.retrieved_nodes:
|
|
59
|
+
return self.retrieved_nodes[0].node
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CivilizationRAG:
|
|
64
|
+
"""
|
|
65
|
+
RAG system for retrieving and formatting organizational knowledge.
|
|
66
|
+
Supports vector search, keyword fallback, and context expansion.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
org_id: str,
|
|
72
|
+
store: CivilizationStore | None = None,
|
|
73
|
+
embedder: CivilizationEmbedder | None = None,
|
|
74
|
+
config: RAGConfig | None = None,
|
|
75
|
+
):
|
|
76
|
+
self.org_id = org_id
|
|
77
|
+
self.store = store
|
|
78
|
+
self.embedder = embedder or CivilizationEmbedder()
|
|
79
|
+
self.config = config or RAGConfig()
|
|
80
|
+
self._cache: dict[str, list[RetrievedNode]] = {}
|
|
81
|
+
|
|
82
|
+
async def query(
|
|
83
|
+
self,
|
|
84
|
+
query: str,
|
|
85
|
+
config_override: RAGConfig | None = None,
|
|
86
|
+
) -> RAGResult:
|
|
87
|
+
"""
|
|
88
|
+
Query the knowledge base and retrieve relevant nodes.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
query: The search query
|
|
92
|
+
config_override: Optional config to override defaults
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
RAGResult with retrieved nodes and formatted context
|
|
96
|
+
"""
|
|
97
|
+
config = config_override or self.config
|
|
98
|
+
result = RAGResult(query=query)
|
|
99
|
+
|
|
100
|
+
# Generate query embedding
|
|
101
|
+
query_embedding = await self.embedder.embed(query)
|
|
102
|
+
|
|
103
|
+
# Vector search
|
|
104
|
+
if self.store:
|
|
105
|
+
vector_results = await self.store.search_by_embedding(
|
|
106
|
+
org_id=self.org_id,
|
|
107
|
+
embedding=query_embedding,
|
|
108
|
+
top_k=config.top_k * 2, # Over-fetch for reranking
|
|
109
|
+
node_types=config.node_type_filters,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
for row in vector_results:
|
|
113
|
+
score = row.get("score", 0.0)
|
|
114
|
+
if score >= config.min_relevance_score:
|
|
115
|
+
node = self.store._row_to_node(row)
|
|
116
|
+
result.retrieved_nodes.append(RetrievedNode(
|
|
117
|
+
node=node,
|
|
118
|
+
score=score,
|
|
119
|
+
source="vector",
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
# Sort by score
|
|
123
|
+
result.retrieved_nodes.sort(key=lambda x: x.score, reverse=True)
|
|
124
|
+
|
|
125
|
+
# Truncate to top_k
|
|
126
|
+
result.retrieved_nodes = result.retrieved_nodes[:config.top_k]
|
|
127
|
+
|
|
128
|
+
# Expand with related nodes if enabled
|
|
129
|
+
if config.expand_related and result.retrieved_nodes:
|
|
130
|
+
await self._expand_related(result, config)
|
|
131
|
+
|
|
132
|
+
# Generate context string
|
|
133
|
+
result.context = self._format_context(result.retrieved_nodes, config)
|
|
134
|
+
|
|
135
|
+
result.metadata = {
|
|
136
|
+
"total_retrieved": len(result.retrieved_nodes),
|
|
137
|
+
"config": {
|
|
138
|
+
"top_k": config.top_k,
|
|
139
|
+
"min_score": config.min_relevance_score,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
async def query_by_type(
|
|
146
|
+
self,
|
|
147
|
+
query: str,
|
|
148
|
+
node_type: NodeType,
|
|
149
|
+
top_k: int = 5,
|
|
150
|
+
) -> RAGResult:
|
|
151
|
+
"""Query for a specific node type."""
|
|
152
|
+
config = RAGConfig(
|
|
153
|
+
top_k=top_k,
|
|
154
|
+
node_type_filters=[node_type],
|
|
155
|
+
)
|
|
156
|
+
return await self.query(query, config)
|
|
157
|
+
|
|
158
|
+
async def get_sop_for_task(self, task_description: str) -> RAGResult:
|
|
159
|
+
"""Find relevant SOPs for a task."""
|
|
160
|
+
return await self.query_by_type(
|
|
161
|
+
query=f"SOP procedure for: {task_description}",
|
|
162
|
+
node_type=NodeType.SOP,
|
|
163
|
+
top_k=3,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
async def get_role_info(self, role_query: str) -> RAGResult:
|
|
167
|
+
"""Find role definitions."""
|
|
168
|
+
config = RAGConfig(
|
|
169
|
+
top_k=3,
|
|
170
|
+
node_type_filters=[NodeType.ROLE, NodeType.JOB_DESCRIPTION],
|
|
171
|
+
)
|
|
172
|
+
return await self.query(role_query, config)
|
|
173
|
+
|
|
174
|
+
async def get_runbook_for_incident(self, incident_description: str) -> RAGResult:
|
|
175
|
+
"""Find relevant runbooks for an incident."""
|
|
176
|
+
return await self.query_by_type(
|
|
177
|
+
query=f"Runbook for incident: {incident_description}",
|
|
178
|
+
node_type=NodeType.RUNBOOK,
|
|
179
|
+
top_k=3,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def _expand_related(self, result: RAGResult, config: RAGConfig) -> None:
|
|
183
|
+
"""Expand results with related nodes."""
|
|
184
|
+
if not self.store:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# Get IDs of related nodes from top results
|
|
188
|
+
related_ids: set[UUID] = set()
|
|
189
|
+
for rn in result.retrieved_nodes[:3]:
|
|
190
|
+
# Check for related_sops, related_roles, etc.
|
|
191
|
+
node_dict = rn.node.model_dump()
|
|
192
|
+
for key, value in node_dict.items():
|
|
193
|
+
if key.startswith("related_") and isinstance(value, list):
|
|
194
|
+
for item in value:
|
|
195
|
+
if isinstance(item, (UUID, str)):
|
|
196
|
+
try:
|
|
197
|
+
related_ids.add(UUID(str(item)))
|
|
198
|
+
except ValueError:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
# Fetch and add related nodes
|
|
202
|
+
existing_ids = {rn.node.id for rn in result.retrieved_nodes}
|
|
203
|
+
for rel_id in related_ids:
|
|
204
|
+
if rel_id not in existing_ids and len(result.retrieved_nodes) < config.top_k + 2:
|
|
205
|
+
related = await self.store.get(rel_id, self.org_id)
|
|
206
|
+
if related:
|
|
207
|
+
result.retrieved_nodes.append(RetrievedNode(
|
|
208
|
+
node=related,
|
|
209
|
+
score=0.5, # Default score for related
|
|
210
|
+
source="related",
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
def _format_context(
|
|
214
|
+
self,
|
|
215
|
+
nodes: list[RetrievedNode],
|
|
216
|
+
config: RAGConfig,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Format retrieved nodes into context string for LLM."""
|
|
219
|
+
if not nodes:
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
parts = []
|
|
223
|
+
token_estimate = 0
|
|
224
|
+
max_tokens = config.max_context_tokens
|
|
225
|
+
|
|
226
|
+
for i, rn in enumerate(nodes, 1):
|
|
227
|
+
node = rn.node
|
|
228
|
+
section = self._format_node(node, i, config.include_metadata)
|
|
229
|
+
|
|
230
|
+
# Rough token estimate (4 chars per token)
|
|
231
|
+
section_tokens = len(section) // 4
|
|
232
|
+
if token_estimate + section_tokens > max_tokens:
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
parts.append(section)
|
|
236
|
+
token_estimate += section_tokens
|
|
237
|
+
|
|
238
|
+
return "\n\n---\n\n".join(parts)
|
|
239
|
+
|
|
240
|
+
def _format_node(
|
|
241
|
+
self,
|
|
242
|
+
node: CivilizationNode,
|
|
243
|
+
index: int,
|
|
244
|
+
include_metadata: bool,
|
|
245
|
+
) -> str:
|
|
246
|
+
"""Format a single node for context."""
|
|
247
|
+
lines = [f"[{index}] {node.node_type.value.upper()}: {node.title}"]
|
|
248
|
+
|
|
249
|
+
if include_metadata:
|
|
250
|
+
if node.owner_id:
|
|
251
|
+
lines.append(f"Owner: {node.owner_id}")
|
|
252
|
+
if node.tags:
|
|
253
|
+
lines.append(f"Tags: {', '.join(node.tags)}")
|
|
254
|
+
|
|
255
|
+
# Type-specific formatting
|
|
256
|
+
node_dict = node.model_dump()
|
|
257
|
+
|
|
258
|
+
if node.node_type == NodeType.SOP:
|
|
259
|
+
if "purpose" in node_dict:
|
|
260
|
+
lines.append(f"Purpose: {node_dict['purpose']}")
|
|
261
|
+
if "steps" in node_dict:
|
|
262
|
+
lines.append("Steps:")
|
|
263
|
+
for step in node_dict["steps"][:5]:
|
|
264
|
+
lines.append(f" {step.get('step_number', '?')}. {step.get('title', 'Untitled')}")
|
|
265
|
+
if step.get("description"):
|
|
266
|
+
lines.append(f" {step['description'][:150]}...")
|
|
267
|
+
|
|
268
|
+
elif node.node_type == NodeType.ROLE:
|
|
269
|
+
if "role_title" in node_dict:
|
|
270
|
+
lines.append(f"Title: {node_dict['role_title']}")
|
|
271
|
+
if "responsibilities" in node_dict:
|
|
272
|
+
lines.append("Responsibilities:")
|
|
273
|
+
for r in node_dict["responsibilities"][:5]:
|
|
274
|
+
lines.append(f" - {r}")
|
|
275
|
+
|
|
276
|
+
elif node.node_type == NodeType.RUNBOOK:
|
|
277
|
+
if "scenario" in node_dict:
|
|
278
|
+
lines.append(f"Scenario: {node_dict['scenario']}")
|
|
279
|
+
if "steps" in node_dict:
|
|
280
|
+
lines.append("Steps:")
|
|
281
|
+
for step in node_dict["steps"][:5]:
|
|
282
|
+
lines.append(f" {step.get('step_number', '?')}. {step.get('action', 'Action')}")
|
|
283
|
+
|
|
284
|
+
elif node.node_type == NodeType.CHECKLIST:
|
|
285
|
+
if "items" in node_dict:
|
|
286
|
+
lines.append("Items:")
|
|
287
|
+
for item in node_dict["items"][:8]:
|
|
288
|
+
lines.append(f" [ ] {item.get('description', '')}")
|
|
289
|
+
|
|
290
|
+
return "\n".join(lines)
|
|
291
|
+
|
|
292
|
+
async def hybrid_search(
|
|
293
|
+
self,
|
|
294
|
+
query: str,
|
|
295
|
+
keyword_weight: float = 0.3,
|
|
296
|
+
) -> RAGResult:
|
|
297
|
+
"""
|
|
298
|
+
Perform hybrid search combining vector and keyword matching.
|
|
299
|
+
"""
|
|
300
|
+
# Vector search
|
|
301
|
+
vector_result = await self.query(query)
|
|
302
|
+
|
|
303
|
+
# For keyword search, we'd need text search capability in the store
|
|
304
|
+
# This is a placeholder that could be enhanced
|
|
305
|
+
result = RAGResult(
|
|
306
|
+
query=query,
|
|
307
|
+
retrieved_nodes=vector_result.retrieved_nodes,
|
|
308
|
+
metadata={"search_type": "hybrid"},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
result.context = self._format_context(result.retrieved_nodes, self.config)
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
def clear_cache(self) -> None:
|
|
315
|
+
"""Clear the query cache."""
|
|
316
|
+
self._cache.clear()
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context injection for agent prompts.
|
|
3
|
+
Formats and injects organizational knowledge into agent context.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..schema import CivilizationNode, NodeType
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InjectionMode(str, Enum):
|
|
17
|
+
FULL = "full" # Include complete node content
|
|
18
|
+
SUMMARY = "summary" # Include only summaries
|
|
19
|
+
REFERENCE = "reference" # Include only titles and IDs
|
|
20
|
+
STRUCTURED = "structured" # Include structured data format
|
|
21
|
+
MINIMAL = "minimal" # Bare minimum context
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class InjectionConfig:
|
|
26
|
+
"""Configuration for context injection."""
|
|
27
|
+
mode: InjectionMode = InjectionMode.SUMMARY
|
|
28
|
+
max_tokens: int = 2000
|
|
29
|
+
include_metadata: bool = True
|
|
30
|
+
include_relationships: bool = True
|
|
31
|
+
priority_types: list[NodeType] | None = None
|
|
32
|
+
format: str = "markdown" # "markdown", "xml", "json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class InjectionResult:
|
|
37
|
+
"""Result of context injection."""
|
|
38
|
+
context: str
|
|
39
|
+
nodes_included: int
|
|
40
|
+
truncated: bool = False
|
|
41
|
+
token_estimate: int = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ContextInjector:
|
|
45
|
+
"""
|
|
46
|
+
Injects civilization knowledge into agent prompts.
|
|
47
|
+
Handles formatting, token budgeting, and prioritization.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
PRIORITY_ORDER = [
|
|
51
|
+
NodeType.SOP,
|
|
52
|
+
NodeType.RUNBOOK,
|
|
53
|
+
NodeType.CHECKLIST,
|
|
54
|
+
NodeType.ROLE,
|
|
55
|
+
NodeType.WORKFLOW,
|
|
56
|
+
NodeType.POLICY,
|
|
57
|
+
NodeType.JOB_DESCRIPTION,
|
|
58
|
+
NodeType.CLIENT_PROFILE,
|
|
59
|
+
NodeType.KNOWLEDGE_ARTICLE,
|
|
60
|
+
NodeType.ORG_CHART,
|
|
61
|
+
NodeType.PERSON,
|
|
62
|
+
NodeType.PERSONAL_ROUTINE,
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def __init__(self, config: InjectionConfig | None = None):
|
|
66
|
+
self.config = config or InjectionConfig()
|
|
67
|
+
|
|
68
|
+
def inject(
|
|
69
|
+
self,
|
|
70
|
+
nodes: list[CivilizationNode],
|
|
71
|
+
config_override: InjectionConfig | None = None,
|
|
72
|
+
) -> InjectionResult:
|
|
73
|
+
"""
|
|
74
|
+
Generate injection context from nodes.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
nodes: Nodes to include in context
|
|
78
|
+
config_override: Optional config override
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
InjectionResult with formatted context
|
|
82
|
+
"""
|
|
83
|
+
config = config_override or self.config
|
|
84
|
+
result = InjectionResult(context="", nodes_included=0)
|
|
85
|
+
|
|
86
|
+
if not nodes:
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
# Sort by priority
|
|
90
|
+
sorted_nodes = self._prioritize_nodes(nodes, config)
|
|
91
|
+
|
|
92
|
+
# Format based on mode
|
|
93
|
+
formatter = self._get_formatter(config.format)
|
|
94
|
+
sections = []
|
|
95
|
+
total_tokens = 0
|
|
96
|
+
|
|
97
|
+
for node in sorted_nodes:
|
|
98
|
+
section = self._format_node(node, config)
|
|
99
|
+
section_tokens = len(section) // 4 # Rough estimate
|
|
100
|
+
|
|
101
|
+
if total_tokens + section_tokens > config.max_tokens:
|
|
102
|
+
result.truncated = True
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
sections.append(section)
|
|
106
|
+
total_tokens += section_tokens
|
|
107
|
+
result.nodes_included += 1
|
|
108
|
+
|
|
109
|
+
result.context = formatter(sections, config)
|
|
110
|
+
result.token_estimate = total_tokens
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
def _prioritize_nodes(
|
|
115
|
+
self,
|
|
116
|
+
nodes: list[CivilizationNode],
|
|
117
|
+
config: InjectionConfig,
|
|
118
|
+
) -> list[CivilizationNode]:
|
|
119
|
+
"""Sort nodes by priority."""
|
|
120
|
+
priority_types = config.priority_types or self.PRIORITY_ORDER
|
|
121
|
+
|
|
122
|
+
def sort_key(node: CivilizationNode) -> tuple:
|
|
123
|
+
try:
|
|
124
|
+
type_priority = priority_types.index(node.node_type)
|
|
125
|
+
except ValueError:
|
|
126
|
+
type_priority = len(priority_types)
|
|
127
|
+
return (type_priority, -node.updated_at.timestamp())
|
|
128
|
+
|
|
129
|
+
return sorted(nodes, key=sort_key)
|
|
130
|
+
|
|
131
|
+
def _format_node(
|
|
132
|
+
self,
|
|
133
|
+
node: CivilizationNode,
|
|
134
|
+
config: InjectionConfig,
|
|
135
|
+
) -> str:
|
|
136
|
+
"""Format a single node based on injection mode."""
|
|
137
|
+
if config.mode == InjectionMode.REFERENCE:
|
|
138
|
+
return self._format_reference(node)
|
|
139
|
+
elif config.mode == InjectionMode.MINIMAL:
|
|
140
|
+
return self._format_minimal(node)
|
|
141
|
+
elif config.mode == InjectionMode.SUMMARY:
|
|
142
|
+
return self._format_summary(node, config)
|
|
143
|
+
elif config.mode == InjectionMode.STRUCTURED:
|
|
144
|
+
return self._format_structured(node, config)
|
|
145
|
+
else: # FULL
|
|
146
|
+
return self._format_full(node, config)
|
|
147
|
+
|
|
148
|
+
def _format_reference(self, node: CivilizationNode) -> str:
|
|
149
|
+
"""Format as reference only."""
|
|
150
|
+
return f"- [{node.node_type.value}] {node.title} (ID: {node.id})"
|
|
151
|
+
|
|
152
|
+
def _format_minimal(self, node: CivilizationNode) -> str:
|
|
153
|
+
"""Format with minimal information."""
|
|
154
|
+
return f"{node.node_type.value.upper()}: {node.title}"
|
|
155
|
+
|
|
156
|
+
def _format_summary(
|
|
157
|
+
self,
|
|
158
|
+
node: CivilizationNode,
|
|
159
|
+
config: InjectionConfig,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Format as summary."""
|
|
162
|
+
lines = [f"**{node.node_type.value.upper()}**: {node.title}"]
|
|
163
|
+
|
|
164
|
+
node_dict = node.model_dump()
|
|
165
|
+
|
|
166
|
+
# Add key fields based on node type
|
|
167
|
+
if node.node_type == NodeType.SOP and "purpose" in node_dict:
|
|
168
|
+
lines.append(f"Purpose: {node_dict['purpose']}")
|
|
169
|
+
if "steps" in node_dict:
|
|
170
|
+
step_count = len(node_dict["steps"])
|
|
171
|
+
lines.append(f"Steps: {step_count} total")
|
|
172
|
+
|
|
173
|
+
elif node.node_type == NodeType.ROLE:
|
|
174
|
+
if "role_title" in node_dict:
|
|
175
|
+
lines.append(f"Role: {node_dict['role_title']}")
|
|
176
|
+
if "responsibilities" in node_dict:
|
|
177
|
+
lines.append(f"Key responsibilities: {len(node_dict['responsibilities'])} defined")
|
|
178
|
+
|
|
179
|
+
elif node.node_type == NodeType.RUNBOOK:
|
|
180
|
+
if "scenario" in node_dict:
|
|
181
|
+
lines.append(f"Scenario: {node_dict['scenario']}")
|
|
182
|
+
if "severity" in node_dict:
|
|
183
|
+
lines.append(f"Severity: {node_dict['severity']}")
|
|
184
|
+
|
|
185
|
+
if config.include_metadata and node.tags:
|
|
186
|
+
lines.append(f"Tags: {', '.join(node.tags[:5])}")
|
|
187
|
+
|
|
188
|
+
return "\n".join(lines)
|
|
189
|
+
|
|
190
|
+
def _format_structured(
|
|
191
|
+
self,
|
|
192
|
+
node: CivilizationNode,
|
|
193
|
+
config: InjectionConfig,
|
|
194
|
+
) -> str:
|
|
195
|
+
"""Format as structured data."""
|
|
196
|
+
import json
|
|
197
|
+
data = {
|
|
198
|
+
"type": node.node_type.value,
|
|
199
|
+
"title": node.title,
|
|
200
|
+
"id": str(node.id),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
node_dict = node.model_dump()
|
|
204
|
+
|
|
205
|
+
# Include type-specific fields
|
|
206
|
+
if node.node_type == NodeType.SOP:
|
|
207
|
+
data["purpose"] = node_dict.get("purpose", "")
|
|
208
|
+
data["step_count"] = len(node_dict.get("steps", []))
|
|
209
|
+
data["roles"] = node_dict.get("roles_involved", [])
|
|
210
|
+
|
|
211
|
+
elif node.node_type == NodeType.ROLE:
|
|
212
|
+
data["role_title"] = node_dict.get("role_title", "")
|
|
213
|
+
data["department"] = node_dict.get("department", "")
|
|
214
|
+
|
|
215
|
+
if config.include_metadata:
|
|
216
|
+
data["tags"] = node.tags
|
|
217
|
+
data["owner"] = node.owner_id
|
|
218
|
+
|
|
219
|
+
return json.dumps(data, indent=2)
|
|
220
|
+
|
|
221
|
+
def _format_full(
|
|
222
|
+
self,
|
|
223
|
+
node: CivilizationNode,
|
|
224
|
+
config: InjectionConfig,
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Format with full content."""
|
|
227
|
+
lines = [
|
|
228
|
+
f"## {node.node_type.value.upper()}: {node.title}",
|
|
229
|
+
"",
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
node_dict = node.model_dump()
|
|
233
|
+
|
|
234
|
+
# Full content varies by type
|
|
235
|
+
if node.node_type == NodeType.SOP:
|
|
236
|
+
lines.append(f"**Purpose:** {node_dict.get('purpose', 'N/A')}")
|
|
237
|
+
lines.append(f"**Scope:** {node_dict.get('scope', 'N/A')}")
|
|
238
|
+
lines.append("")
|
|
239
|
+
|
|
240
|
+
if "steps" in node_dict:
|
|
241
|
+
lines.append("### Steps")
|
|
242
|
+
for step in node_dict["steps"]:
|
|
243
|
+
lines.append(f"{step.get('step_number', '?')}. **{step.get('title', 'Step')}**")
|
|
244
|
+
lines.append(f" {step.get('description', '')}")
|
|
245
|
+
if step.get("responsible_role"):
|
|
246
|
+
lines.append(f" - Role: {step['responsible_role']}")
|
|
247
|
+
if step.get("tools_required"):
|
|
248
|
+
lines.append(f" - Tools: {', '.join(step['tools_required'])}")
|
|
249
|
+
lines.append("")
|
|
250
|
+
|
|
251
|
+
elif node.node_type == NodeType.ROLE:
|
|
252
|
+
lines.append(f"**Title:** {node_dict.get('role_title', 'N/A')}")
|
|
253
|
+
lines.append(f"**Department:** {node_dict.get('department', 'N/A')}")
|
|
254
|
+
lines.append("")
|
|
255
|
+
|
|
256
|
+
if "responsibilities" in node_dict:
|
|
257
|
+
lines.append("### Responsibilities")
|
|
258
|
+
for r in node_dict["responsibilities"]:
|
|
259
|
+
lines.append(f"- {r}")
|
|
260
|
+
|
|
261
|
+
if "accountabilities" in node_dict:
|
|
262
|
+
lines.append("")
|
|
263
|
+
lines.append("### Accountabilities")
|
|
264
|
+
for a in node_dict["accountabilities"]:
|
|
265
|
+
lines.append(f"- {a}")
|
|
266
|
+
|
|
267
|
+
elif node.node_type == NodeType.CHECKLIST:
|
|
268
|
+
lines.append(f"**Purpose:** {node_dict.get('purpose', 'N/A')}")
|
|
269
|
+
lines.append("")
|
|
270
|
+
|
|
271
|
+
if "items" in node_dict:
|
|
272
|
+
lines.append("### Items")
|
|
273
|
+
for item in node_dict["items"]:
|
|
274
|
+
req = "[Required]" if item.get("required") else "[Optional]"
|
|
275
|
+
lines.append(f"- {req} {item.get('description', '')}")
|
|
276
|
+
|
|
277
|
+
if config.include_metadata:
|
|
278
|
+
lines.append("")
|
|
279
|
+
lines.append("---")
|
|
280
|
+
lines.append(f"*Owner: {node.owner_id or 'Unassigned'}*")
|
|
281
|
+
lines.append(f"*Tags: {', '.join(node.tags) if node.tags else 'None'}*")
|
|
282
|
+
lines.append(f"*Last updated: {node.updated_at.isoformat()}*")
|
|
283
|
+
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
def _get_formatter(self, format_type: str):
|
|
287
|
+
"""Get formatter function for output format."""
|
|
288
|
+
if format_type == "xml":
|
|
289
|
+
return self._format_as_xml
|
|
290
|
+
elif format_type == "json":
|
|
291
|
+
return self._format_as_json
|
|
292
|
+
else: # markdown
|
|
293
|
+
return self._format_as_markdown
|
|
294
|
+
|
|
295
|
+
def _format_as_markdown(
|
|
296
|
+
self,
|
|
297
|
+
sections: list[str],
|
|
298
|
+
config: InjectionConfig,
|
|
299
|
+
) -> str:
|
|
300
|
+
"""Format sections as markdown."""
|
|
301
|
+
header = "# Organizational Knowledge Context\n\n"
|
|
302
|
+
return header + "\n\n---\n\n".join(sections)
|
|
303
|
+
|
|
304
|
+
def _format_as_xml(
|
|
305
|
+
self,
|
|
306
|
+
sections: list[str],
|
|
307
|
+
config: InjectionConfig,
|
|
308
|
+
) -> str:
|
|
309
|
+
"""Format sections as XML."""
|
|
310
|
+
import html
|
|
311
|
+
items = "\n".join(
|
|
312
|
+
f"<knowledge_item>\n{html.escape(s)}\n</knowledge_item>"
|
|
313
|
+
for s in sections
|
|
314
|
+
)
|
|
315
|
+
return f"<organizational_knowledge>\n{items}\n</organizational_knowledge>"
|
|
316
|
+
|
|
317
|
+
def _format_as_json(
|
|
318
|
+
self,
|
|
319
|
+
sections: list[str],
|
|
320
|
+
config: InjectionConfig,
|
|
321
|
+
) -> str:
|
|
322
|
+
"""Format sections as JSON array."""
|
|
323
|
+
import json
|
|
324
|
+
return json.dumps({"knowledge_items": sections}, indent=2)
|
|
325
|
+
|
|
326
|
+
def build_system_prompt_section(
|
|
327
|
+
self,
|
|
328
|
+
nodes: list[CivilizationNode],
|
|
329
|
+
section_name: str = "ORGANIZATIONAL_CONTEXT",
|
|
330
|
+
) -> str:
|
|
331
|
+
"""
|
|
332
|
+
Build a section suitable for system prompts.
|
|
333
|
+
"""
|
|
334
|
+
result = self.inject(nodes)
|
|
335
|
+
if not result.context:
|
|
336
|
+
return ""
|
|
337
|
+
|
|
338
|
+
return f"""
|
|
339
|
+
<{section_name}>
|
|
340
|
+
{result.context}
|
|
341
|
+
</{section_name}>
|
|
342
|
+
"""
|