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,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive interviewer for knowledge elicitation.
|
|
3
|
+
Guides users through structured interviews to capture organizational knowledge.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
from uuid import UUID, uuid4
|
|
12
|
+
|
|
13
|
+
from ..schema import NodeType, CivilizationNode
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InterviewState(str, Enum):
|
|
19
|
+
NOT_STARTED = "not_started"
|
|
20
|
+
IN_PROGRESS = "in_progress"
|
|
21
|
+
PAUSED = "paused"
|
|
22
|
+
COMPLETED = "completed"
|
|
23
|
+
ABANDONED = "abandoned"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class InterviewResponse:
|
|
28
|
+
"""A single response in an interview."""
|
|
29
|
+
question_id: str
|
|
30
|
+
question_text: str
|
|
31
|
+
response_text: str
|
|
32
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
33
|
+
confidence: float | None = None
|
|
34
|
+
follow_up_needed: bool = False
|
|
35
|
+
extracted_entities: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class InterviewSession:
|
|
40
|
+
"""Represents an interview session."""
|
|
41
|
+
id: UUID = field(default_factory=uuid4)
|
|
42
|
+
org_id: str = ""
|
|
43
|
+
target_node_type: NodeType = NodeType.SOP
|
|
44
|
+
subject: str = ""
|
|
45
|
+
interviewer_id: str | None = None
|
|
46
|
+
interviewee_id: str | None = None
|
|
47
|
+
state: InterviewState = InterviewState.NOT_STARTED
|
|
48
|
+
responses: list[InterviewResponse] = field(default_factory=list)
|
|
49
|
+
current_question_index: int = 0
|
|
50
|
+
total_questions: int = 0
|
|
51
|
+
started_at: datetime | None = None
|
|
52
|
+
completed_at: datetime | None = None
|
|
53
|
+
metadata: dict = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def progress(self) -> float:
|
|
57
|
+
"""Return progress as percentage (0-100)."""
|
|
58
|
+
if self.total_questions == 0:
|
|
59
|
+
return 0.0
|
|
60
|
+
return (len(self.responses) / self.total_questions) * 100
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def duration_minutes(self) -> float | None:
|
|
64
|
+
"""Return interview duration in minutes."""
|
|
65
|
+
if not self.started_at:
|
|
66
|
+
return None
|
|
67
|
+
end = self.completed_at or datetime.utcnow()
|
|
68
|
+
return (end - self.started_at).total_seconds() / 60
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InterviewTemplate:
|
|
72
|
+
"""Template for a specific type of interview."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, node_type: NodeType, questions: list[dict]):
|
|
75
|
+
self.node_type = node_type
|
|
76
|
+
self.questions = questions
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def for_sop(cls) -> "InterviewTemplate":
|
|
80
|
+
"""Create template for SOP interviews."""
|
|
81
|
+
return cls(NodeType.SOP, [
|
|
82
|
+
{"id": "sop_name", "text": "What is the name of this procedure?", "required": True},
|
|
83
|
+
{"id": "sop_purpose", "text": "What is the purpose of this SOP? What problem does it solve?", "required": True},
|
|
84
|
+
{"id": "sop_scope", "text": "What is the scope? What does this SOP cover and not cover?", "required": True},
|
|
85
|
+
{"id": "sop_trigger", "text": "What triggers this procedure? When should someone start it?", "required": True},
|
|
86
|
+
{"id": "sop_roles", "text": "Who is involved in this procedure? What roles are needed?", "required": True},
|
|
87
|
+
{"id": "sop_steps", "text": "Walk me through the steps. What happens first, second, etc.?", "required": True},
|
|
88
|
+
{"id": "sop_tools", "text": "What tools or systems are used in this procedure?", "required": False},
|
|
89
|
+
{"id": "sop_duration", "text": "How long does this procedure typically take?", "required": False},
|
|
90
|
+
{"id": "sop_failure", "text": "What happens if something goes wrong? How do you handle failures?", "required": False},
|
|
91
|
+
{"id": "sop_success", "text": "How do you know the procedure was successful? What are the success criteria?", "required": True},
|
|
92
|
+
{"id": "sop_exceptions", "text": "Are there any exceptions or edge cases to be aware of?", "required": False},
|
|
93
|
+
{"id": "sop_related", "text": "Are there other procedures that relate to this one?", "required": False},
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def for_role(cls) -> "InterviewTemplate":
|
|
98
|
+
"""Create template for role interviews."""
|
|
99
|
+
return cls(NodeType.ROLE, [
|
|
100
|
+
{"id": "role_title", "text": "What is the title of this role?", "required": True},
|
|
101
|
+
{"id": "role_department", "text": "Which department or team does this role belong to?", "required": True},
|
|
102
|
+
{"id": "role_mission", "text": "What is the primary mission or purpose of this role?", "required": True},
|
|
103
|
+
{"id": "role_responsibilities", "text": "What are the key responsibilities of this role?", "required": True},
|
|
104
|
+
{"id": "role_accountabilities", "text": "What outcomes is this role accountable for?", "required": True},
|
|
105
|
+
{"id": "role_authority", "text": "What decisions can this role make independently?", "required": True},
|
|
106
|
+
{"id": "role_escalation", "text": "Who does this role escalate issues to?", "required": True},
|
|
107
|
+
{"id": "role_interfaces", "text": "Which other roles or teams does this role regularly interact with?", "required": True},
|
|
108
|
+
{"id": "role_tools", "text": "What tools and systems does this role use daily?", "required": False},
|
|
109
|
+
{"id": "role_sops", "text": "What SOPs or procedures does this role own or follow?", "required": False},
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def for_checklist(cls) -> "InterviewTemplate":
|
|
114
|
+
"""Create template for checklist interviews."""
|
|
115
|
+
return cls(NodeType.CHECKLIST, [
|
|
116
|
+
{"id": "cl_name", "text": "What is this checklist called?", "required": True},
|
|
117
|
+
{"id": "cl_purpose", "text": "What is the purpose of this checklist?", "required": True},
|
|
118
|
+
{"id": "cl_trigger", "text": "When should this checklist be used?", "required": True},
|
|
119
|
+
{"id": "cl_frequency", "text": "How often is this checklist used? (daily, weekly, per-event, etc.)", "required": False},
|
|
120
|
+
{"id": "cl_items", "text": "Walk me through each item on the checklist.", "required": True},
|
|
121
|
+
{"id": "cl_verification", "text": "How do you verify each item is complete?", "required": False},
|
|
122
|
+
{"id": "cl_responsible", "text": "Who is responsible for completing this checklist?", "required": True},
|
|
123
|
+
{"id": "cl_related_sop", "text": "Is this checklist part of a larger SOP?", "required": False},
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_template(cls, node_type: NodeType) -> "InterviewTemplate":
|
|
128
|
+
"""Get the appropriate template for a node type."""
|
|
129
|
+
templates = {
|
|
130
|
+
NodeType.SOP: cls.for_sop,
|
|
131
|
+
NodeType.ROLE: cls.for_role,
|
|
132
|
+
NodeType.CHECKLIST: cls.for_checklist,
|
|
133
|
+
}
|
|
134
|
+
factory = templates.get(node_type, cls.for_sop)
|
|
135
|
+
return factory()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Interviewer:
|
|
139
|
+
"""
|
|
140
|
+
Conducts structured interviews to elicit organizational knowledge.
|
|
141
|
+
Supports asynchronous, multi-session interviews.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
org_id: str,
|
|
147
|
+
store=None, # CivilizationStore
|
|
148
|
+
llm_callback: Callable[[str], str] | None = None,
|
|
149
|
+
):
|
|
150
|
+
self.org_id = org_id
|
|
151
|
+
self.store = store
|
|
152
|
+
self.llm_callback = llm_callback
|
|
153
|
+
self.sessions: dict[UUID, InterviewSession] = {}
|
|
154
|
+
|
|
155
|
+
def start_session(
|
|
156
|
+
self,
|
|
157
|
+
target_node_type: NodeType,
|
|
158
|
+
subject: str,
|
|
159
|
+
interviewee_id: str | None = None,
|
|
160
|
+
) -> InterviewSession:
|
|
161
|
+
"""Start a new interview session."""
|
|
162
|
+
template = InterviewTemplate.get_template(target_node_type)
|
|
163
|
+
|
|
164
|
+
session = InterviewSession(
|
|
165
|
+
org_id=self.org_id,
|
|
166
|
+
target_node_type=target_node_type,
|
|
167
|
+
subject=subject,
|
|
168
|
+
interviewee_id=interviewee_id,
|
|
169
|
+
state=InterviewState.IN_PROGRESS,
|
|
170
|
+
total_questions=len(template.questions),
|
|
171
|
+
started_at=datetime.utcnow(),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.sessions[session.id] = session
|
|
175
|
+
return session
|
|
176
|
+
|
|
177
|
+
def get_current_question(self, session_id: UUID) -> dict | None:
|
|
178
|
+
"""Get the current question for a session."""
|
|
179
|
+
session = self.sessions.get(session_id)
|
|
180
|
+
if not session or session.state != InterviewState.IN_PROGRESS:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
template = InterviewTemplate.get_template(session.target_node_type)
|
|
184
|
+
if session.current_question_index >= len(template.questions):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return template.questions[session.current_question_index]
|
|
188
|
+
|
|
189
|
+
def submit_response(
|
|
190
|
+
self,
|
|
191
|
+
session_id: UUID,
|
|
192
|
+
response_text: str,
|
|
193
|
+
confidence: float | None = None,
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
Submit a response to the current question.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dict with: next_question, is_complete, follow_up
|
|
200
|
+
"""
|
|
201
|
+
session = self.sessions.get(session_id)
|
|
202
|
+
if not session or session.state != InterviewState.IN_PROGRESS:
|
|
203
|
+
return {"error": "Invalid session or session not in progress"}
|
|
204
|
+
|
|
205
|
+
template = InterviewTemplate.get_template(session.target_node_type)
|
|
206
|
+
if session.current_question_index >= len(template.questions):
|
|
207
|
+
return {"error": "No more questions"}
|
|
208
|
+
|
|
209
|
+
current_q = template.questions[session.current_question_index]
|
|
210
|
+
|
|
211
|
+
# Record response
|
|
212
|
+
response = InterviewResponse(
|
|
213
|
+
question_id=current_q["id"],
|
|
214
|
+
question_text=current_q["text"],
|
|
215
|
+
response_text=response_text,
|
|
216
|
+
confidence=confidence,
|
|
217
|
+
)
|
|
218
|
+
session.responses.append(response)
|
|
219
|
+
session.current_question_index += 1
|
|
220
|
+
|
|
221
|
+
# Check if interview is complete
|
|
222
|
+
if session.current_question_index >= len(template.questions):
|
|
223
|
+
session.state = InterviewState.COMPLETED
|
|
224
|
+
session.completed_at = datetime.utcnow()
|
|
225
|
+
return {
|
|
226
|
+
"is_complete": True,
|
|
227
|
+
"next_question": None,
|
|
228
|
+
"progress": 100.0,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
next_q = template.questions[session.current_question_index]
|
|
232
|
+
return {
|
|
233
|
+
"is_complete": False,
|
|
234
|
+
"next_question": next_q,
|
|
235
|
+
"progress": session.progress,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def pause_session(self, session_id: UUID) -> bool:
|
|
239
|
+
"""Pause an interview session."""
|
|
240
|
+
session = self.sessions.get(session_id)
|
|
241
|
+
if session and session.state == InterviewState.IN_PROGRESS:
|
|
242
|
+
session.state = InterviewState.PAUSED
|
|
243
|
+
return True
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
def resume_session(self, session_id: UUID) -> InterviewSession | None:
|
|
247
|
+
"""Resume a paused interview session."""
|
|
248
|
+
session = self.sessions.get(session_id)
|
|
249
|
+
if session and session.state == InterviewState.PAUSED:
|
|
250
|
+
session.state = InterviewState.IN_PROGRESS
|
|
251
|
+
return session
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def abandon_session(self, session_id: UUID) -> bool:
|
|
255
|
+
"""Abandon an interview session."""
|
|
256
|
+
session = self.sessions.get(session_id)
|
|
257
|
+
if session:
|
|
258
|
+
session.state = InterviewState.ABANDONED
|
|
259
|
+
return True
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def get_session(self, session_id: UUID) -> InterviewSession | None:
|
|
263
|
+
"""Get a session by ID."""
|
|
264
|
+
return self.sessions.get(session_id)
|
|
265
|
+
|
|
266
|
+
def compile_to_node(self, session_id: UUID) -> CivilizationNode | None:
|
|
267
|
+
"""
|
|
268
|
+
Compile interview responses into a CivilizationNode.
|
|
269
|
+
Only works for completed sessions.
|
|
270
|
+
"""
|
|
271
|
+
session = self.sessions.get(session_id)
|
|
272
|
+
if not session or session.state != InterviewState.COMPLETED:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
# Build response lookup
|
|
276
|
+
responses = {r.question_id: r.response_text for r in session.responses}
|
|
277
|
+
|
|
278
|
+
if session.target_node_type == NodeType.SOP:
|
|
279
|
+
from ..schema import SOP, SOPStep
|
|
280
|
+
return SOP(
|
|
281
|
+
org_id=session.org_id,
|
|
282
|
+
title=responses.get("sop_name", session.subject),
|
|
283
|
+
purpose=responses.get("sop_purpose", ""),
|
|
284
|
+
scope=responses.get("sop_scope", ""),
|
|
285
|
+
triggers=[responses.get("sop_trigger", "")] if responses.get("sop_trigger") else [],
|
|
286
|
+
steps=self._parse_steps(responses.get("sop_steps", "")),
|
|
287
|
+
roles_involved=self._parse_list(responses.get("sop_roles", "")),
|
|
288
|
+
tools_required=self._parse_list(responses.get("sop_tools", "")),
|
|
289
|
+
success_criteria=self._parse_list(responses.get("sop_success", "")),
|
|
290
|
+
metadata={"source": "interview", "session_id": str(session.id)},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Add more node type compilations as needed
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def _parse_steps(self, text: str) -> list:
|
|
297
|
+
"""Parse step descriptions into SOPStep objects."""
|
|
298
|
+
from ..schema import SOPStep
|
|
299
|
+
if not text:
|
|
300
|
+
return []
|
|
301
|
+
|
|
302
|
+
# Simple parsing: split by numbered items or newlines
|
|
303
|
+
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
|
304
|
+
steps = []
|
|
305
|
+
for i, line in enumerate(lines, 1):
|
|
306
|
+
# Remove leading numbers/bullets
|
|
307
|
+
import re
|
|
308
|
+
cleaned = re.sub(r"^[\d\.\)\-\*]+\s*", "", line)
|
|
309
|
+
if cleaned:
|
|
310
|
+
steps.append(SOPStep(
|
|
311
|
+
step_number=i,
|
|
312
|
+
title=f"Step {i}",
|
|
313
|
+
description=cleaned,
|
|
314
|
+
))
|
|
315
|
+
return steps if steps else [SOPStep(step_number=1, title="Step 1", description=text)]
|
|
316
|
+
|
|
317
|
+
def _parse_list(self, text: str) -> list[str]:
|
|
318
|
+
"""Parse comma or newline separated list."""
|
|
319
|
+
if not text:
|
|
320
|
+
return []
|
|
321
|
+
import re
|
|
322
|
+
items = re.split(r"[,\n]", text)
|
|
323
|
+
return [item.strip() for item in items if item.strip()]
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic question generation for knowledge elicitation.
|
|
3
|
+
Generates contextual follow-up questions based on responses and gaps.
|
|
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, Callable
|
|
10
|
+
from uuid import UUID, uuid4
|
|
11
|
+
|
|
12
|
+
from ..schema import NodeType, CivilizationNode
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QuestionType(str, Enum):
|
|
18
|
+
OPEN = "open" # Open-ended questions
|
|
19
|
+
CLOSED = "closed" # Yes/No questions
|
|
20
|
+
MULTIPLE_CHOICE = "multiple_choice"
|
|
21
|
+
SCALE = "scale" # 1-5 or 1-10 ratings
|
|
22
|
+
LIST = "list" # Ask for a list of items
|
|
23
|
+
CLARIFICATION = "clarification" # Follow-up for unclear answers
|
|
24
|
+
CONFIRMATION = "confirmation" # Verify understanding
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QuestionPriority(str, Enum):
|
|
28
|
+
REQUIRED = "required"
|
|
29
|
+
RECOMMENDED = "recommended"
|
|
30
|
+
OPTIONAL = "optional"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Question:
|
|
35
|
+
"""Represents a question to ask during knowledge elicitation."""
|
|
36
|
+
id: str
|
|
37
|
+
text: str
|
|
38
|
+
question_type: QuestionType = QuestionType.OPEN
|
|
39
|
+
priority: QuestionPriority = QuestionPriority.REQUIRED
|
|
40
|
+
context: str | None = None
|
|
41
|
+
options: list[str] = field(default_factory=list) # For multiple choice
|
|
42
|
+
follow_up_for: str | None = None # ID of question this follows up on
|
|
43
|
+
validation_hint: str | None = None
|
|
44
|
+
examples: list[str] = field(default_factory=list)
|
|
45
|
+
metadata: dict = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class QuestionContext:
|
|
50
|
+
"""Context for generating questions."""
|
|
51
|
+
target_node_type: NodeType
|
|
52
|
+
subject: str
|
|
53
|
+
existing_responses: dict[str, str] = field(default_factory=dict)
|
|
54
|
+
existing_nodes: list[CivilizationNode] = field(default_factory=list)
|
|
55
|
+
detected_gaps: list[str] = field(default_factory=list)
|
|
56
|
+
interview_depth: str = "standard" # "brief", "standard", "thorough"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Questioner:
|
|
60
|
+
"""
|
|
61
|
+
Generates dynamic questions for knowledge elicitation.
|
|
62
|
+
Adapts questions based on context, previous responses, and detected gaps.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
llm_callback: Callable[[str, str], str] | None = None,
|
|
68
|
+
):
|
|
69
|
+
self.llm_callback = llm_callback
|
|
70
|
+
self._question_templates = self._load_templates()
|
|
71
|
+
|
|
72
|
+
def _load_templates(self) -> dict[NodeType, list[Question]]:
|
|
73
|
+
"""Load question templates for each node type."""
|
|
74
|
+
return {
|
|
75
|
+
NodeType.SOP: [
|
|
76
|
+
Question(
|
|
77
|
+
id="sop_overview",
|
|
78
|
+
text="Can you give me a high-level overview of this procedure?",
|
|
79
|
+
question_type=QuestionType.OPEN,
|
|
80
|
+
priority=QuestionPriority.REQUIRED,
|
|
81
|
+
context="Start with the big picture before diving into details",
|
|
82
|
+
),
|
|
83
|
+
Question(
|
|
84
|
+
id="sop_trigger",
|
|
85
|
+
text="What events or conditions trigger this procedure?",
|
|
86
|
+
question_type=QuestionType.LIST,
|
|
87
|
+
priority=QuestionPriority.REQUIRED,
|
|
88
|
+
examples=["Customer complaint received", "Monthly audit due", "New employee onboarding"],
|
|
89
|
+
),
|
|
90
|
+
Question(
|
|
91
|
+
id="sop_prerequisites",
|
|
92
|
+
text="What needs to be in place before starting this procedure?",
|
|
93
|
+
question_type=QuestionType.LIST,
|
|
94
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
95
|
+
),
|
|
96
|
+
Question(
|
|
97
|
+
id="sop_first_step",
|
|
98
|
+
text="What is the very first thing someone does when starting this procedure?",
|
|
99
|
+
question_type=QuestionType.OPEN,
|
|
100
|
+
priority=QuestionPriority.REQUIRED,
|
|
101
|
+
),
|
|
102
|
+
Question(
|
|
103
|
+
id="sop_decision_points",
|
|
104
|
+
text="Are there any decision points where the procedure can branch?",
|
|
105
|
+
question_type=QuestionType.OPEN,
|
|
106
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
107
|
+
context="We want to capture if-then logic in the procedure",
|
|
108
|
+
),
|
|
109
|
+
Question(
|
|
110
|
+
id="sop_handoffs",
|
|
111
|
+
text="Are there any handoffs between people or teams during this procedure?",
|
|
112
|
+
question_type=QuestionType.OPEN,
|
|
113
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
114
|
+
),
|
|
115
|
+
Question(
|
|
116
|
+
id="sop_failure_modes",
|
|
117
|
+
text="What are the most common ways this procedure can fail?",
|
|
118
|
+
question_type=QuestionType.LIST,
|
|
119
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
120
|
+
),
|
|
121
|
+
Question(
|
|
122
|
+
id="sop_time_critical",
|
|
123
|
+
text="Are there any time-sensitive steps or SLAs to be aware of?",
|
|
124
|
+
question_type=QuestionType.OPEN,
|
|
125
|
+
priority=QuestionPriority.OPTIONAL,
|
|
126
|
+
),
|
|
127
|
+
],
|
|
128
|
+
NodeType.ROLE: [
|
|
129
|
+
Question(
|
|
130
|
+
id="role_mission",
|
|
131
|
+
text="What is the primary mission or purpose of this role?",
|
|
132
|
+
question_type=QuestionType.OPEN,
|
|
133
|
+
priority=QuestionPriority.REQUIRED,
|
|
134
|
+
),
|
|
135
|
+
Question(
|
|
136
|
+
id="role_daily",
|
|
137
|
+
text="What does a typical day look like for someone in this role?",
|
|
138
|
+
question_type=QuestionType.OPEN,
|
|
139
|
+
priority=QuestionPriority.REQUIRED,
|
|
140
|
+
),
|
|
141
|
+
Question(
|
|
142
|
+
id="role_decisions",
|
|
143
|
+
text="What decisions can this role make independently?",
|
|
144
|
+
question_type=QuestionType.LIST,
|
|
145
|
+
priority=QuestionPriority.REQUIRED,
|
|
146
|
+
),
|
|
147
|
+
Question(
|
|
148
|
+
id="role_escalation",
|
|
149
|
+
text="What situations require escalation? To whom?",
|
|
150
|
+
question_type=QuestionType.OPEN,
|
|
151
|
+
priority=QuestionPriority.REQUIRED,
|
|
152
|
+
),
|
|
153
|
+
Question(
|
|
154
|
+
id="role_interfaces",
|
|
155
|
+
text="Which other roles or teams does this role interact with most?",
|
|
156
|
+
question_type=QuestionType.LIST,
|
|
157
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
158
|
+
),
|
|
159
|
+
Question(
|
|
160
|
+
id="role_success",
|
|
161
|
+
text="How is success measured for this role?",
|
|
162
|
+
question_type=QuestionType.LIST,
|
|
163
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
164
|
+
),
|
|
165
|
+
],
|
|
166
|
+
NodeType.CHECKLIST: [
|
|
167
|
+
Question(
|
|
168
|
+
id="cl_purpose",
|
|
169
|
+
text="What is the purpose of this checklist?",
|
|
170
|
+
question_type=QuestionType.OPEN,
|
|
171
|
+
priority=QuestionPriority.REQUIRED,
|
|
172
|
+
),
|
|
173
|
+
Question(
|
|
174
|
+
id="cl_when",
|
|
175
|
+
text="When should this checklist be used?",
|
|
176
|
+
question_type=QuestionType.OPEN,
|
|
177
|
+
priority=QuestionPriority.REQUIRED,
|
|
178
|
+
),
|
|
179
|
+
Question(
|
|
180
|
+
id="cl_items",
|
|
181
|
+
text="Walk me through each item on the checklist.",
|
|
182
|
+
question_type=QuestionType.LIST,
|
|
183
|
+
priority=QuestionPriority.REQUIRED,
|
|
184
|
+
),
|
|
185
|
+
Question(
|
|
186
|
+
id="cl_critical",
|
|
187
|
+
text="Are any items especially critical? What happens if they're missed?",
|
|
188
|
+
question_type=QuestionType.OPEN,
|
|
189
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
190
|
+
),
|
|
191
|
+
],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def get_initial_questions(
|
|
195
|
+
self,
|
|
196
|
+
context: QuestionContext,
|
|
197
|
+
) -> list[Question]:
|
|
198
|
+
"""Get initial questions based on context."""
|
|
199
|
+
templates = self._question_templates.get(context.target_node_type, [])
|
|
200
|
+
|
|
201
|
+
# Filter by depth
|
|
202
|
+
if context.interview_depth == "brief":
|
|
203
|
+
return [q for q in templates if q.priority == QuestionPriority.REQUIRED]
|
|
204
|
+
elif context.interview_depth == "thorough":
|
|
205
|
+
return templates
|
|
206
|
+
else: # standard
|
|
207
|
+
return [q for q in templates if q.priority != QuestionPriority.OPTIONAL]
|
|
208
|
+
|
|
209
|
+
def generate_follow_up(
|
|
210
|
+
self,
|
|
211
|
+
question_id: str,
|
|
212
|
+
response: str,
|
|
213
|
+
context: QuestionContext,
|
|
214
|
+
) -> list[Question]:
|
|
215
|
+
"""Generate follow-up questions based on a response."""
|
|
216
|
+
follow_ups = []
|
|
217
|
+
|
|
218
|
+
# Check for vague responses that need clarification
|
|
219
|
+
vague_indicators = ["sometimes", "usually", "it depends", "maybe", "i think"]
|
|
220
|
+
if any(indicator in response.lower() for indicator in vague_indicators):
|
|
221
|
+
follow_ups.append(Question(
|
|
222
|
+
id=f"{question_id}_clarify",
|
|
223
|
+
text="Can you be more specific? In what situations does this vary?",
|
|
224
|
+
question_type=QuestionType.CLARIFICATION,
|
|
225
|
+
priority=QuestionPriority.REQUIRED,
|
|
226
|
+
follow_up_for=question_id,
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
# Check for mentions of other processes/roles that aren't documented
|
|
230
|
+
if context.existing_nodes:
|
|
231
|
+
existing_titles = {n.title.lower() for n in context.existing_nodes}
|
|
232
|
+
# Simple heuristic: look for capitalized phrases that might be references
|
|
233
|
+
import re
|
|
234
|
+
potential_refs = re.findall(r"([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)", response)
|
|
235
|
+
for ref in potential_refs:
|
|
236
|
+
if ref.lower() not in existing_titles and len(ref) > 3:
|
|
237
|
+
follow_ups.append(Question(
|
|
238
|
+
id=f"ref_{ref.lower().replace(' ', '_')}",
|
|
239
|
+
text=f"You mentioned '{ref}'. Is this a process, role, or system we should document?",
|
|
240
|
+
question_type=QuestionType.CLOSED,
|
|
241
|
+
priority=QuestionPriority.OPTIONAL,
|
|
242
|
+
follow_up_for=question_id,
|
|
243
|
+
))
|
|
244
|
+
break # Only ask about one reference at a time
|
|
245
|
+
|
|
246
|
+
# Check for step mentions that need elaboration
|
|
247
|
+
if "step" in response.lower() or any(c.isdigit() for c in response):
|
|
248
|
+
if len(response) < 100: # Short response about steps
|
|
249
|
+
follow_ups.append(Question(
|
|
250
|
+
id=f"{question_id}_elaborate_steps",
|
|
251
|
+
text="Can you walk me through these steps in more detail?",
|
|
252
|
+
question_type=QuestionType.OPEN,
|
|
253
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
254
|
+
follow_up_for=question_id,
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
return follow_ups
|
|
258
|
+
|
|
259
|
+
def generate_gap_questions(
|
|
260
|
+
self,
|
|
261
|
+
gaps: list[str],
|
|
262
|
+
context: QuestionContext,
|
|
263
|
+
) -> list[Question]:
|
|
264
|
+
"""Generate questions to fill identified knowledge gaps."""
|
|
265
|
+
questions = []
|
|
266
|
+
|
|
267
|
+
gap_question_map = {
|
|
268
|
+
"missing_owner": Question(
|
|
269
|
+
id="gap_owner",
|
|
270
|
+
text="Who is responsible for maintaining and updating this?",
|
|
271
|
+
question_type=QuestionType.OPEN,
|
|
272
|
+
priority=QuestionPriority.REQUIRED,
|
|
273
|
+
),
|
|
274
|
+
"missing_tools": Question(
|
|
275
|
+
id="gap_tools",
|
|
276
|
+
text="What tools or systems are used in this process?",
|
|
277
|
+
question_type=QuestionType.LIST,
|
|
278
|
+
priority=QuestionPriority.REQUIRED,
|
|
279
|
+
),
|
|
280
|
+
"missing_timeline": Question(
|
|
281
|
+
id="gap_timeline",
|
|
282
|
+
text="How long does this typically take? Are there any SLAs?",
|
|
283
|
+
question_type=QuestionType.OPEN,
|
|
284
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
285
|
+
),
|
|
286
|
+
"missing_exceptions": Question(
|
|
287
|
+
id="gap_exceptions",
|
|
288
|
+
text="Are there any exceptions or special cases to be aware of?",
|
|
289
|
+
question_type=QuestionType.OPEN,
|
|
290
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
291
|
+
),
|
|
292
|
+
"missing_metrics": Question(
|
|
293
|
+
id="gap_metrics",
|
|
294
|
+
text="How do you measure success or track performance?",
|
|
295
|
+
question_type=QuestionType.LIST,
|
|
296
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
297
|
+
),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for gap in gaps:
|
|
301
|
+
if gap in gap_question_map:
|
|
302
|
+
questions.append(gap_question_map[gap])
|
|
303
|
+
|
|
304
|
+
return questions
|
|
305
|
+
|
|
306
|
+
async def generate_smart_question(
|
|
307
|
+
self,
|
|
308
|
+
context: QuestionContext,
|
|
309
|
+
previous_qa: list[tuple[str, str]],
|
|
310
|
+
) -> Question | None:
|
|
311
|
+
"""
|
|
312
|
+
Use LLM to generate a contextually relevant follow-up question.
|
|
313
|
+
Requires llm_callback to be set.
|
|
314
|
+
"""
|
|
315
|
+
if not self.llm_callback:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
# Build prompt for LLM
|
|
319
|
+
qa_history = "\n".join([f"Q: {q}\nA: {a}" for q, a in previous_qa[-5:]])
|
|
320
|
+
|
|
321
|
+
prompt = f"""You are helping document organizational knowledge about: {context.subject}
|
|
322
|
+
Document type: {context.target_node_type.value}
|
|
323
|
+
|
|
324
|
+
Previous Q&A:
|
|
325
|
+
{qa_history}
|
|
326
|
+
|
|
327
|
+
Based on this conversation, generate ONE follow-up question that would help complete the documentation.
|
|
328
|
+
The question should:
|
|
329
|
+
1. Fill in gaps in the information provided
|
|
330
|
+
2. Clarify any ambiguous points
|
|
331
|
+
3. Explore important details not yet covered
|
|
332
|
+
|
|
333
|
+
Return ONLY the question text, nothing else."""
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
question_text = self.llm_callback("system", prompt)
|
|
337
|
+
return Question(
|
|
338
|
+
id=f"smart_{uuid4().hex[:8]}",
|
|
339
|
+
text=question_text.strip(),
|
|
340
|
+
question_type=QuestionType.OPEN,
|
|
341
|
+
priority=QuestionPriority.RECOMMENDED,
|
|
342
|
+
metadata={"generated": True},
|
|
343
|
+
)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.warning(f"Failed to generate smart question: {e}")
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
def format_question_for_display(self, question: Question) -> str:
|
|
349
|
+
"""Format a question for user display."""
|
|
350
|
+
text = question.text
|
|
351
|
+
|
|
352
|
+
if question.context:
|
|
353
|
+
text = f"{text}\n\n(Context: {question.context})"
|
|
354
|
+
|
|
355
|
+
if question.examples:
|
|
356
|
+
examples_str = ", ".join(question.examples[:3])
|
|
357
|
+
text = f"{text}\n\nExamples: {examples_str}"
|
|
358
|
+
|
|
359
|
+
if question.options:
|
|
360
|
+
options_str = "\n".join([f" {i+1}. {opt}" for i, opt in enumerate(question.options)])
|
|
361
|
+
text = f"{text}\n\nOptions:\n{options_str}"
|
|
362
|
+
|
|
363
|
+
return text
|