gini-toolkit 6.0.1.dev0__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.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Justification validation — the crux of the dialectic (REASONING_2.0_DESIGN.md §3.6).
|
|
2
|
+
|
|
3
|
+
The Twin never takes the model's word for an omission. Each stated `why` is classified into a
|
|
4
|
+
small set of checkable patterns and validated DETERMINISTICALLY:
|
|
5
|
+
|
|
6
|
+
• pedagogical ("it would give the answer away") → allowed by POLICY for hint-shaped moves
|
|
7
|
+
(Socratic withholding is legitimate); logged either way.
|
|
8
|
+
• scope ("that's not what they asked") → checked lexically against the utterance:
|
|
9
|
+
valid only when there IS a question and its words genuinely don't touch the concern.
|
|
10
|
+
• already-addressed ("covered in an earlier hint") → checked against the Twin's history of
|
|
11
|
+
concern ids the model actually covered in prior turns of this mission.
|
|
12
|
+
• state ("not needed — the LAN is single-subnet") → the free text is TRANSLATED (one focused,
|
|
13
|
+
schema-constrained LLM call) onto the oracle's OWN predicate grammar, then EVALUATED with
|
|
14
|
+
`objectives.evaluate_check` against the live world. The translation may use a model; the
|
|
15
|
+
verdict never does. Untranslatable / no world → unvalidated → the objection stands.
|
|
16
|
+
• unknown → unvalidated → the objection stands.
|
|
17
|
+
|
|
18
|
+
An objection is DEFEATED only by a validated justification — defeasible reasoning with ground
|
|
19
|
+
truth as the referee."""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
# move kinds where withholding detail is a legitimate teaching move (the mission GM's hint;
|
|
26
|
+
# coach surfaces extend this in phase C)
|
|
27
|
+
PEDAGOGICAL_OK_KINDS = {"hint"}
|
|
28
|
+
|
|
29
|
+
_PED_RE = re.compile(r"give\s*(it|the answer)?\s*away|reveal|spoil|socratic|full answer|"
|
|
30
|
+
r"answer for them|figure it out", re.I)
|
|
31
|
+
_SCOPE_RE = re.compile(r"not what (they|the student) asked|off.?topic|different question|"
|
|
32
|
+
r"unrelated|about .+ not|this question'?s topic|other topic", re.I)
|
|
33
|
+
# must pair with a COVERAGE verb — a bare "already" is usually a state claim ("there are
|
|
34
|
+
# already two hosts"), which belongs to the oracle-checked STATE path, not this one.
|
|
35
|
+
_ALREADY_RE = re.compile(
|
|
36
|
+
r"already (covered|addressed|mentioned|said|explained|answered|hinted|told)|"
|
|
37
|
+
r"covered (before|earlier|previously|that)|previous (hint|turn|answer|line)|"
|
|
38
|
+
r"(earlier|last) (hint|turn|answer|time|line)", re.I)
|
|
39
|
+
|
|
40
|
+
PEDAGOGICAL, SCOPE, ALREADY, STATE, UNKNOWN = "pedagogical", "scope", "already", "state", "unknown"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def classify(why: str) -> str:
|
|
44
|
+
"""Deterministic keyword classification of a stated omission reason. Order matters:
|
|
45
|
+
pedagogical/scope/already are specific idioms; anything else that *asserts something about
|
|
46
|
+
the world* is treated as a state claim (and must survive translation + evaluation)."""
|
|
47
|
+
w = (why or "").strip()
|
|
48
|
+
if not w:
|
|
49
|
+
return UNKNOWN
|
|
50
|
+
if _PED_RE.search(w):
|
|
51
|
+
return PEDAGOGICAL
|
|
52
|
+
if _SCOPE_RE.search(w):
|
|
53
|
+
return SCOPE
|
|
54
|
+
if _ALREADY_RE.search(w):
|
|
55
|
+
return ALREADY
|
|
56
|
+
return STATE
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# -- state-claim translation (LLM proposes, the oracle disposes) -------------- #
|
|
60
|
+
TRANSLATE_SCHEMA: dict = {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {"predicate": {"type": ["string", "null"]}},
|
|
63
|
+
"required": ["predicate"],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_TRANSLATE_SYSTEM = (
|
|
67
|
+
"You translate ONE claim about a network lab board into ONE predicate from this exact "
|
|
68
|
+
"grammar (nothing else): exists(type) · count(type) >= n · link(a, b) · path(a, b) · "
|
|
69
|
+
"through(a, b, gate) · contains_type(group, type) · all_linked(type). Types are element "
|
|
70
|
+
"type keys (host, switch, router, …). Output ONLY JSON: {\"predicate\": \"<one predicate>\"} "
|
|
71
|
+
"or {\"predicate\": null} if the claim cannot be expressed in this grammar. No prose.")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def llm_translate(runner, why: str) -> str | None:
|
|
75
|
+
"""One focused, schema-constrained call turning a free-text state claim into a predicate the
|
|
76
|
+
oracle can evaluate. Returns None when there's no model, no clean parse, or the model says
|
|
77
|
+
the claim isn't expressible — all of which leave the justification unvalidated."""
|
|
78
|
+
if runner is None or getattr(runner, "_llm", None) is None:
|
|
79
|
+
return None
|
|
80
|
+
from ..personas import Persona, first_json
|
|
81
|
+
persona = Persona("Translator", system=_TRANSLATE_SYSTEM, temperature=0.0,
|
|
82
|
+
schema=TRANSLATE_SCHEMA)
|
|
83
|
+
out = runner.call(persona, task=f"Claim: {why!r}")
|
|
84
|
+
obj = first_json(out)
|
|
85
|
+
pred = obj.get("predicate") if isinstance(obj, dict) else None
|
|
86
|
+
return pred if isinstance(pred, str) and pred.strip() else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def state_holds(predicate: str, world) -> bool | None:
|
|
90
|
+
"""Evaluate a translated predicate against the live world with the oracle's OWN evaluator
|
|
91
|
+
(`objectives.evaluate_check`) — one predicate semantics, never a second one. None = cannot
|
|
92
|
+
evaluate (bad predicate / no world) — which leaves the objection standing."""
|
|
93
|
+
if not predicate or world is None:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
from ...domain import objectives as _obj
|
|
97
|
+
if not _obj.check_ok(predicate):
|
|
98
|
+
return None
|
|
99
|
+
# accept either the predicate-World interface (has .exists) or a bare Topology
|
|
100
|
+
view = world if hasattr(world, "exists") else _obj.TopologyWorld(world)
|
|
101
|
+
return bool(_obj.evaluate_check(predicate, view))
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# -- adjudication ------------------------------------------------------------- #
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class Adjudication:
|
|
109
|
+
valid: bool
|
|
110
|
+
kind: str
|
|
111
|
+
reason: str = "" # why it failed (feeds the objection question) / how it was validated
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _tokens(text: str) -> set:
|
|
115
|
+
return {t for t in re.findall(r"[a-z0-9]+", (text or "").lower()) if len(t) > 2}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def adjudicate(concern, why: str, ctx) -> Adjudication:
|
|
119
|
+
"""Validate one omission justification against ground truth. `ctx` carries move_kind,
|
|
120
|
+
utterance, world, history (concern ids covered in prior turns), and a translate callable."""
|
|
121
|
+
kind = classify(why)
|
|
122
|
+
|
|
123
|
+
if kind == PEDAGOGICAL:
|
|
124
|
+
if getattr(ctx, "move_kind", "") in PEDAGOGICAL_OK_KINDS:
|
|
125
|
+
return Adjudication(True, kind, "withholding is legitimate for a hint")
|
|
126
|
+
return Adjudication(False, kind,
|
|
127
|
+
"this turn isn't a hint — answering shouldn't withhold this")
|
|
128
|
+
|
|
129
|
+
if kind == SCOPE:
|
|
130
|
+
utterance = getattr(ctx, "utterance", "") or ""
|
|
131
|
+
if not utterance:
|
|
132
|
+
return Adjudication(False, kind, "nothing was asked — there is no topic to be off")
|
|
133
|
+
overlap = _tokens(utterance) & (_tokens(concern.statement) | _tokens(concern.id))
|
|
134
|
+
if overlap:
|
|
135
|
+
return Adjudication(False, kind,
|
|
136
|
+
f"the question mentions {', '.join(sorted(overlap))} — it IS on topic")
|
|
137
|
+
return Adjudication(True, kind, "the question doesn't touch this concern")
|
|
138
|
+
|
|
139
|
+
if kind == ALREADY:
|
|
140
|
+
if concern.id in (getattr(ctx, "history", None) or set()):
|
|
141
|
+
return Adjudication(True, kind, "it was covered in an earlier turn")
|
|
142
|
+
return Adjudication(False, kind, "it was never actually covered in an earlier turn")
|
|
143
|
+
|
|
144
|
+
if kind == STATE:
|
|
145
|
+
translate = getattr(ctx, "translate", None)
|
|
146
|
+
pred = translate(why) if callable(translate) else None
|
|
147
|
+
if not pred:
|
|
148
|
+
return Adjudication(False, kind, "the claim couldn't be checked against the board")
|
|
149
|
+
held = state_holds(pred, getattr(ctx, "world", None))
|
|
150
|
+
if held is True:
|
|
151
|
+
return Adjudication(True, kind, f"checked: {pred} holds")
|
|
152
|
+
if held is False:
|
|
153
|
+
return Adjudication(False, kind, f"checked: {pred} is FALSE on this board")
|
|
154
|
+
return Adjudication(False, kind, "the claim couldn't be checked against the board")
|
|
155
|
+
|
|
156
|
+
return Adjudication(False, UNKNOWN, "no checkable reason was given")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""The learner-model concern source (REASONING_2.0_DESIGN.md phase D; LEARNER_MODEL_DESIGN.md
|
|
2
|
+
§6.1 / L-F). This is the CONSUMPTION seam: the learner model itself is built on a parallel
|
|
3
|
+
track (`domain/learner.py`); this module duck-types against its ratified shape so the Twin is
|
|
4
|
+
ready the moment it lands.
|
|
5
|
+
|
|
6
|
+
Expected learner-state shape (duck-typed, all optional):
|
|
7
|
+
• .misconceptions — iterable of active misconceptions with .id, .concept, .statement and an
|
|
8
|
+
evidence count (`.evidence` list or `.evidence_count`);
|
|
9
|
+
• .band(concept) -> "unknown" | "weak" | "developing" | "solid" (counters+decay, auditable).
|
|
10
|
+
|
|
11
|
+
Rules (per the ratified design):
|
|
12
|
+
• an ACTIVE misconception on a concept the current work touches → salience 3; other active
|
|
13
|
+
misconceptions → salience 2;
|
|
14
|
+
• a WEAK-band concept touched by the current work → salience 2;
|
|
15
|
+
• `unknown` band = cold start = NO concern (unknown ≠ weak — no adaptation, not remediation);
|
|
16
|
+
• every concern carries evidence (the misconception's evidence count / the band), same as
|
|
17
|
+
every other Twin source: no evidence, no concern."""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .contracts import Concern
|
|
21
|
+
from .salience import cap
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _evidence_count(m) -> int:
|
|
25
|
+
ev = getattr(m, "evidence", None)
|
|
26
|
+
if isinstance(ev, (list, tuple)):
|
|
27
|
+
return len(ev)
|
|
28
|
+
return int(getattr(m, "evidence_count", 0) or 0)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def learner_concerns(learner, *, concepts=()) -> list[Concern]:
|
|
32
|
+
"""Concerns about THIS student, for the current work. `concepts` = the concept keys the
|
|
33
|
+
current mission/lab teaches (e.g. from the fragment's `teaches:`)."""
|
|
34
|
+
if learner is None:
|
|
35
|
+
return []
|
|
36
|
+
touched = set(concepts or ())
|
|
37
|
+
concerns: list[Concern] = []
|
|
38
|
+
|
|
39
|
+
for m in getattr(learner, "misconceptions", None) or []:
|
|
40
|
+
n = _evidence_count(m)
|
|
41
|
+
if n <= 0:
|
|
42
|
+
continue # no evidence, no concern
|
|
43
|
+
relevant = getattr(m, "concept", "") in touched
|
|
44
|
+
concerns.append(Concern(
|
|
45
|
+
id=f"misconception:{m.id}", kind="misconception",
|
|
46
|
+
statement=f"the student likely {m.statement}",
|
|
47
|
+
evidence=f"detector evidence x{n} on {getattr(m, 'concept', '?')}",
|
|
48
|
+
salience=3 if relevant else 2, source="learner-model"))
|
|
49
|
+
|
|
50
|
+
band_of = getattr(learner, "band", None)
|
|
51
|
+
if callable(band_of):
|
|
52
|
+
for concept in sorted(touched):
|
|
53
|
+
try:
|
|
54
|
+
band = band_of(concept)
|
|
55
|
+
except Exception:
|
|
56
|
+
continue
|
|
57
|
+
if band == "weak": # unknown = cold start = NO concern
|
|
58
|
+
concerns.append(Concern(
|
|
59
|
+
id=f"learner:weak:{concept}", kind="misconception",
|
|
60
|
+
statement=f"the student is weak on {concept}",
|
|
61
|
+
evidence=f"mastery band: weak on {concept}",
|
|
62
|
+
salience=2, source="learner-model"))
|
|
63
|
+
|
|
64
|
+
return cap(concerns)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""The mission-GM concern enumerator — a deterministic projection of what already sits on the
|
|
2
|
+
blackboard into the Twin's Concern set. Nothing is sensed anew and nothing is judged: every
|
|
3
|
+
concern is backed by a cached Verdict (and, for objectives, the predicate explainer's
|
|
4
|
+
board-grounded why). REASONING_2.0_DESIGN.md §3.2, first row of the surface table."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .contracts import Concern
|
|
8
|
+
from .salience import cap, salience_for
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _why(objective, world) -> str:
|
|
12
|
+
"""The deterministic, board-grounded reason an objective is red (domain/explain.py) —
|
|
13
|
+
the same explainer the Reasoning persona uses; '' when the world isn't on hand."""
|
|
14
|
+
if objective is None or world is None:
|
|
15
|
+
return ""
|
|
16
|
+
try:
|
|
17
|
+
from ...domain import explain as _explain
|
|
18
|
+
return _explain.diagnose(objective, world) or ""
|
|
19
|
+
except Exception:
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def mission_concerns(blackboard, lesson) -> list[Concern]:
|
|
24
|
+
"""Enumerate this turn's concerns for the mission game-master, salience-capped.
|
|
25
|
+
|
|
26
|
+
Sources (all already deterministic, all already computed):
|
|
27
|
+
• every UNMET objective — statement is its `say`, evidence is the explainer's why
|
|
28
|
+
(or the bare verdict when no world is cached);
|
|
29
|
+
• legality flags (off-task elements, illegal links) — evidence is the flagged names.
|
|
30
|
+
"""
|
|
31
|
+
concerns: list[Concern] = []
|
|
32
|
+
say = {o.id: o for o in getattr(lesson, "objectives", [])}
|
|
33
|
+
world = getattr(blackboard, "_world", None)
|
|
34
|
+
|
|
35
|
+
for oid in blackboard.unmet_objectives():
|
|
36
|
+
obj = say.get(oid)
|
|
37
|
+
why = _why(obj, world)
|
|
38
|
+
statement = getattr(obj, "say", oid)
|
|
39
|
+
concerns.append(Concern(
|
|
40
|
+
id=f"objective:{oid}", kind="objective",
|
|
41
|
+
statement=f"'{statement}' is unmet",
|
|
42
|
+
evidence=why or "objective verdict: unmet",
|
|
43
|
+
salience=salience_for("objective"), source="blackboard"))
|
|
44
|
+
|
|
45
|
+
flags = blackboard.flags()
|
|
46
|
+
if flags.get("off_task"):
|
|
47
|
+
names = ", ".join(flags["off_task"])
|
|
48
|
+
concerns.append(Concern(
|
|
49
|
+
id="legality:off_task", kind="legality",
|
|
50
|
+
statement=f"off-task element(s) on the board: {names}",
|
|
51
|
+
evidence=f"off_task verdict lists: {names}",
|
|
52
|
+
salience=salience_for("legality"), source="blackboard"))
|
|
53
|
+
if flags.get("illegal_links"):
|
|
54
|
+
concerns.append(Concern(
|
|
55
|
+
id="legality:illegal_links", kind="legality",
|
|
56
|
+
statement="illegal connection(s) present",
|
|
57
|
+
evidence="illegal_links verdict is failing",
|
|
58
|
+
salience=salience_for("legality"), source="blackboard"))
|
|
59
|
+
|
|
60
|
+
return cap(concerns)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The OS-Coach concern enumerator (REASONING_2.0_DESIGN.md §5, second surface).
|
|
2
|
+
|
|
3
|
+
The coach streams a single Socratic nudge straight to chat, so phase C gives it the Twin's
|
|
4
|
+
FEED-FORWARD half: a deterministic concern set (watcher events, scheduling flags, a faulted
|
|
5
|
+
shadow) whose TOP concern is injected into the prompt as the thing the hint should target —
|
|
6
|
+
"did the hint target the most salient event?" answered by construction. The same concern set
|
|
7
|
+
is the upgraded no-model fallback ("things worth looking at"). The audit/revision half needs
|
|
8
|
+
the coach to become a structured turn (not a fire-and-forget stream) — deferred, by design.
|
|
9
|
+
|
|
10
|
+
Socratic withholding is ALWAYS legitimate here: the coach's job is one nudge, so no coverage
|
|
11
|
+
obligation is imposed on it."""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .contracts import Concern
|
|
15
|
+
from .salience import cap
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def coach_concerns(events, machine_state) -> list[Concern]:
|
|
19
|
+
"""Enumerate what matters for one coach nudge — all deterministic, all evidence-backed.
|
|
20
|
+
|
|
21
|
+
`events` are the drained StateWatcher OsEvents (the teachable moments); `machine_state`
|
|
22
|
+
contributes the currently-active scheduling flags and the shadow manifest (a student shadow
|
|
23
|
+
that crashed back to the primary outranks everything — they think their code is running)."""
|
|
24
|
+
concerns: list[Concern] = []
|
|
25
|
+
|
|
26
|
+
for e in events or []:
|
|
27
|
+
concerns.append(Concern(
|
|
28
|
+
id=f"watcher:{e.kind}" + (f":{e.pid}" if e.pid is not None else ""),
|
|
29
|
+
kind="watcher-event", statement=e.detail, evidence=e.detail,
|
|
30
|
+
salience=2, source="watcher"))
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
for name, s in (machine_state.shadows() or {}).items():
|
|
34
|
+
if getattr(s, "faults", 0) and getattr(s, "is_student", False):
|
|
35
|
+
concerns.append(Concern(
|
|
36
|
+
id=f"shadow:{name}:faulted", kind="watcher-event",
|
|
37
|
+
statement=(f"your {name} shadow crashed {s.faults}x and the shipped "
|
|
38
|
+
"primary is running instead"),
|
|
39
|
+
evidence=f"manifest: faults={s.faults} active={int(bool(s.active))}",
|
|
40
|
+
salience=3, source="shadow-manifest"))
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
flags = machine_state.scheduling_flags() or {}
|
|
46
|
+
seen = {c.id for c in concerns}
|
|
47
|
+
for kind, pids in flags.items():
|
|
48
|
+
for pid in pids:
|
|
49
|
+
cid = f"watcher:{kind}:{pid}"
|
|
50
|
+
if cid not in seen: # active-but-already-drained conditions still show
|
|
51
|
+
concerns.append(Concern(
|
|
52
|
+
id=cid, kind="watcher-event",
|
|
53
|
+
statement=f"pid {pid} is currently in {kind.replace('_', ' ')}",
|
|
54
|
+
evidence=f"scheduling flag {kind} active for pid {pid}",
|
|
55
|
+
salience=2, source="watcher"))
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return cap(concerns)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def focus_line(concerns: list[Concern]) -> str:
|
|
63
|
+
"""The feed-forward injection for the coach prompt: the single most salient concern the
|
|
64
|
+
nudge should target (deterministically chosen — the model words it, never picks it)."""
|
|
65
|
+
if not concerns:
|
|
66
|
+
return ""
|
|
67
|
+
top = concerns[0]
|
|
68
|
+
return (f"The most salient issue RIGHT NOW (target your nudge at this): "
|
|
69
|
+
f"{top.statement} [{top.evidence}]")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def fallback_text(concerns: list[Concern]) -> str:
|
|
73
|
+
"""The no-model coach reply — the concern set rendered directly (better than the old raw
|
|
74
|
+
event list: salience-ordered, evidence-backed, still zero-LLM)."""
|
|
75
|
+
if not concerns:
|
|
76
|
+
return ("The run looks steady. Try slowing the time-slice or spawning another "
|
|
77
|
+
"CPU-bound process, and watch the switches.")
|
|
78
|
+
lines = [f"- {c.statement}" for c in concerns[:3]]
|
|
79
|
+
return "Worth looking at right now:\n" + "\n".join(lines)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Salience — rules only, never a model (the notifier philosophy: tune the RULES if noisy).
|
|
2
|
+
|
|
3
|
+
The caps are the anti-noise guarantee: at most MAX_CONCERNS reach a turn, and only concerns at
|
|
4
|
+
or above MUST_ADDRESS must be covered by the model (lower ones are context the Twin tracks but
|
|
5
|
+
never objects about)."""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
MAX_CONCERNS = 5 # hard cap per turn — the Twin whispers, it does not checklist
|
|
9
|
+
MUST_ADDRESS = 2 # salience >= this must appear in the coverage report (addressed or justified)
|
|
10
|
+
|
|
11
|
+
# base salience per concern kind (0..3). 3 = urgent (objection even under coverage-silence).
|
|
12
|
+
KIND_SALIENCE = {
|
|
13
|
+
"legality": 3, # off-task / illegal links — active rule violations
|
|
14
|
+
"objective": 2, # an unmet objective with a grounded why
|
|
15
|
+
"watcher-event": 2, # OS pedagogical events (starvation, monopoly…) — phase C
|
|
16
|
+
"composition-gap": 2, # unfilled requires / authoring disagreements — phase C
|
|
17
|
+
"misconception": 2, # learner-model concerns (3 when touching the current work) — phase D
|
|
18
|
+
"grammar-option": 1, # a valid-but-unused possibility — context, never an objection
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def salience_for(kind: str) -> int:
|
|
23
|
+
return KIND_SALIENCE.get(kind, 1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cap(concerns: list) -> list:
|
|
27
|
+
"""Highest-salience first, capped at MAX_CONCERNS; stable within a salience band so
|
|
28
|
+
enumeration order (objectives before flags, etc.) is preserved deterministically."""
|
|
29
|
+
ranked = sorted(concerns, key=lambda c: -c.salience)
|
|
30
|
+
return ranked[:MAX_CONCERNS]
|
gini/agent/understand.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Understanding front-door — raw NL (question / sentence / fragment) -> structured Intent.
|
|
2
|
+
|
|
3
|
+
Everything downstream (retrieval, routing, reasoning) keys off the Intent, so this is the
|
|
4
|
+
gate. It runs on the always-on context (element index + canvas + recent turns) because a
|
|
5
|
+
fragment ("why broke", "the db thing") only resolves against the canvas and history.
|
|
6
|
+
|
|
7
|
+
Two-tier, cheap-first:
|
|
8
|
+
1. a deterministic parse handles the clear cases (element names, command verbs, concept
|
|
9
|
+
keywords) at zero latency;
|
|
10
|
+
2. an optional LLM refine (same model) only fires when the deterministic parse is
|
|
11
|
+
low-confidence — a small, focused prompt, NOT the full tutor prompt.
|
|
12
|
+
|
|
13
|
+
The active mode biases the Intent (Coach -> diagnose, Wizard -> guide, Explain -> explain)
|
|
14
|
+
but doesn't fully override it. Pure logic + an injectable `llm` callable, so it's testable.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from ..domain import concepts as _concepts
|
|
23
|
+
from ..domain import recipes as _recipes
|
|
24
|
+
from ..domain.devices import all_devices
|
|
25
|
+
|
|
26
|
+
TYPES = ("explain", "how_to", "build", "diagnose", "meta", "chitchat")
|
|
27
|
+
ANCHORS = ("canvas", "concept", "hybrid")
|
|
28
|
+
FORMS = ("tell", "show", "guide")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Intent:
|
|
33
|
+
type: str = "explain"
|
|
34
|
+
anchor: str = "concept"
|
|
35
|
+
output_form: str = "tell"
|
|
36
|
+
topics: list[str] = field(default_factory=list)
|
|
37
|
+
refs: list[str] = field(default_factory=list)
|
|
38
|
+
depth: str = "shallow"
|
|
39
|
+
confidence: float = 0.5
|
|
40
|
+
query: str = ""
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return {"type": self.type, "anchor": self.anchor, "output_form": self.output_form,
|
|
44
|
+
"topics": self.topics, "refs": self.refs, "depth": self.depth,
|
|
45
|
+
"confidence": round(self.confidence, 2)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -- keyword tables --------------------------------------------------------- #
|
|
49
|
+
_BUILD_CMD = ("add ", "connect ", "remove ", "delete ", "place ", "wire ", "attach ", "rename ")
|
|
50
|
+
_HOWTO = ("how to", "how do i", "how do you", "how can i", "how would i", "how should i",
|
|
51
|
+
"how we", "how do we", "how can we", "show how", "show me how")
|
|
52
|
+
# verbs that mean "materialise it" (vs. "use / work" which stay explanatory)
|
|
53
|
+
_BUILD_WORDS = ("create", "build", "set up", "setup", "construct", "make", "deploy",
|
|
54
|
+
"implement", "scaffold")
|
|
55
|
+
_SHOW = ("show me", "show an example", "give me an example", "construct", "build me",
|
|
56
|
+
"build a", "build an", "set up", "setup", "lay out", "demonstrate", "create a",
|
|
57
|
+
"create an", "make a", "make an")
|
|
58
|
+
_GUIDE = ("walk me through", "guide me", "step by step", "step-by-step", "help me build")
|
|
59
|
+
_EXPLAIN = ("explain", "what is", "what's a", "what are", "what does", "tell me about",
|
|
60
|
+
"describe", "when do i", "when to use", "when should i")
|
|
61
|
+
_DIAGNOSE = ("what's wrong", "whats wrong", "is my", "are my", "check my", "not working",
|
|
62
|
+
"unreachable", "why isn't", "why can't", "why won't", "broken", "fix ",
|
|
63
|
+
"what is wrong", "isn't working", "doesn't work")
|
|
64
|
+
_META = ("what can you do", "which elements", "what elements", "list the", "what do you",
|
|
65
|
+
"help me get started", "what can i build")
|
|
66
|
+
_GREET = ("hi", "hello", "hey", "thanks", "thank you", "yo", "sup")
|
|
67
|
+
_DEEP = ("why", "how does", "how do", "under the hood", "in detail", "explain how",
|
|
68
|
+
"difference between", "compare", "what happens when", "trade-off", "tradeoff",
|
|
69
|
+
"internally", "actually work")
|
|
70
|
+
_DEMONSTRATIVE = ("this ", "these ", " my ", " here", "the current", "my canvas", "my setup",
|
|
71
|
+
"my topology")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _has(text: str, needles) -> bool:
|
|
75
|
+
return any(n in text for n in needles)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _extract_topics(text: str) -> list[str]:
|
|
79
|
+
"""Concept keywords, element keys, and recipe tags present in the text (dedup, ordered)."""
|
|
80
|
+
topics: list[str] = []
|
|
81
|
+
|
|
82
|
+
def add(t):
|
|
83
|
+
if t and t not in topics:
|
|
84
|
+
topics.append(t)
|
|
85
|
+
|
|
86
|
+
for c in _concepts.CONCEPTS:
|
|
87
|
+
for kw in c.keywords:
|
|
88
|
+
if kw in text:
|
|
89
|
+
add(kw)
|
|
90
|
+
for d in all_devices():
|
|
91
|
+
if re.search(rf"\b{re.escape(d.key)}\b", text) or d.label.lower() in text:
|
|
92
|
+
add(d.key)
|
|
93
|
+
for r in _recipes.RECIPES:
|
|
94
|
+
for tag in r.intent:
|
|
95
|
+
if tag in text:
|
|
96
|
+
add(tag)
|
|
97
|
+
return topics
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _extract_refs(text: str, canvas_names) -> list[str]:
|
|
101
|
+
"""Device names from the canvas that appear as whole words in the text."""
|
|
102
|
+
if not canvas_names:
|
|
103
|
+
return []
|
|
104
|
+
low = text.lower()
|
|
105
|
+
return [n for n in canvas_names if re.search(rf"\b{re.escape(n.lower())}\b", low)]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# -- the parse -------------------------------------------------------------- #
|
|
109
|
+
def understand(text: str, *, canvas_names=None, mode: str = "chat",
|
|
110
|
+
llm=None, min_confidence: float = 0.45) -> Intent:
|
|
111
|
+
"""Interpret one user message. `canvas_names` is the list of placed device names;
|
|
112
|
+
`mode` is the active panel mode; `llm` is an optional callable(prompt)->str used only
|
|
113
|
+
when the deterministic parse is unsure."""
|
|
114
|
+
raw = (text or "").strip()
|
|
115
|
+
low = raw.lower()
|
|
116
|
+
words = low.split()
|
|
117
|
+
canvas_empty = not canvas_names
|
|
118
|
+
|
|
119
|
+
it = Intent(query=raw)
|
|
120
|
+
it.topics = _extract_topics(low)
|
|
121
|
+
it.refs = _extract_refs(raw, canvas_names or [])
|
|
122
|
+
|
|
123
|
+
# --- type ---
|
|
124
|
+
typed = True
|
|
125
|
+
if _has(low, _GREET) and len(words) <= 3:
|
|
126
|
+
it.type = "chitchat"
|
|
127
|
+
elif _has(low, _META):
|
|
128
|
+
it.type = "meta"
|
|
129
|
+
elif any(low.startswith(v) for v in _BUILD_CMD):
|
|
130
|
+
it.type = "build" # an imperative command
|
|
131
|
+
elif _has(low, _DIAGNOSE):
|
|
132
|
+
it.type = "diagnose"
|
|
133
|
+
elif _has(low, _HOWTO):
|
|
134
|
+
it.type = "how_to"
|
|
135
|
+
elif _has(low, _SHOW):
|
|
136
|
+
it.type = "how_to" # "show me / build a X" is a how-to-build
|
|
137
|
+
elif _has(low, _EXPLAIN):
|
|
138
|
+
it.type = "explain"
|
|
139
|
+
elif it.topics:
|
|
140
|
+
it.type = "explain" # a bare topic -> explain it
|
|
141
|
+
typed = False
|
|
142
|
+
else:
|
|
143
|
+
it.type = "explain"
|
|
144
|
+
typed = False
|
|
145
|
+
|
|
146
|
+
# --- output_form ---
|
|
147
|
+
if _has(low, _GUIDE):
|
|
148
|
+
it.output_form = "guide"
|
|
149
|
+
elif _has(low, _SHOW) or (it.type in ("how_to", "build")
|
|
150
|
+
and _has(low, ("example",) + _BUILD_WORDS)):
|
|
151
|
+
it.output_form = "show" # "show/how to CREATE/BUILD X" -> materialise
|
|
152
|
+
else:
|
|
153
|
+
it.output_form = "tell" # "how to USE X" -> explain + offer to build
|
|
154
|
+
|
|
155
|
+
# --- anchor ---
|
|
156
|
+
generative = it.type in ("how_to", "build") and (
|
|
157
|
+
it.output_form in ("show", "guide") or _has(low, _BUILD_WORDS + ("add",)))
|
|
158
|
+
if canvas_empty:
|
|
159
|
+
it.anchor = "concept"
|
|
160
|
+
elif it.refs or _has(low, _DEMONSTRATIVE):
|
|
161
|
+
it.anchor = "hybrid" if generative else "canvas"
|
|
162
|
+
elif generative:
|
|
163
|
+
it.anchor = "hybrid" # build onto an existing canvas
|
|
164
|
+
elif it.type == "diagnose":
|
|
165
|
+
it.anchor = "canvas"
|
|
166
|
+
else:
|
|
167
|
+
it.anchor = "concept"
|
|
168
|
+
|
|
169
|
+
# --- depth ---
|
|
170
|
+
it.depth = "deep" if _has(low, _DEEP) else "shallow"
|
|
171
|
+
|
|
172
|
+
# --- confidence ---
|
|
173
|
+
conf = 0.5 + (0.3 if typed else 0.0) + (0.2 if it.topics else 0.0)
|
|
174
|
+
if len(words) < 3 and not it.topics and not it.refs:
|
|
175
|
+
conf -= 0.35
|
|
176
|
+
it.confidence = max(0.0, min(1.0, conf))
|
|
177
|
+
|
|
178
|
+
_apply_mode(it, mode, canvas_empty, low)
|
|
179
|
+
|
|
180
|
+
# --- LLM refine only when unsure ---
|
|
181
|
+
if it.confidence < min_confidence and llm is not None:
|
|
182
|
+
_llm_refine(it, canvas_names, llm)
|
|
183
|
+
return it
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _apply_mode(it: Intent, mode: str, canvas_empty: bool, low: str = "") -> None:
|
|
187
|
+
"""The active panel mode biases the Intent (doesn't fully override the parse)."""
|
|
188
|
+
if mode == "coach":
|
|
189
|
+
it.type = "diagnose"
|
|
190
|
+
it.anchor = "concept" if canvas_empty else "canvas"
|
|
191
|
+
it.output_form = "tell"
|
|
192
|
+
elif mode == "wizard":
|
|
193
|
+
# Wizard is for guided building: a bare goal ("a serverless api") is a build target
|
|
194
|
+
# unless the student explicitly asked to be told about something.
|
|
195
|
+
if not _has(low, _EXPLAIN) and not _has(low, _META):
|
|
196
|
+
it.type = "build"
|
|
197
|
+
it.output_form = "guide"
|
|
198
|
+
elif mode == "explain" and it.type not in ("diagnose", "build"):
|
|
199
|
+
it.type = "explain"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
_REFINE_PROMPT = (
|
|
203
|
+
"You interpret a student's message in the GINI network/cloud lab. Reply with ONLY a "
|
|
204
|
+
"JSON object: {\"type\": explain|how_to|build|diagnose|meta|chitchat, \"anchor\": "
|
|
205
|
+
"canvas|concept|hybrid, \"output_form\": tell|show|guide, \"topics\": [..], \"depth\": "
|
|
206
|
+
"shallow|deep}. No prose.\n"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _llm_refine(it: Intent, canvas_names, llm) -> None:
|
|
211
|
+
"""Ask the model to interpret an ambiguous fragment; merge if it returns valid JSON."""
|
|
212
|
+
ctx = f"Canvas devices: {', '.join(canvas_names) if canvas_names else '(empty)'}.\n"
|
|
213
|
+
try:
|
|
214
|
+
out = llm(_REFINE_PROMPT + ctx + f"Message: {it.query!r}")
|
|
215
|
+
obj = _first_json(out)
|
|
216
|
+
except Exception:
|
|
217
|
+
obj = None
|
|
218
|
+
if not isinstance(obj, dict):
|
|
219
|
+
return
|
|
220
|
+
if obj.get("type") in TYPES:
|
|
221
|
+
it.type = obj["type"]
|
|
222
|
+
if obj.get("anchor") in ANCHORS:
|
|
223
|
+
it.anchor = obj["anchor"]
|
|
224
|
+
if obj.get("output_form") in FORMS:
|
|
225
|
+
it.output_form = obj["output_form"]
|
|
226
|
+
if obj.get("depth") in ("shallow", "deep"):
|
|
227
|
+
it.depth = obj["depth"]
|
|
228
|
+
if isinstance(obj.get("topics"), list):
|
|
229
|
+
for t in obj["topics"]:
|
|
230
|
+
if isinstance(t, str) and t.strip() and t not in it.topics:
|
|
231
|
+
it.topics.append(t.strip())
|
|
232
|
+
it.confidence = max(it.confidence, 0.6) # the model resolved it
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _first_json(text: str):
|
|
236
|
+
depth = 0
|
|
237
|
+
start = None
|
|
238
|
+
for i, ch in enumerate(text or ""):
|
|
239
|
+
if ch == "{":
|
|
240
|
+
if depth == 0:
|
|
241
|
+
start = i
|
|
242
|
+
depth += 1
|
|
243
|
+
elif ch == "}" and depth > 0:
|
|
244
|
+
depth -= 1
|
|
245
|
+
if depth == 0 and start is not None:
|
|
246
|
+
try:
|
|
247
|
+
return json.loads(text[start:i + 1])
|
|
248
|
+
except json.JSONDecodeError:
|
|
249
|
+
start = None
|
|
250
|
+
return None
|