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
gini/agent/recall.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Hybrid recall — the layered retriever behind Ask GINI.
|
|
2
|
+
|
|
3
|
+
Given a student's question, find the GINI concept notes and recipes that ground the answer,
|
|
4
|
+
and report how CONFIDENT the match is (`strength`) so the reasoner can pick a grounding
|
|
5
|
+
stance. Three layers, cheap-first, each engaged only when the previous is thin:
|
|
6
|
+
|
|
7
|
+
L0 lexical — idf-weighted coverage over concepts+recipes, using `domain.lexicon` to
|
|
8
|
+
normalize student phrasing into GINI vocabulary (always runs, no deps);
|
|
9
|
+
L1 expand — if L0 is thin AND a model is available, ask it to rewrite the query into
|
|
10
|
+
GINI keywords, then re-run L0 (reuses the existing LLM; no embeddings);
|
|
11
|
+
L2 embed — if still empty AND an embedder is available, semantic recall over the
|
|
12
|
+
precomputed KB index (see `agent.embed`), which catches genuine synonyms
|
|
13
|
+
the finite lexicon can't anticipate ("software defined" ↔ SDN).
|
|
14
|
+
|
|
15
|
+
`strength ∈ {strong, thin, empty}` drives `ask.grounding_stance`. Pure/deterministic except
|
|
16
|
+
the injected `llm` (L1) and `embedder` (L2), so L0 is fully unit-testable offline.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import math
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
from ..domain import concepts as _concepts
|
|
26
|
+
from ..domain import lexicon as _lex
|
|
27
|
+
from ..domain import recipes as _recipes
|
|
28
|
+
|
|
29
|
+
# strength thresholds on best idf-weighted coverage of the query's content terms
|
|
30
|
+
_STRONG = 0.45
|
|
31
|
+
_THIN = 0.15
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Hit:
|
|
36
|
+
kind: str # concept | recipe
|
|
37
|
+
key: str
|
|
38
|
+
obj: object
|
|
39
|
+
score: float # idf-weighted match mass (for ranking)
|
|
40
|
+
coverage: float # fraction of the query's idf mass that matched (for strength)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class RecallResult:
|
|
45
|
+
concept_hits: list[Hit] = field(default_factory=list)
|
|
46
|
+
recipe_hits: list[Hit] = field(default_factory=list)
|
|
47
|
+
strength: str = "empty"
|
|
48
|
+
used_expansion: bool = False
|
|
49
|
+
used_embedding: bool = False
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def concepts(self) -> list:
|
|
53
|
+
return [h.obj for h in self.concept_hits]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def recipes(self) -> list:
|
|
57
|
+
return [h.obj for h in self.recipe_hits]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# -- corpus + idf (built once) --------------------------------------------- #
|
|
61
|
+
def _concept_text(c) -> str:
|
|
62
|
+
return " ".join((c.title, " ".join(c.keywords), " ".join(c.elements), c.body[:400]))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _recipe_text(r) -> str:
|
|
66
|
+
return " ".join((r.name, " ".join(r.intent), r.summary, r.teaches))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class _Doc:
|
|
71
|
+
kind: str
|
|
72
|
+
key: str
|
|
73
|
+
obj: object
|
|
74
|
+
tokens: frozenset
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_docs() -> list[_Doc]:
|
|
78
|
+
docs: list[_Doc] = []
|
|
79
|
+
for c in _concepts.CONCEPTS:
|
|
80
|
+
docs.append(_Doc("concept", c.key, c, frozenset(_lex.normalize(_concept_text(c)))))
|
|
81
|
+
for r in _recipes.RECIPES:
|
|
82
|
+
docs.append(_Doc("recipe", r.id, r, frozenset(_lex.normalize(_recipe_text(r)))))
|
|
83
|
+
return docs
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_DOCS: list[_Doc] = _build_docs()
|
|
87
|
+
_N = len(_DOCS)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _idf_table() -> dict[str, float]:
|
|
91
|
+
df: dict[str, int] = {}
|
|
92
|
+
for d in _DOCS:
|
|
93
|
+
for t in d.tokens:
|
|
94
|
+
df[t] = df.get(t, 0) + 1
|
|
95
|
+
return {t: math.log((_N + 1) / (n + 0.5)) for t, n in df.items()}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_IDF = _idf_table()
|
|
99
|
+
_IDF_UNKNOWN = math.log((_N + 1) / 0.5) # a query term absent from the KB is rare = informative
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _idf(token: str) -> float:
|
|
103
|
+
return _IDF.get(token, _IDF_UNKNOWN)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _query_tokens(text: str, terms) -> list[str]:
|
|
107
|
+
toks = _lex.normalize(text, query=True)
|
|
108
|
+
if terms:
|
|
109
|
+
for extra in _lex.normalize(" ".join(str(t) for t in terms)):
|
|
110
|
+
if extra not in toks:
|
|
111
|
+
toks.append(extra)
|
|
112
|
+
return toks
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _score(qtokens, doc: _Doc) -> tuple[float, float]:
|
|
116
|
+
"""(ranking score, coverage) of a query against one doc."""
|
|
117
|
+
if not qtokens:
|
|
118
|
+
return 0.0, 0.0
|
|
119
|
+
matched = [t for t in qtokens if t in doc.tokens]
|
|
120
|
+
score = sum(_idf(t) for t in matched)
|
|
121
|
+
denom = sum(_idf(t) for t in qtokens)
|
|
122
|
+
coverage = (score / denom) if denom else 0.0
|
|
123
|
+
return score, coverage
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _rank(qtokens, kind: str) -> list[Hit]:
|
|
127
|
+
hits: list[Hit] = []
|
|
128
|
+
for d in _DOCS:
|
|
129
|
+
if d.kind != kind:
|
|
130
|
+
continue
|
|
131
|
+
score, cov = _score(qtokens, d)
|
|
132
|
+
if score > 0:
|
|
133
|
+
hits.append(Hit(kind, d.key, d.obj, score, cov))
|
|
134
|
+
hits.sort(key=lambda h: -h.score)
|
|
135
|
+
return hits
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _strength(*hit_lists) -> str:
|
|
139
|
+
best = max((h.coverage for hits in hit_lists for h in hits), default=0.0)
|
|
140
|
+
if best >= _STRONG:
|
|
141
|
+
return "strong"
|
|
142
|
+
if best >= _THIN:
|
|
143
|
+
return "thin"
|
|
144
|
+
return "empty"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# -- public L0 API ---------------------------------------------------------- #
|
|
148
|
+
def search_concepts(text: str, terms=None) -> list[Hit]:
|
|
149
|
+
return _rank(_query_tokens(text, terms or []), "concept")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def search_recipes(text: str, terms=None) -> list[Hit]:
|
|
153
|
+
return _rank(_query_tokens(text, terms or []), "recipe")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def best_concept(text: str) -> tuple[object | None, str]:
|
|
157
|
+
"""Top concept + its strength — used by the offline 'explain <topic>' path."""
|
|
158
|
+
hits = search_concepts(text)
|
|
159
|
+
if not hits:
|
|
160
|
+
return None, "empty"
|
|
161
|
+
return hits[0].obj, _strength(hits[:1])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# -- L1 expansion ----------------------------------------------------------- #
|
|
165
|
+
_EXPAND_PROMPT = (
|
|
166
|
+
"A student in a networks/cloud lab asked: {q!r}. Reply with ONLY a JSON array of up to 6 "
|
|
167
|
+
"lowercase keywords naming the networking/cloud/OS topic (e.g. [\"load balancer\", "
|
|
168
|
+
"\"round robin\"]). No prose."
|
|
169
|
+
)
|
|
170
|
+
_expand_cache: dict[str, list[str]] = {}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_array(text: str) -> list[str]:
|
|
174
|
+
m = re.search(r"\[.*?\]", text or "", re.S)
|
|
175
|
+
if not m:
|
|
176
|
+
return []
|
|
177
|
+
try:
|
|
178
|
+
arr = json.loads(m.group(0))
|
|
179
|
+
except json.JSONDecodeError:
|
|
180
|
+
return []
|
|
181
|
+
return [str(x).strip() for x in arr if isinstance(x, (str, int)) and str(x).strip()]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def expand_query(text: str, llm) -> list[str]:
|
|
185
|
+
"""Ask the model to name the topic in GINI-ish keywords (cached). Returns [] on any error
|
|
186
|
+
so the caller degrades to L0."""
|
|
187
|
+
key = (text or "").strip().lower()
|
|
188
|
+
if key in _expand_cache:
|
|
189
|
+
return _expand_cache[key]
|
|
190
|
+
out: list[str] = []
|
|
191
|
+
try:
|
|
192
|
+
out = _parse_array(llm(_EXPAND_PROMPT.format(q=text)))
|
|
193
|
+
except Exception:
|
|
194
|
+
out = []
|
|
195
|
+
_expand_cache[key] = out
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# -- L2 embedding ----------------------------------------------------------- #
|
|
200
|
+
def _obj_for(doc_id: str):
|
|
201
|
+
kind, _, key = doc_id.partition(":")
|
|
202
|
+
if kind == "concept":
|
|
203
|
+
return "concept", key, _concepts.by_key(key)
|
|
204
|
+
if kind == "recipe":
|
|
205
|
+
return "recipe", key, _recipes.get_recipe(key)
|
|
206
|
+
return kind, key, None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# -- the orchestrator ------------------------------------------------------- #
|
|
210
|
+
def recall(text: str, terms=None, *, llm=None, embedder=None, deep: bool = False) -> RecallResult:
|
|
211
|
+
"""Run L0 → (L1 if thin) → (L2 if empty) and return ranked hits + strength."""
|
|
212
|
+
terms = list(terms or [])
|
|
213
|
+
chits = search_concepts(text, terms)
|
|
214
|
+
rhits = search_recipes(text, terms)
|
|
215
|
+
strength = _strength(chits, rhits)
|
|
216
|
+
used_expansion = used_embedding = False
|
|
217
|
+
|
|
218
|
+
if strength != "strong" and llm is not None:
|
|
219
|
+
extra = expand_query(text, llm)
|
|
220
|
+
if extra:
|
|
221
|
+
terms = terms + extra
|
|
222
|
+
chits = search_concepts(text, terms)
|
|
223
|
+
rhits = search_recipes(text, terms)
|
|
224
|
+
new_strength = _strength(chits, rhits)
|
|
225
|
+
used_expansion = new_strength != strength or bool(extra)
|
|
226
|
+
strength = new_strength
|
|
227
|
+
|
|
228
|
+
if strength == "empty" and embedder is not None and getattr(embedder, "available", lambda: False)():
|
|
229
|
+
seen_c = {h.key for h in chits}
|
|
230
|
+
seen_r = {h.key for h in rhits}
|
|
231
|
+
for doc_id, escore in embedder.query(text, k=4):
|
|
232
|
+
kind, key, obj = _obj_for(doc_id)
|
|
233
|
+
if obj is None:
|
|
234
|
+
continue
|
|
235
|
+
if kind == "concept" and key not in seen_c:
|
|
236
|
+
chits.append(Hit("concept", key, obj, float(escore), float(escore)))
|
|
237
|
+
seen_c.add(key)
|
|
238
|
+
used_embedding = True
|
|
239
|
+
elif kind == "recipe" and key not in seen_r:
|
|
240
|
+
rhits.append(Hit("recipe", key, obj, float(escore), float(escore)))
|
|
241
|
+
seen_r.add(key)
|
|
242
|
+
used_embedding = True
|
|
243
|
+
if used_embedding:
|
|
244
|
+
chits.sort(key=lambda h: -h.score)
|
|
245
|
+
rhits.sort(key=lambda h: -h.score)
|
|
246
|
+
strength = "thin" # semantic-only matches are treated as partial grounding
|
|
247
|
+
|
|
248
|
+
return RecallResult(chits, rhits, strength, used_expansion, used_embedding)
|
gini/agent/session.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Session accumulator — the agent's working *domain* memory.
|
|
2
|
+
|
|
3
|
+
Distinct from dialogue history (which is turns of conversation): this holds the GINI
|
|
4
|
+
knowledge CARDS retrieved so far this session, so the agent builds up understanding and can
|
|
5
|
+
reason over earlier facts on a later, deeper question. Cards are deduped by (kind, key) and
|
|
6
|
+
kept most-recent-last. When the block outgrows its budget the oldest cards are folded into a
|
|
7
|
+
running summary via a small-LLM pass (the chosen compaction) — with a bounded rule-based
|
|
8
|
+
fallback when no model is available.
|
|
9
|
+
|
|
10
|
+
Pure logic + an injectable `llm` callable(prompt)->str, so it's testable offline.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .kb import Card
|
|
15
|
+
|
|
16
|
+
_SUMMARY_PROMPT = (
|
|
17
|
+
"Compress these GINI facts a student has already seen this session into a few compact "
|
|
18
|
+
"sentences. Keep element names and the key 'how it works' points; drop repetition. "
|
|
19
|
+
"Reply with only the summary.\n\n"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SessionKnowledge:
|
|
24
|
+
def __init__(self, max_chars: int = 6000, keep_recent: int = 8) -> None:
|
|
25
|
+
self.cards: list[Card] = [] # most-recent last
|
|
26
|
+
self.summary: str = ""
|
|
27
|
+
self.max_chars = max_chars
|
|
28
|
+
self.keep_recent = keep_recent
|
|
29
|
+
|
|
30
|
+
# -- accumulate --------------------------------------------------------- #
|
|
31
|
+
def add(self, cards, llm=None) -> None:
|
|
32
|
+
"""Fold newly-retrieved cards in (dedup by kind+key, move-to-end), then compact."""
|
|
33
|
+
for c in cards or []:
|
|
34
|
+
self.cards = [x for x in self.cards if (x.kind, x.key) != (c.kind, c.key)]
|
|
35
|
+
self.cards.append(c)
|
|
36
|
+
self.compact(llm)
|
|
37
|
+
|
|
38
|
+
def _size(self) -> int:
|
|
39
|
+
return len(self.summary) + sum(len(c.text) for c in self.cards)
|
|
40
|
+
|
|
41
|
+
# -- compaction --------------------------------------------------------- #
|
|
42
|
+
def compact(self, llm=None) -> None:
|
|
43
|
+
"""Keep the block under budget by summarising the oldest cards into `summary`."""
|
|
44
|
+
guard = 0
|
|
45
|
+
while self._size() > self.max_chars and len(self.cards) > 1 and guard < 20:
|
|
46
|
+
guard += 1
|
|
47
|
+
keep = min(self.keep_recent, max(1, len(self.cards) - 1))
|
|
48
|
+
overflow = self.cards[:-keep]
|
|
49
|
+
if not overflow: # recent cards alone exceed budget
|
|
50
|
+
overflow = self.cards[:1]
|
|
51
|
+
self.summary = self._summarize(self.summary,
|
|
52
|
+
"\n".join(c.text for c in overflow), llm)
|
|
53
|
+
self.cards = self.cards[len(overflow):]
|
|
54
|
+
# last resort: even the summary is too big
|
|
55
|
+
if len(self.summary) > self.max_chars:
|
|
56
|
+
self.summary = self.summary[-self.max_chars:]
|
|
57
|
+
|
|
58
|
+
def _summarize(self, prior: str, new: str, llm) -> str:
|
|
59
|
+
if llm is not None:
|
|
60
|
+
try:
|
|
61
|
+
out = (llm(_SUMMARY_PROMPT + (prior + "\n" if prior else "") + new) or "").strip()
|
|
62
|
+
if out:
|
|
63
|
+
return out
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
combined = (prior + " " + new).strip()
|
|
67
|
+
return combined[-(self.max_chars // 2):] # bounded rule-based fallback
|
|
68
|
+
|
|
69
|
+
# -- render ------------------------------------------------------------- #
|
|
70
|
+
def as_context(self) -> str:
|
|
71
|
+
parts = []
|
|
72
|
+
if self.summary:
|
|
73
|
+
parts.append("Session knowledge so far: " + self.summary)
|
|
74
|
+
if self.cards:
|
|
75
|
+
parts.append("\n".join("- " + c.text for c in self.cards))
|
|
76
|
+
return "\n".join(parts)
|
|
77
|
+
|
|
78
|
+
def clear(self) -> None:
|
|
79
|
+
self.cards, self.summary = [], ""
|