codelith 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/__init__.py +1 -0
- backend/agents/__init__.py +6 -0
- backend/agents/assessment_agent.py +273 -0
- backend/agents/coding_agent.py +779 -0
- backend/agents/concept_categories.py +131 -0
- backend/agents/concept_detector.py +1217 -0
- backend/agents/debug_agent.py +166 -0
- backend/agents/teacher_agent.py +179 -0
- backend/cli/__init__.py +1 -0
- backend/cli/config_cmd.py +135 -0
- backend/cli/main.py +606 -0
- backend/daemon/__init__.py +1 -0
- backend/daemon/launcher.py +243 -0
- backend/daemon/server.py +453 -0
- backend/daemon/state.py +110 -0
- backend/daemon/static/assets/Gambarino-Regular-BjbcsURA.otf +0 -0
- backend/daemon/static/assets/abnfDiagram-VCTEODGH-CCJBE2aE.js +1 -0
- backend/daemon/static/assets/arc-BEvzHx4o.js +1 -0
- backend/daemon/static/assets/architecture-7GRP2DOG-DaWrPggL.js +1 -0
- backend/daemon/static/assets/architectureDiagram-5GKGNRK7-pR-klcZv.js +36 -0
- backend/daemon/static/assets/array-BifhSqXX.js +1 -0
- backend/daemon/static/assets/blockDiagram-I7D4REHJ-C504Gj6_.js +129 -0
- backend/daemon/static/assets/c4Diagram-7LVT6UL2-BjM04Mni.js +38 -0
- backend/daemon/static/assets/channel-DzSauwD3.js +1 -0
- backend/daemon/static/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- backend/daemon/static/assets/chunk-4HAMMTFA-EgoP78tp.js +62 -0
- backend/daemon/static/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- backend/daemon/static/assets/chunk-75Z2AOVW-EXNbuzun.js +2 -0
- backend/daemon/static/assets/chunk-DU6HZSFF-CF3OK3MZ.js +127 -0
- backend/daemon/static/assets/chunk-F27PBJKO-G71ylWJa.js +1 -0
- backend/daemon/static/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- backend/daemon/static/assets/chunk-GMAD6QVW-2yfGg28o.js +72 -0
- backend/daemon/static/assets/chunk-GVQU2GXP-C_VeaX4U.js +1 -0
- backend/daemon/static/assets/chunk-IMKFNOWR-CNexRjjn.js +231 -0
- backend/daemon/static/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- backend/daemon/static/assets/chunk-P2QGCYS3-E4AByfsD.js +1 -0
- backend/daemon/static/assets/chunk-POPQ4Y6H-Bisbc2-3.js +1 -0
- backend/daemon/static/assets/chunk-PWAF6VOD-DaoPxZAa.js +1 -0
- backend/daemon/static/assets/chunk-SHT3W25Y-DarPToto.js +168 -0
- backend/daemon/static/assets/chunk-SVP7TREG-DvMOAiwI.js +88 -0
- backend/daemon/static/assets/chunk-TICWLB2K-DheuvyGM.js +206 -0
- backend/daemon/static/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
- backend/daemon/static/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- backend/daemon/static/assets/classDiagram-ZZMXUADV-Ys5zkCXW.js +1 -0
- backend/daemon/static/assets/classDiagram-v2-VYDZK3BY-Ys5zkCXW.js +1 -0
- backend/daemon/static/assets/cose-bilkent-JH36ORCC-DLPLnxrP.js +1 -0
- backend/daemon/static/assets/cynefin-OW5HDTMX-Dv1OY_0y.js +1 -0
- backend/daemon/static/assets/cynefinDiagram-5FMLGOSQ-Ur7MTCmF.js +62 -0
- backend/daemon/static/assets/cytoscape.esm-CECbKnxF.js +321 -0
- backend/daemon/static/assets/dagre-CJLTJMFW.js +1 -0
- backend/daemon/static/assets/dagre-GXQ25YYZ-R3BwTvng.js +4 -0
- backend/daemon/static/assets/defaultLocale-BFoDCU3G.js +1 -0
- backend/daemon/static/assets/diagram-S7CK7UJ4-BxIoEKb4.js +30 -0
- backend/daemon/static/assets/diagram-UQ7AKVKN-DO4cuWN-.js +41 -0
- backend/daemon/static/assets/diagram-VSXAHHWV-DW5imp5t.js +3 -0
- backend/daemon/static/assets/diagram-VX7I27RA-CdZ3k7wQ.js +24 -0
- backend/daemon/static/assets/diagram-Z3DM3KII-DPyjbneL.js +24 -0
- backend/daemon/static/assets/dist-DTg6UBE_.js +1 -0
- backend/daemon/static/assets/ebnfDiagram-PWID7BFC-BO7VQsye.js +1 -0
- backend/daemon/static/assets/erDiagram-RLTQ6QDP-CevvjECq.js +99 -0
- backend/daemon/static/assets/eventmodeling-NTZA5JFV-yNfKR6-v.js +1 -0
- backend/daemon/static/assets/flowDiagram-HODETNUW-B4GT41mU.js +1 -0
- backend/daemon/static/assets/ganttDiagram-EL5Y4UJY-DNW5fWw1.js +292 -0
- backend/daemon/static/assets/gitGraph-4MIJSDKK-DKgVkWaZ.js +1 -0
- backend/daemon/static/assets/gitGraphDiagram-WWUBYQGX-0S7OF9Aj.js +106 -0
- backend/daemon/static/assets/index-D3vj8REa.js +63 -0
- backend/daemon/static/assets/index-D4lMFaiv.css +1 -0
- backend/daemon/static/assets/info-A6RAGUB7-Bxy-SzRN.js +1 -0
- backend/daemon/static/assets/infoDiagram-27XIBGKW-ClzQji6X.js +2 -0
- backend/daemon/static/assets/init-C-OQMol4.js +1 -0
- backend/daemon/static/assets/ishikawaDiagram-5VMMS53U-B3Lo-sS3.js +70 -0
- backend/daemon/static/assets/journeyDiagram-3NMN7TZE-0KL6R2Rz.js +139 -0
- backend/daemon/static/assets/kanban-definition-UXKFOSKX-zt5NbEep.js +89 -0
- backend/daemon/static/assets/katex-CXMH3UgJ.js +257 -0
- backend/daemon/static/assets/line-CiAFRJVJ.js +1 -0
- backend/daemon/static/assets/linear-BI6yqEPV.js +1 -0
- backend/daemon/static/assets/logo_darkmode-BPDdj6GZ.png +0 -0
- backend/daemon/static/assets/logo_lightmode-C3ZWMgAH.png +0 -0
- backend/daemon/static/assets/mermaid-parser.core-DEadI1Ja.js +7 -0
- backend/daemon/static/assets/mindmap-definition-YA3MSWOX-TGKGYg5n.js +96 -0
- backend/daemon/static/assets/ordinal-BDEzSJ7C.js +1 -0
- backend/daemon/static/assets/packet-AYTQ26CC-CZTSuh5x.js +1 -0
- backend/daemon/static/assets/path-fybaL0A-.js +1 -0
- backend/daemon/static/assets/pegDiagram-XKGWAZYB-DGd8LACA.js +1 -0
- backend/daemon/static/assets/pie-WAS4IAKB-B59sPr3Z.js +1 -0
- backend/daemon/static/assets/pieDiagram-E7YTZNPT-CpwxCR3L.js +39 -0
- backend/daemon/static/assets/quadrantDiagram-AXDQQJYC-BwSeF_E_.js +7 -0
- backend/daemon/static/assets/radar-RG4KPBEZ-DAa4JvTb.js +1 -0
- backend/daemon/static/assets/railroad-74A4TZTK-BitdNgDt.js +1 -0
- backend/daemon/static/assets/railroad-abnf-HS5TGJTU-DCrNKqAH.js +1 -0
- backend/daemon/static/assets/railroad-ebnf-LZEXJU2U-DmEwx8OK.js +1 -0
- backend/daemon/static/assets/railroad-peg-WCYAUIDC-CPc8dTCP.js +1 -0
- backend/daemon/static/assets/railroadDiagram-O6MQD6OU-DuizuzwD.js +1 -0
- backend/daemon/static/assets/requirementDiagram-BXWQKSXE-BjMk0yS8.js +84 -0
- backend/daemon/static/assets/rough.esm-Dy-Kn_BL.js +1 -0
- backend/daemon/static/assets/sankeyDiagram-P5KCCOFB-0T_bhkmz.js +40 -0
- backend/daemon/static/assets/sequenceDiagram-WJ2MYXX4-Cwa-1Stp.js +162 -0
- backend/daemon/static/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- backend/daemon/static/assets/src-BH-TyZbA.js +1 -0
- backend/daemon/static/assets/stateDiagram-D77RDMKH-BpQSg_QL.js +1 -0
- backend/daemon/static/assets/stateDiagram-v2-MP3YSRHH-BItVXKof.js +1 -0
- backend/daemon/static/assets/swimlanes-42K2YHIH-h_ED18Vy.js +1 -0
- backend/daemon/static/assets/swimlanesDiagram-VR7AAH4N-D0fo0LN-.js +8 -0
- backend/daemon/static/assets/timeline-definition-24CTP7MA-DKfSO33a.js +120 -0
- backend/daemon/static/assets/treeView-Q6P3EWNA-DAj9fxfC.js +1 -0
- backend/daemon/static/assets/treemap-WGGIJYW6-5IIXD9Zu.js +1 -0
- backend/daemon/static/assets/vennDiagram-4TSXK5OY-BoBvVEci.js +34 -0
- backend/daemon/static/assets/wardley-WFR3VGLG-CGsd7s_-.js +1 -0
- backend/daemon/static/assets/wardleyDiagram-VM6X3IG4-QHdK5NsY.js +78 -0
- backend/daemon/static/assets/xychartDiagram-S5SC5T6Z-MN_fdKCJ.js +7 -0
- backend/daemon/static/index.html +49 -0
- backend/database/__init__.py +1 -0
- backend/database/concept_slug.py +39 -0
- backend/database/concepts.py +804 -0
- backend/llm/__init__.py +5 -0
- backend/llm/client.py +333 -0
- backend/llm/config.py +254 -0
- backend/llm/key_setup.py +237 -0
- backend/main.py +13 -0
- backend/orchestrator/__init__.py +1 -0
- backend/orchestrator/events.py +52 -0
- backend/orchestrator/graph.py +316 -0
- backend/orchestrator/modes.py +125 -0
- codelith-0.1.0.dist-info/METADATA +301 -0
- codelith-0.1.0.dist-info/RECORD +129 -0
- codelith-0.1.0.dist-info/WHEEL +5 -0
- codelith-0.1.0.dist-info/entry_points.txt +2 -0
- codelith-0.1.0.dist-info/licenses/LICENSE +21 -0
- codelith-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
"""Concept detection — shared engine used by the assessment and teacher agents.
|
|
2
|
+
|
|
3
|
+
This module owns the full detection pipeline:
|
|
4
|
+
|
|
5
|
+
1. **Registry scan** — file contents written/edited by the coding agent are
|
|
6
|
+
matched against :data:`CONCEPT_PATTERNS`, a curated registry of known
|
|
7
|
+
patterns, each with a name, category, description, and (usually) a Mermaid
|
|
8
|
+
diagram.
|
|
9
|
+
2. **LLM detection** — for code the registry does not recognise, the LLM
|
|
10
|
+
identifies additional concepts (mode-gated: skipped when
|
|
11
|
+
``llm_detection`` is off since it costs an LLM call per written file).
|
|
12
|
+
|
|
13
|
+
The graph runs this once per turn in the ``detect_concepts`` node and stores
|
|
14
|
+
the result in ``state["concepts_detected"]``. The assessment and teacher
|
|
15
|
+
agents then read that shared result instead of each re-scanning
|
|
16
|
+
``tool_calls_log`` — halving the LLM calls per turn and keeping the teacher
|
|
17
|
+
agent's rich per-concept diagram for the dashboard.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from backend.agents.concept_categories import (
|
|
28
|
+
is_valid_category,
|
|
29
|
+
normalize_category,
|
|
30
|
+
)
|
|
31
|
+
from backend.database.concept_slug import concept_slug
|
|
32
|
+
from backend.database.concepts import (
|
|
33
|
+
content_hash,
|
|
34
|
+
file_scan_cached,
|
|
35
|
+
get_cached_diagram,
|
|
36
|
+
)
|
|
37
|
+
from backend.llm.client import resolve_api_key, get_client
|
|
38
|
+
from backend.llm.config import get_model
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Concept registry — known patterns and their short explanations
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
CONCEPT_PATTERNS: dict[str, dict[str, str]] = {
|
|
45
|
+
# React hooks
|
|
46
|
+
"useEffect": {
|
|
47
|
+
"name": "useEffect",
|
|
48
|
+
"category": "api",
|
|
49
|
+
"subcategory": "React Hook",
|
|
50
|
+
"description": (
|
|
51
|
+
"A React hook that runs side effects after render. "
|
|
52
|
+
"Commonly used for data fetching, subscriptions, and DOM manipulation. "
|
|
53
|
+
"Accepts a cleanup function and a dependency array to control re-runs."
|
|
54
|
+
),
|
|
55
|
+
"diagram": (
|
|
56
|
+
"flowchart LR\n"
|
|
57
|
+
" R[Render] --> E[Run effect]\n"
|
|
58
|
+
" E --> D{deps changed?}\n"
|
|
59
|
+
" D -- yes --> E\n"
|
|
60
|
+
" D -- no --> S[Skip]\n"
|
|
61
|
+
" E --> C[Cleanup fn]\n"
|
|
62
|
+
" C --> R"
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
"useState": {
|
|
66
|
+
"name": "useState",
|
|
67
|
+
"category": "api",
|
|
68
|
+
"subcategory": "React Hook",
|
|
69
|
+
"description": (
|
|
70
|
+
"A React hook that adds state to a functional component. "
|
|
71
|
+
"Returns a state value and a setter function. "
|
|
72
|
+
"Re-renders the component when the state changes."
|
|
73
|
+
),
|
|
74
|
+
"diagram": (
|
|
75
|
+
"flowchart LR\n"
|
|
76
|
+
" S[Current state] --> C[Component renders]\n"
|
|
77
|
+
" C --> U[User event calls setter]\n"
|
|
78
|
+
" U --> N[State updated]\n"
|
|
79
|
+
" N --> C"
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
"useMemo": {
|
|
83
|
+
"name": "useMemo",
|
|
84
|
+
"category": "api",
|
|
85
|
+
"subcategory": "React Hook",
|
|
86
|
+
"description": (
|
|
87
|
+
"A React hook that memoizes an expensive computation. "
|
|
88
|
+
"Only recalculates when its dependencies change, "
|
|
89
|
+
"preventing unnecessary re-renders."
|
|
90
|
+
),
|
|
91
|
+
"diagram": (
|
|
92
|
+
"flowchart LR\n"
|
|
93
|
+
" R[Render] --> Q{deps changed?}\n"
|
|
94
|
+
" Q -- yes --> F[Recompute value]\n"
|
|
95
|
+
" Q -- no --> M[Reuse cached value]\n"
|
|
96
|
+
" F --> M"
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
"useCallback": {
|
|
100
|
+
"name": "useCallback",
|
|
101
|
+
"category": "api",
|
|
102
|
+
"subcategory": "React Hook",
|
|
103
|
+
"description": (
|
|
104
|
+
"A React hook that memoizes a callback function. "
|
|
105
|
+
"Useful when passing callbacks to child components that "
|
|
106
|
+
"depend on referential equality."
|
|
107
|
+
),
|
|
108
|
+
"diagram": (
|
|
109
|
+
"flowchart LR\n"
|
|
110
|
+
" R[Render] --> Q{deps changed?}\n"
|
|
111
|
+
" Q -- yes --> F[New function identity]\n"
|
|
112
|
+
" Q -- no --> M[Same function identity]\n"
|
|
113
|
+
" M --> P[Child skips re-render]\n"
|
|
114
|
+
" F --> P"
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
"useRef": {
|
|
118
|
+
"name": "useRef",
|
|
119
|
+
"category": "api",
|
|
120
|
+
"subcategory": "React Hook",
|
|
121
|
+
"description": (
|
|
122
|
+
"A React hook that creates a mutable ref object. "
|
|
123
|
+
"Persists across renders without causing re-renders. "
|
|
124
|
+
"Commonly used for DOM access and storing previous values."
|
|
125
|
+
),
|
|
126
|
+
"diagram": (
|
|
127
|
+
"flowchart LR\n"
|
|
128
|
+
" R[Render] --> B[Read ref.current]\n"
|
|
129
|
+
" B --> W[Write ref.current]\n"
|
|
130
|
+
" W --> N[No re-render] --> R"
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
"useContext": {
|
|
134
|
+
"name": "useContext",
|
|
135
|
+
"category": "api",
|
|
136
|
+
"subcategory": "React Hook",
|
|
137
|
+
"description": (
|
|
138
|
+
"A React hook that reads values from the nearest Context Provider. "
|
|
139
|
+
"Avoids prop drilling by letting components access shared state."
|
|
140
|
+
),
|
|
141
|
+
"diagram": (
|
|
142
|
+
"flowchart TD\n"
|
|
143
|
+
" P[Provider holds value] --> A[Component A]\n"
|
|
144
|
+
" A --> B[Component B]\n"
|
|
145
|
+
" B --> C[useContext reads value]\n"
|
|
146
|
+
" C -.no prop drilling.-> P"
|
|
147
|
+
),
|
|
148
|
+
},
|
|
149
|
+
# JavaScript / TypeScript patterns
|
|
150
|
+
"async function": {
|
|
151
|
+
"name": "Async/Await",
|
|
152
|
+
"category": "abstract",
|
|
153
|
+
"subcategory": "Asynchronous Pattern",
|
|
154
|
+
"description": (
|
|
155
|
+
"Syntactic sugar over Promises. An async function returns a "
|
|
156
|
+
"Promise and can use 'await' to pause until a Promise resolves, "
|
|
157
|
+
"making asynchronous code read like synchronous code."
|
|
158
|
+
),
|
|
159
|
+
"diagram": (
|
|
160
|
+
"sequenceDiagram\n"
|
|
161
|
+
" participant C as Caller\n"
|
|
162
|
+
" participant F as async fn\n"
|
|
163
|
+
" C->>F: call (returns Promise)\n"
|
|
164
|
+
" F-->>C: promise pending\n"
|
|
165
|
+
" Note over F: awaits I/O...\n"
|
|
166
|
+
" F-->>C: resolve with value\n"
|
|
167
|
+
" C->>C: continue after await"
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
"Promise": {
|
|
171
|
+
"name": "Promises",
|
|
172
|
+
"category": "abstract",
|
|
173
|
+
"subcategory": "Asynchronous Pattern",
|
|
174
|
+
"description": (
|
|
175
|
+
"An object representing the eventual completion or failure of "
|
|
176
|
+
"an asynchronous operation. Chains of .then()/.catch() handle "
|
|
177
|
+
"success and error paths."
|
|
178
|
+
),
|
|
179
|
+
"diagram": (
|
|
180
|
+
"stateDiagram-v2\n"
|
|
181
|
+
" [*] --> pending\n"
|
|
182
|
+
" pending --> fulfilled: .then()\n"
|
|
183
|
+
" pending --> rejected: error\n"
|
|
184
|
+
" rejected --> [*]: .catch()\n"
|
|
185
|
+
" fulfilled --> [*]"
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
"export default": {
|
|
189
|
+
"name": "Default Export",
|
|
190
|
+
"category": "abstract",
|
|
191
|
+
"subcategory": "Module System",
|
|
192
|
+
"description": (
|
|
193
|
+
"ES module syntax that marks one value as the module's primary "
|
|
194
|
+
"export. Importers can name it anything: import Foo from './mod'."
|
|
195
|
+
),
|
|
196
|
+
"diagram": (
|
|
197
|
+
"flowchart LR\n"
|
|
198
|
+
" M[Module] -- default export --> V[One primary value]\n"
|
|
199
|
+
" V --> I1[import Foo from mod]\n"
|
|
200
|
+
" V --> I2[import Bar from mod]\n"
|
|
201
|
+
" I1 -.any name works.-> V"
|
|
202
|
+
),
|
|
203
|
+
},
|
|
204
|
+
"interface ": {
|
|
205
|
+
"name": "TypeScript Interface",
|
|
206
|
+
"category": "data_model",
|
|
207
|
+
"subcategory": "TypeScript",
|
|
208
|
+
"description": (
|
|
209
|
+
"Defines the shape of an object — its properties and their types. "
|
|
210
|
+
"Interfaces are checked at compile time and erased in the "
|
|
211
|
+
"generated JavaScript."
|
|
212
|
+
),
|
|
213
|
+
"diagram": (
|
|
214
|
+
"erDiagram\n"
|
|
215
|
+
" USER {\n"
|
|
216
|
+
" string id\n"
|
|
217
|
+
" string email\n"
|
|
218
|
+
" }\n"
|
|
219
|
+
" ORDER ||--o{ USER : placed-by"
|
|
220
|
+
),
|
|
221
|
+
},
|
|
222
|
+
"type ": {
|
|
223
|
+
"name": "TypeScript Type Alias",
|
|
224
|
+
"category": "data_model",
|
|
225
|
+
"subcategory": "TypeScript",
|
|
226
|
+
"description": (
|
|
227
|
+
"Gives a name to a type expression (union, intersection, object, "
|
|
228
|
+
"primitive). Unlike interfaces, type aliases can represent "
|
|
229
|
+
"unions and mapped types."
|
|
230
|
+
),
|
|
231
|
+
"diagram": (
|
|
232
|
+
"erDiagram\n"
|
|
233
|
+
" USER {\n"
|
|
234
|
+
" string id\n"
|
|
235
|
+
" string status\n"
|
|
236
|
+
" }\n"
|
|
237
|
+
" USER ||--o| PROFILE : one-to-optional"
|
|
238
|
+
),
|
|
239
|
+
},
|
|
240
|
+
# Python patterns
|
|
241
|
+
"def __init__": {
|
|
242
|
+
"name": "__init__ (Constructor)",
|
|
243
|
+
"category": "structure",
|
|
244
|
+
"subcategory": "Python OOP",
|
|
245
|
+
"description": (
|
|
246
|
+
"The constructor method for a Python class. Called when a new "
|
|
247
|
+
"instance is created. Initializes the object's attributes."
|
|
248
|
+
),
|
|
249
|
+
"diagram": (
|
|
250
|
+
"classDiagram\n"
|
|
251
|
+
" class Animal {\n"
|
|
252
|
+
" +String name\n"
|
|
253
|
+
" +__init__(name)\n"
|
|
254
|
+
" +speak()\n"
|
|
255
|
+
" }\n"
|
|
256
|
+
" Animal : constructor initializes attributes"
|
|
257
|
+
),
|
|
258
|
+
},
|
|
259
|
+
"async def": {
|
|
260
|
+
"name": "Python Async Functions",
|
|
261
|
+
"category": "abstract",
|
|
262
|
+
"subcategory": "Asynchronous Pattern",
|
|
263
|
+
"description": (
|
|
264
|
+
"Defines a coroutine that can be awaited. Used with asyncio "
|
|
265
|
+
"for non-blocking I/O operations like network requests and "
|
|
266
|
+
"file access."
|
|
267
|
+
),
|
|
268
|
+
"diagram": (
|
|
269
|
+
"sequenceDiagram\n"
|
|
270
|
+
" participant L as Event loop\n"
|
|
271
|
+
" participant C as Coroutine\n"
|
|
272
|
+
" L->>C: start coroutine\n"
|
|
273
|
+
" C-->>L: await I/O (yield)\n"
|
|
274
|
+
" L->>C: I/O done, resume\n"
|
|
275
|
+
" C-->>L: return result"
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
"decorator": {
|
|
279
|
+
"name": "Decorators",
|
|
280
|
+
"category": "abstract",
|
|
281
|
+
"subcategory": "Python Pattern",
|
|
282
|
+
"description": (
|
|
283
|
+
"Functions that modify other functions or classes. Applied with "
|
|
284
|
+
"@syntax above the target. Common uses: logging, caching, "
|
|
285
|
+
"authentication checks."
|
|
286
|
+
),
|
|
287
|
+
"diagram": (
|
|
288
|
+
"sequenceDiagram\n"
|
|
289
|
+
" participant C as Caller\n"
|
|
290
|
+
" participant W as Wrapper\n"
|
|
291
|
+
" participant F as Original fn\n"
|
|
292
|
+
" C->>W: call decorated fn\n"
|
|
293
|
+
" W->>F: delegate\n"
|
|
294
|
+
" F-->>W: result\n"
|
|
295
|
+
" W-->>C: result (plus extra behavior)"
|
|
296
|
+
),
|
|
297
|
+
},
|
|
298
|
+
# General patterns
|
|
299
|
+
"for ": {
|
|
300
|
+
"name": "For Loops",
|
|
301
|
+
"category": "algorithm",
|
|
302
|
+
"subcategory": "Control Flow",
|
|
303
|
+
"description": (
|
|
304
|
+
"Definite iteration over a sequence (range, list, string, or "
|
|
305
|
+
"iterator). The loop variable takes each element in turn; "
|
|
306
|
+
"'break' and 'continue' control the flow."
|
|
307
|
+
),
|
|
308
|
+
"diagram": (
|
|
309
|
+
"flowchart LR\n"
|
|
310
|
+
" S[Start] --> C{more items?}\n"
|
|
311
|
+
" C -- yes --> B[run body] --> N[next item] --> C\n"
|
|
312
|
+
" C -- no --> D[done]"
|
|
313
|
+
),
|
|
314
|
+
},
|
|
315
|
+
"while ": {
|
|
316
|
+
"name": "While Loops",
|
|
317
|
+
"category": "algorithm",
|
|
318
|
+
"subcategory": "Control Flow",
|
|
319
|
+
"description": (
|
|
320
|
+
"Indefinite iteration: the body runs as long as the condition "
|
|
321
|
+
"holds. Requires the condition to eventually become false "
|
|
322
|
+
"(or a break) to avoid an infinite loop."
|
|
323
|
+
),
|
|
324
|
+
"diagram": (
|
|
325
|
+
"flowchart LR\n"
|
|
326
|
+
" S[Start] --> C{condition true?}\n"
|
|
327
|
+
" C -- yes --> B[run body] --> C\n"
|
|
328
|
+
" C -- no --> D[done]"
|
|
329
|
+
),
|
|
330
|
+
},
|
|
331
|
+
"class ": {
|
|
332
|
+
"name": "Classes / OOP",
|
|
333
|
+
"category": "structure",
|
|
334
|
+
"subcategory": "Object-Oriented Programming",
|
|
335
|
+
"description": (
|
|
336
|
+
"Blueprints for creating objects. Combine state (attributes) "
|
|
337
|
+
"and behavior (methods) into a single unit. Support "
|
|
338
|
+
"inheritance, encapsulation, and polymorphism."
|
|
339
|
+
),
|
|
340
|
+
"diagram": (
|
|
341
|
+
"classDiagram\n"
|
|
342
|
+
" class Animal {\n"
|
|
343
|
+
" +String name\n"
|
|
344
|
+
" +speak()\n"
|
|
345
|
+
" }\n"
|
|
346
|
+
" Animal <|-- Dog : inheritance\n"
|
|
347
|
+
" Animal : +attributes\n"
|
|
348
|
+
" Animal : +methods()"
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
"try:": {
|
|
352
|
+
"name": "Try/Except (Error Handling)",
|
|
353
|
+
"category": "abstract",
|
|
354
|
+
"subcategory": "Error Handling",
|
|
355
|
+
"description": (
|
|
356
|
+
"Gracefully handles runtime errors. Code in the 'try' block "
|
|
357
|
+
"runs normally; if an exception occurs, control jumps to "
|
|
358
|
+
"'except' instead of crashing."
|
|
359
|
+
),
|
|
360
|
+
"diagram": (
|
|
361
|
+
"flowchart TD\n"
|
|
362
|
+
" T[Try block] --> E{exception?}\n"
|
|
363
|
+
" E -- no --> N[continue normally]\n"
|
|
364
|
+
" E -- yes --> X[Except block]\n"
|
|
365
|
+
" X --> N"
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
"switch": {
|
|
369
|
+
"name": "Switch Statement",
|
|
370
|
+
"category": "algorithm",
|
|
371
|
+
"subcategory": "Control Flow",
|
|
372
|
+
"description": (
|
|
373
|
+
"Multi-way branching on one value. Each 'case' matches a "
|
|
374
|
+
"possible value and runs its block; 'default' handles anything "
|
|
375
|
+
"else. Cleaner than long if/else-if chains on the same value."
|
|
376
|
+
),
|
|
377
|
+
"diagram": (
|
|
378
|
+
"flowchart TD\n"
|
|
379
|
+
" V[Value to match] --> C1{case 1}\n"
|
|
380
|
+
" C1 -- match --> B1[Run block 1]\n"
|
|
381
|
+
" C1 -- no --> C2{case 2}\n"
|
|
382
|
+
" C2 -- match --> B2[Run block 2]\n"
|
|
383
|
+
" C2 -- no --> D[Default block]"
|
|
384
|
+
),
|
|
385
|
+
},
|
|
386
|
+
" ? ": {
|
|
387
|
+
"name": "Ternary Operator",
|
|
388
|
+
"category": "algorithm",
|
|
389
|
+
"subcategory": "Control Flow",
|
|
390
|
+
"description": (
|
|
391
|
+
"Inline conditional: 'condition ? a : b' evaluates to 'a' when "
|
|
392
|
+
"the condition is true, otherwise 'b'. A compact alternative "
|
|
393
|
+
"to a two-branch if/else used inside expressions."
|
|
394
|
+
),
|
|
395
|
+
"diagram": (
|
|
396
|
+
"flowchart LR\n"
|
|
397
|
+
" C{condition?} -- true --> A[value a]\n"
|
|
398
|
+
" C -- false --> B[value b]\n"
|
|
399
|
+
" A --> R[result]\n"
|
|
400
|
+
" B --> R"
|
|
401
|
+
),
|
|
402
|
+
},
|
|
403
|
+
"import ": {
|
|
404
|
+
"name": "Imports / Modules",
|
|
405
|
+
"category": "abstract",
|
|
406
|
+
"subcategory": "Module System",
|
|
407
|
+
"description": (
|
|
408
|
+
"Brings code from other files or packages into the current "
|
|
409
|
+
"namespace. Enables code reuse and separation of concerns."
|
|
410
|
+
),
|
|
411
|
+
"diagram": (
|
|
412
|
+
"flowchart LR\n"
|
|
413
|
+
" M[Other module] --> N[import brings names here]\n"
|
|
414
|
+
" N --> U[Use without redefining]\n"
|
|
415
|
+
" P[Package] --> N"
|
|
416
|
+
),
|
|
417
|
+
},
|
|
418
|
+
"lambda": {
|
|
419
|
+
"name": "Lambda Functions",
|
|
420
|
+
"category": "algorithm",
|
|
421
|
+
"subcategory": "Functional Programming",
|
|
422
|
+
"description": (
|
|
423
|
+
"Anonymous, inline functions defined with the 'lambda' keyword. "
|
|
424
|
+
"Useful for short callbacks in map(), filter(), and sorted()."
|
|
425
|
+
),
|
|
426
|
+
"diagram": (
|
|
427
|
+
"flowchart LR\n"
|
|
428
|
+
" A[lambda x: expression] --> C[Passed as callback]\n"
|
|
429
|
+
" C --> H[HOF: map, filter, sorted]\n"
|
|
430
|
+
" H --> R[Result per element]"
|
|
431
|
+
),
|
|
432
|
+
},
|
|
433
|
+
"map(": {
|
|
434
|
+
"name": "map()",
|
|
435
|
+
"category": "algorithm",
|
|
436
|
+
"subcategory": "Functional Programming",
|
|
437
|
+
"description": (
|
|
438
|
+
"Applies a function to every element of an iterable, returning "
|
|
439
|
+
"a new iterable of results. Often combined with list() to "
|
|
440
|
+
"produce a list."
|
|
441
|
+
),
|
|
442
|
+
"diagram": (
|
|
443
|
+
"flowchart LR\n"
|
|
444
|
+
" I[1, 2, 3] --> M[map fn]\n"
|
|
445
|
+
" M --> O[fn 1, fn 2, fn 3]\n"
|
|
446
|
+
" O --> L[list gathers results]"
|
|
447
|
+
),
|
|
448
|
+
},
|
|
449
|
+
"filter(": {
|
|
450
|
+
"name": "filter()",
|
|
451
|
+
"category": "algorithm",
|
|
452
|
+
"subcategory": "Functional Programming",
|
|
453
|
+
"description": (
|
|
454
|
+
"Returns an iterable of elements for which the predicate "
|
|
455
|
+
"function returned True. Useful for selecting a subset of data."
|
|
456
|
+
),
|
|
457
|
+
"diagram": (
|
|
458
|
+
"flowchart LR\n"
|
|
459
|
+
" I[All elements] --> P{predicate true?}\n"
|
|
460
|
+
" P -- yes --> K[Kept]\n"
|
|
461
|
+
" P -- no --> D[Dropped]"
|
|
462
|
+
),
|
|
463
|
+
},
|
|
464
|
+
"querySelector": {
|
|
465
|
+
"name": "DOM Querying",
|
|
466
|
+
"category": "api",
|
|
467
|
+
"subcategory": "DOM / Browser API",
|
|
468
|
+
"description": (
|
|
469
|
+
"Selects a single element in the DOM using a CSS selector. "
|
|
470
|
+
"querySelectorAll() selects all matching elements."
|
|
471
|
+
),
|
|
472
|
+
"diagram": (
|
|
473
|
+
"flowchart TD\n"
|
|
474
|
+
" S[CSS selector string] --> D[DOM tree search]\n"
|
|
475
|
+
" D --> F[First matching element]\n"
|
|
476
|
+
" D --> A[querySelectorAll: all matches]"
|
|
477
|
+
),
|
|
478
|
+
},
|
|
479
|
+
"addEventListener": {
|
|
480
|
+
"name": "Event Listeners",
|
|
481
|
+
"category": "api",
|
|
482
|
+
"subcategory": "DOM / Browser API",
|
|
483
|
+
"description": (
|
|
484
|
+
"Registers a callback that runs when a specific event fires on "
|
|
485
|
+
"an element (click, submit, keydown, etc.). Crucial for "
|
|
486
|
+
"interactive web applications."
|
|
487
|
+
),
|
|
488
|
+
"diagram": (
|
|
489
|
+
"sequenceDiagram\n"
|
|
490
|
+
" participant U as User\n"
|
|
491
|
+
" participant E as Element\n"
|
|
492
|
+
" participant CB as Callback\n"
|
|
493
|
+
" U->>E: click / keydown\n"
|
|
494
|
+
" E->>CB: invoke listener\n"
|
|
495
|
+
" CB-->>E: handle event"
|
|
496
|
+
),
|
|
497
|
+
},
|
|
498
|
+
"fetch(": {
|
|
499
|
+
"name": "Fetch API",
|
|
500
|
+
"category": "api",
|
|
501
|
+
"subcategory": "Networking",
|
|
502
|
+
"description": (
|
|
503
|
+
"Makes HTTP requests from the browser or Node.js. Returns a "
|
|
504
|
+
"Promise that resolves to a Response object. Typically "
|
|
505
|
+
"combined with .json() to parse the body."
|
|
506
|
+
),
|
|
507
|
+
"diagram": (
|
|
508
|
+
"sequenceDiagram\n"
|
|
509
|
+
" participant A as App\n"
|
|
510
|
+
" participant N as Network\n"
|
|
511
|
+
" A->>N: fetch(url)\n"
|
|
512
|
+
" N-->>A: Promise<Response>\n"
|
|
513
|
+
" A->>N: response.json()\n"
|
|
514
|
+
" N-->>A: parsed data"
|
|
515
|
+
),
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@dataclass
|
|
521
|
+
class DetectedConcept:
|
|
522
|
+
"""A concept identified from code."""
|
|
523
|
+
|
|
524
|
+
name: str
|
|
525
|
+
category: str
|
|
526
|
+
description: str
|
|
527
|
+
# Human-readable sub-label (e.g. "React Hook") under the canonical
|
|
528
|
+
# taxonomy ``category`` (e.g. "api").
|
|
529
|
+
subcategory: str = ""
|
|
530
|
+
source_file: str = ""
|
|
531
|
+
line_range: tuple[int, int] = (0, 0)
|
|
532
|
+
# Optional Mermaid diagram definition rendered on the dashboard.
|
|
533
|
+
diagram: str = ""
|
|
534
|
+
# The author's core choice behind this concept — why THIS approach
|
|
535
|
+
# over the alternative, a trade-off accepted, a constraint honored.
|
|
536
|
+
# Empty for textbook-only concepts with no author choice, and for
|
|
537
|
+
# registry matches (the registry teaches the technique, not this
|
|
538
|
+
# file's use of it). Always populated for ``decisions`` concepts,
|
|
539
|
+
# where the choice itself is the concept.
|
|
540
|
+
decision: str = ""
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
# Mermaid syntax validation (lightweight, no parser dependency)
|
|
545
|
+
# ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
MERMAID_DIAGRAM_TYPES = (
|
|
548
|
+
"flowchart",
|
|
549
|
+
"classDiagram",
|
|
550
|
+
"sequenceDiagram",
|
|
551
|
+
"erDiagram",
|
|
552
|
+
"stateDiagram-v2",
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
_FENCE_RE = re.compile(r"^\s*```\w*\s*$", re.MULTILINE)
|
|
556
|
+
|
|
557
|
+
# One general mechanism for every structural check: replace each
|
|
558
|
+
# label/quoted span with a neutral placeholder, then validate the
|
|
559
|
+
# remaining "skeleton". A label span is a single-line, well-paired
|
|
560
|
+
# (...), [...] or {...} group with no nested delimiters; a quoted span
|
|
561
|
+
# honors ``\"`` escapes and may contain brackets of any kind. Content
|
|
562
|
+
# inside a span is exempt from balance checks — this one rule is what
|
|
563
|
+
# lets quotes inside labels (``A["say \"hi\""]``) and ER cardinality
|
|
564
|
+
# braces (``ORDER ||--o{ USER``) coexist with strict checks outside
|
|
565
|
+
# spans, with no per-character exemption logic to keep in sync.
|
|
566
|
+
_STRIP_SPAN_RE = re.compile(
|
|
567
|
+
r'"(?:\\.|[^"\\\n])*"' # quoted span, \" escapes honored
|
|
568
|
+
r"|\([^()\[\]{}\"\n]*\)" # (...) label
|
|
569
|
+
r"|\[[^()\[\]{}\"\n]*\]" # [...] label
|
|
570
|
+
r"|\{[^()\[\]{}\"\n]*\}" # {...} decision-node label
|
|
571
|
+
)
|
|
572
|
+
_PLACEHOLDER = "\uFFFD"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _strip_labels(diagram: str) -> str:
|
|
576
|
+
"""Stage 1: replace every label/quoted span with a placeholder.
|
|
577
|
+
|
|
578
|
+
Single left-to-right pass, quoted spans first so a quote inside a
|
|
579
|
+
label is consumed before label matching ever sees it. Spans cannot
|
|
580
|
+
cross newlines, so ER relation braces (``||--o{``) survive unpaired
|
|
581
|
+
on their line — correctly, since they are structural there.
|
|
582
|
+
"""
|
|
583
|
+
return _STRIP_SPAN_RE.sub(_PLACEHOLDER, diagram)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def is_valid_mermaid(diagram: str) -> bool:
|
|
587
|
+
"""Lightweight sanity check that *diagram* parses as Mermaid.
|
|
588
|
+
|
|
589
|
+
Two stages:
|
|
590
|
+
|
|
591
|
+
1. :func:`_strip_labels` extracts every ``[...]``/``(...)``/``{...}``
|
|
592
|
+
label span and quoted string into placeholders, producing a
|
|
593
|
+
skeleton of pure structural characters.
|
|
594
|
+
2. The skeleton is checked for: a routed diagram-type header,
|
|
595
|
+
balanced ``( )``/``[ ]`` brackets, braces only where they are
|
|
596
|
+
structural (ER relations, class attribute blocks), and no stray
|
|
597
|
+
double quotes outside spans.
|
|
598
|
+
|
|
599
|
+
Not a full parser — it catches the failure modes that make the
|
|
600
|
+
dashboard's Mermaid renderer show a syntax error instead of a
|
|
601
|
+
diagram. Empty/whitespace-only input is invalid: the save path
|
|
602
|
+
treats a blank diagram as "no diagram", but callers who backfill
|
|
603
|
+
need the distinction made explicit.
|
|
604
|
+
"""
|
|
605
|
+
if not isinstance(diagram, str) or not diagram.strip():
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
text = _FENCE_RE.sub("", diagram).strip()
|
|
609
|
+
if not text:
|
|
610
|
+
return False
|
|
611
|
+
skeleton = _strip_labels(text)
|
|
612
|
+
lines = skeleton.splitlines()
|
|
613
|
+
if not lines[0].strip().startswith(MERMAID_DIAGRAM_TYPES):
|
|
614
|
+
return False
|
|
615
|
+
|
|
616
|
+
first_word = lines[0].strip().split(None, 1)[0]
|
|
617
|
+
braces_structural = first_word in ("erDiagram", "classDiagram")
|
|
618
|
+
|
|
619
|
+
for line in lines:
|
|
620
|
+
stripped = line.strip()
|
|
621
|
+
if not stripped or stripped.startswith("%%"):
|
|
622
|
+
continue
|
|
623
|
+
|
|
624
|
+
if not braces_structural and ("{" in stripped or "}" in stripped):
|
|
625
|
+
# Braces surviving the strip are only legal in ER relations
|
|
626
|
+
# and class attribute blocks.
|
|
627
|
+
return False
|
|
628
|
+
|
|
629
|
+
depth = 0
|
|
630
|
+
for ch in stripped:
|
|
631
|
+
if ch in "[(":
|
|
632
|
+
depth += 1
|
|
633
|
+
elif ch in ")]":
|
|
634
|
+
depth -= 1
|
|
635
|
+
if depth < 0:
|
|
636
|
+
return False # closer before opener
|
|
637
|
+
elif ch == '"':
|
|
638
|
+
return False # quote outside any span
|
|
639
|
+
if depth != 0:
|
|
640
|
+
return False # unclosed bracket
|
|
641
|
+
|
|
642
|
+
return True
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# ---------------------------------------------------------------------------
|
|
646
|
+
# Detection engine
|
|
647
|
+
# ---------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
def detect_concepts_from_file(file_path: str, content: str) -> list[DetectedConcept]:
|
|
650
|
+
"""Scan *content* for known concept patterns and return matches."""
|
|
651
|
+
concepts: list[DetectedConcept] = []
|
|
652
|
+
seen: set[str] = set()
|
|
653
|
+
|
|
654
|
+
lines = content.splitlines()
|
|
655
|
+
for line_no, line in enumerate(lines, start=1):
|
|
656
|
+
for pattern, info in CONCEPT_PATTERNS.items():
|
|
657
|
+
if pattern in line and info["name"] not in seen:
|
|
658
|
+
seen.add(info["name"])
|
|
659
|
+
concepts.append(
|
|
660
|
+
DetectedConcept(
|
|
661
|
+
name=info["name"],
|
|
662
|
+
category=info["category"],
|
|
663
|
+
subcategory=info.get("subcategory", ""),
|
|
664
|
+
description=info["description"],
|
|
665
|
+
source_file=file_path,
|
|
666
|
+
line_range=(line_no, line_no),
|
|
667
|
+
diagram=info.get("diagram", ""),
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
return concepts
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def detect_concepts_from_tool_calls(
|
|
675
|
+
tool_calls: list[dict[str, Any]],
|
|
676
|
+
) -> list[DetectedConcept]:
|
|
677
|
+
"""Analyze a batch of tool-call dicts (from the coding agent) and
|
|
678
|
+
return any detected concepts.
|
|
679
|
+
|
|
680
|
+
Each tool call is expected to have ``function.name`` and
|
|
681
|
+
``function.arguments`` (JSON string) with keys like ``file_path``
|
|
682
|
+
and ``content`` (for write_file) or ``old_string``/``new_string``
|
|
683
|
+
(for edit_file).
|
|
684
|
+
"""
|
|
685
|
+
concepts: list[DetectedConcept] = []
|
|
686
|
+
seen: set[str] = set()
|
|
687
|
+
|
|
688
|
+
for tc in tool_calls:
|
|
689
|
+
fn = tc.get("function", {})
|
|
690
|
+
name = fn.get("name", "")
|
|
691
|
+
try:
|
|
692
|
+
args = json.loads(fn.get("arguments", "{}"))
|
|
693
|
+
except (json.JSONDecodeError, TypeError):
|
|
694
|
+
continue
|
|
695
|
+
|
|
696
|
+
# Gather the text to scan
|
|
697
|
+
text_to_scan = ""
|
|
698
|
+
file_path = ""
|
|
699
|
+
|
|
700
|
+
if name == "write_file":
|
|
701
|
+
file_path = args.get("file_path", "")
|
|
702
|
+
text_to_scan = args.get("content", "")
|
|
703
|
+
elif name == "edit_file":
|
|
704
|
+
file_path = args.get("file_path", "")
|
|
705
|
+
text_to_scan = args.get("new_string", "")
|
|
706
|
+
elif name == "read_file":
|
|
707
|
+
# We don't detect concepts from reads
|
|
708
|
+
continue
|
|
709
|
+
else:
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
if not text_to_scan:
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
detected = detect_concepts_from_file(file_path, text_to_scan)
|
|
716
|
+
for c in detected:
|
|
717
|
+
if c.name not in seen:
|
|
718
|
+
seen.add(c.name)
|
|
719
|
+
concepts.append(c)
|
|
720
|
+
|
|
721
|
+
return concepts
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# ---------------------------------------------------------------------------
|
|
725
|
+
# LLM-enhanced detection (for patterns not in the registry)
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
|
|
728
|
+
LLM_DETECT_PROMPT = """\
|
|
729
|
+
You are a code-analysis assistant. Given the following code snippet,
|
|
730
|
+
list the key programming concepts, patterns, or techniques used.
|
|
731
|
+
Return ONLY a JSON array of objects with keys: "name", "category",
|
|
732
|
+
"description", "decision", "diagram".
|
|
733
|
+
If there are no notable concepts, return an empty array [].
|
|
734
|
+
|
|
735
|
+
The "category" value MUST be exactly one of these six strings — no
|
|
736
|
+
other values, no capitalization changes, no invented labels:
|
|
737
|
+
- "algorithm" — loops, sorting, searching, recursion, big-O reasoning
|
|
738
|
+
- "structure" — classes, interfaces, inheritance, encapsulation, OOP
|
|
739
|
+
- "api" — calls into a defined interface: fetch, DOM, SDKs, hooks
|
|
740
|
+
- "data_model" — how data is shaped or moved: schemas, type aliases,
|
|
741
|
+
JSON payloads, ORMs, validation
|
|
742
|
+
- "decisions" — architecture and integration choices made in THIS
|
|
743
|
+
codebase: which service, library, or tool was picked for a job
|
|
744
|
+
(Supabase for auth or the database, Firebase, Stripe, Redis, ...)
|
|
745
|
+
and how features/modules are wired together (who owns what, which
|
|
746
|
+
module reaches which)
|
|
747
|
+
- "abstract" — cross-cutting ideas: error handling, modules, async,
|
|
748
|
+
design patterns
|
|
749
|
+
|
|
750
|
+
"decisions" concepts are about the CODEBASE, not a technique. Emit one
|
|
751
|
+
ONLY when the snippet itself shows the evidence — an import, an SDK
|
|
752
|
+
client being constructed, a config/env read, or a call from one module
|
|
753
|
+
into another. Never speculate about files you cannot see. Name it
|
|
754
|
+
concretely ("Supabase session auth" beats "Authentication"; "Teacher
|
|
755
|
+
agent reads detect_concepts state" beats "Shared state"), and ALWAYS
|
|
756
|
+
fill "decision" for a decisions concept — the choice and the reason for
|
|
757
|
+
it IS the concept. Two or three decisions per snippet is plenty; skip
|
|
758
|
+
them entirely when the code shows no such choice.
|
|
759
|
+
|
|
760
|
+
The "decision" value records the CHOICE THE AUTHOR MADE — one sentence,
|
|
761
|
+
specific to this code, never generic:
|
|
762
|
+
- why THIS approach was chosen over the obvious alternative
|
|
763
|
+
- a trade-off accepted (e.g. "re-renders the whole list on toggle;
|
|
764
|
+
acceptable because lists stay under ~50 items")
|
|
765
|
+
- a constraint honored (e.g. "keeps polling on the main thread because
|
|
766
|
+
the target API has no webhook support")
|
|
767
|
+
Write it as a plain sentence. If a concept is textbook-only with no
|
|
768
|
+
author choice behind it, use "" — an empty decision is always better
|
|
769
|
+
than an invented one.
|
|
770
|
+
|
|
771
|
+
The "diagram" value must be a small, valid Mermaid diagram WHOSE TYPE
|
|
772
|
+
matches the concept's category — this routing is required:
|
|
773
|
+
- "algorithm" -> flowchart (process steps and decision points)
|
|
774
|
+
- "structure" -> classDiagram (classes, methods, inheritance)
|
|
775
|
+
- "api" -> sequenceDiagram (participants exchanging messages)
|
|
776
|
+
- "data_model" -> erDiagram (entities with attributes and relations)
|
|
777
|
+
- "decisions" -> flowchart (how the pieces connect: feature -> tool
|
|
778
|
+
or service -> data store), few nodes, one arrow per real connection
|
|
779
|
+
- "abstract" -> flowchart (a small concept map: the idea at the center,
|
|
780
|
+
what it touches or enables around it)
|
|
781
|
+
Aim for 3-6 nodes. Use simple ASCII labels, wrap each
|
|
782
|
+
label in square brackets (e.g. A[Label]), and never put parentheses or special
|
|
783
|
+
characters inside labels.
|
|
784
|
+
|
|
785
|
+
erDiagram extra rule: relation labels with spaces MUST be quoted, e.g.
|
|
786
|
+
``OperatorMap ||--o{{ Function : "maps to"`` — an unquoted
|
|
787
|
+
multi-word label is a parse error.
|
|
788
|
+
|
|
789
|
+
Name concepts SPECIFICALLY when the code warrants it: "Token refresh
|
|
790
|
+
via single-flight queue" teaches more than "Async Programming". Keep
|
|
791
|
+
generic names ("Recursion", "Event Listeners") for genuinely generic
|
|
792
|
+
code.
|
|
793
|
+
|
|
794
|
+
Code file: {file_path}
|
|
795
|
+
```{lang}
|
|
796
|
+
{code}
|
|
797
|
+
```
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
CATEGORY_FIX_PROMPT = """\
|
|
801
|
+
Your previous response described programming concepts but some
|
|
802
|
+
"category" values were missing or not from the allowed set:
|
|
803
|
+
algorithm, structure, api, data_model, decisions, abstract.
|
|
804
|
+
|
|
805
|
+
Re-send ONLY a JSON object mapping each concept name below to an object
|
|
806
|
+
with its corrected category, e.g. {{"Concept Name": {{"category":
|
|
807
|
+
"api"}}}}. Choose from the six allowed values only.
|
|
808
|
+
|
|
809
|
+
Concepts needing a category:
|
|
810
|
+
{concepts}
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
DIAGRAM_BACKFILL_PROMPT = """\For each concept below, produce ONE small, valid Mermaid diagram whose
|
|
814
|
+
TYPE matches the concept's category — flowchart for algorithm and
|
|
815
|
+
decisions (the latter showing how the wired pieces connect),
|
|
816
|
+
classDiagram for structure, sequenceDiagram for api, erDiagram for
|
|
817
|
+
data_model, and a simple concept-map flowchart for abstract. Aim for
|
|
818
|
+
3-6 nodes. Use simple ASCII labels, wrap each
|
|
819
|
+
label in square brackets (e.g. A[Label]), and never put parentheses or special
|
|
820
|
+
characters inside labels. erDiagram relation labels with spaces MUST
|
|
821
|
+
be quoted, e.g. ``OperatorMap ||--o{{ Function : "maps to"``.
|
|
822
|
+
|
|
823
|
+
Concepts (name | category | description):
|
|
824
|
+
{concepts}
|
|
825
|
+
|
|
826
|
+
Return ONLY a JSON object mapping each concept name to its Mermaid
|
|
827
|
+
diagram string (use "" only if no diagram can make sense).
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _norm_concept_key(name: str) -> str:
|
|
832
|
+
"""Normalize a concept name for tolerant matching.
|
|
833
|
+
|
|
834
|
+
Models echoing the prompt's '- Name (Category): ...' format tend to
|
|
835
|
+
return JSON keys like ``"DOM Manipulation (DOM / Browser API)"``
|
|
836
|
+
instead of the bare concept name — compare on the part before the
|
|
837
|
+
first parenthesis, lowercased.
|
|
838
|
+
"""
|
|
839
|
+
base = name.split("(", 1)[0]
|
|
840
|
+
return " ".join(base.lower().split())
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _backfill_diagrams(concepts: list[DetectedConcept]) -> None:
|
|
844
|
+
"""Ask the LLM for diagrams for concepts detected without one.
|
|
845
|
+
|
|
846
|
+
Models sometimes return the concept list but skip the "diagram"
|
|
847
|
+
field (nondeterministically). One focused follow-up call recovers
|
|
848
|
+
most of those cases. Best-effort: any failure leaves the diagrams
|
|
849
|
+
empty and the text explanation remains the fallback.
|
|
850
|
+
|
|
851
|
+
Mutates *concepts* in place.
|
|
852
|
+
"""
|
|
853
|
+
api_key = resolve_api_key()
|
|
854
|
+
if not api_key:
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
if not concepts:
|
|
858
|
+
return
|
|
859
|
+
|
|
860
|
+
concept_lines = [
|
|
861
|
+
f"- {c.name} | {c.category} | {c.description[:120]}"
|
|
862
|
+
for c in concepts
|
|
863
|
+
]
|
|
864
|
+
prompt = DIAGRAM_BACKFILL_PROMPT.format(concepts="\n".join(concept_lines))
|
|
865
|
+
|
|
866
|
+
try:
|
|
867
|
+
client = get_client()
|
|
868
|
+
completion = client.chat.completions.create(
|
|
869
|
+
model=get_model("detection"),
|
|
870
|
+
messages=[{"role": "user", "content": prompt}],
|
|
871
|
+
# gpt-oss spends most of its budget on reasoning tokens before
|
|
872
|
+
# writing content — 1024 truncated the JSON mid-output
|
|
873
|
+
# (finish_reason: length). 2048 matches the detection call,
|
|
874
|
+
# which is proven to leave room for the full answer.
|
|
875
|
+
max_completion_tokens=2048,
|
|
876
|
+
)
|
|
877
|
+
raw = (completion.choices[0].message.content or "").strip()
|
|
878
|
+
if raw.startswith("```"):
|
|
879
|
+
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
|
880
|
+
raw = re.sub(r"\s*```$", "", raw)
|
|
881
|
+
diagrams = json.loads(raw)
|
|
882
|
+
if not isinstance(diagrams, dict):
|
|
883
|
+
return
|
|
884
|
+
# Tolerant lookup: models may return keys like
|
|
885
|
+
# "Name (Category)" instead of the bare concept name.
|
|
886
|
+
by_key = {_norm_concept_key(k): v for k, v in diagrams.items()}
|
|
887
|
+
for c in concepts:
|
|
888
|
+
value = by_key.get(_norm_concept_key(c.name), "")
|
|
889
|
+
if isinstance(value, str) and value.strip():
|
|
890
|
+
candidate = value.strip()
|
|
891
|
+
if is_valid_mermaid(candidate):
|
|
892
|
+
c.diagram = candidate
|
|
893
|
+
# An invalid replacement is worse than none: the
|
|
894
|
+
# dashboard would render a syntax error instead of
|
|
895
|
+
# falling back to prose. Leave the (invalid) diagram
|
|
896
|
+
# field as-is; the save path strips it.
|
|
897
|
+
except Exception: # noqa: BLE001 - best-effort backfill only
|
|
898
|
+
return
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _retry_categories(concepts: list[DetectedConcept]) -> None:
|
|
902
|
+
"""Ask the LLM to re-tag concepts whose category was missing/invalid.
|
|
903
|
+
|
|
904
|
+
ONE corrective call is made, listing only the offending concepts.
|
|
905
|
+
Concepts whose category is still missing or outside the taxonomy are
|
|
906
|
+
rejected by the caller — a detection is never default-filled.
|
|
907
|
+
Mutates *concepts* in place: fixed entries get a canonical category;
|
|
908
|
+
unfixed entries keep ``category == ""`` (invalid) so the caller can
|
|
909
|
+
drop them.
|
|
910
|
+
"""
|
|
911
|
+
api_key = resolve_api_key()
|
|
912
|
+
if not api_key:
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
concept_lines = [
|
|
916
|
+
f"- {c.name}: {c.description[:120]}"
|
|
917
|
+
for c in concepts
|
|
918
|
+
]
|
|
919
|
+
prompt = CATEGORY_FIX_PROMPT.format(concepts="\n".join(concept_lines))
|
|
920
|
+
try:
|
|
921
|
+
client = get_client()
|
|
922
|
+
completion = client.chat.completions.create(
|
|
923
|
+
model=get_model("detection"),
|
|
924
|
+
messages=[{"role": "user", "content": prompt}],
|
|
925
|
+
max_completion_tokens=1024,
|
|
926
|
+
)
|
|
927
|
+
raw = (completion.choices[0].message.content or "").strip()
|
|
928
|
+
if raw.startswith("```"):
|
|
929
|
+
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
|
930
|
+
raw = re.sub(r"\s*```$", "", raw)
|
|
931
|
+
fixes = json.loads(raw)
|
|
932
|
+
if not isinstance(fixes, dict):
|
|
933
|
+
return
|
|
934
|
+
# Tolerant lookup: keys may be decorated ("Name (hint)").
|
|
935
|
+
by_key = {_norm_concept_key(k): v for k, v in fixes.items()}
|
|
936
|
+
for c in concepts:
|
|
937
|
+
fix = by_key.get(_norm_concept_key(c.name))
|
|
938
|
+
if isinstance(fix, dict):
|
|
939
|
+
fixed = normalize_category(fix.get("category"))
|
|
940
|
+
elif isinstance(fix, str):
|
|
941
|
+
# Tolerate a bare {"Name": "api"} mapping.
|
|
942
|
+
fixed = normalize_category(fix)
|
|
943
|
+
else:
|
|
944
|
+
fixed = None
|
|
945
|
+
if fixed is not None:
|
|
946
|
+
c.category = fixed
|
|
947
|
+
except Exception: # noqa: BLE001 - best-effort retry only
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def detect_concepts_with_llm(
|
|
952
|
+
file_path: str,
|
|
953
|
+
content: str,
|
|
954
|
+
known_names: set[str],
|
|
955
|
+
session: str = "default",
|
|
956
|
+
code_hash: str = "",
|
|
957
|
+
) -> list[DetectedConcept]:
|
|
958
|
+
"""Ask the LLM to identify concepts not already in our registry.
|
|
959
|
+
|
|
960
|
+
Falls back gracefully if the API key is missing or the call fails.
|
|
961
|
+
When *code_hash* is provided, concepts detected without a usable
|
|
962
|
+
diagram first try the store's identity cache (same concept slug +
|
|
963
|
+
same code hash) before spending the backfill LLM call — the cache
|
|
964
|
+
is only consulted for non-empty hashes so tests and hash-less
|
|
965
|
+
callers never touch the database.
|
|
966
|
+
"""
|
|
967
|
+
api_key = resolve_api_key()
|
|
968
|
+
if not api_key:
|
|
969
|
+
return []
|
|
970
|
+
|
|
971
|
+
# Determine language hint from extension
|
|
972
|
+
ext = file_path.rsplit(".", 1)[-1] if "." in file_path else ""
|
|
973
|
+
lang_map = {
|
|
974
|
+
"py": "python",
|
|
975
|
+
"js": "javascript",
|
|
976
|
+
"ts": "typescript",
|
|
977
|
+
"tsx": "tsx",
|
|
978
|
+
"jsx": "jsx",
|
|
979
|
+
"rs": "rust",
|
|
980
|
+
"go": "go",
|
|
981
|
+
}
|
|
982
|
+
lang = lang_map.get(ext, "")
|
|
983
|
+
|
|
984
|
+
# Truncate to avoid huge prompts
|
|
985
|
+
if len(content) > 4000:
|
|
986
|
+
content = content[:4000] + "\n... (truncated)"
|
|
987
|
+
|
|
988
|
+
prompt = LLM_DETECT_PROMPT.format(
|
|
989
|
+
file_path=file_path, lang=lang, code=content
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
try:
|
|
993
|
+
client = get_client()
|
|
994
|
+
completion = client.chat.completions.create(
|
|
995
|
+
model=get_model("detection"),
|
|
996
|
+
messages=[{"role": "user", "content": prompt}],
|
|
997
|
+
# Diagrams per concept make the output longer than plain
|
|
998
|
+
# detection, so allow more completion tokens here.
|
|
999
|
+
max_completion_tokens=2048,
|
|
1000
|
+
)
|
|
1001
|
+
raw = completion.choices[0].message.content or "[]"
|
|
1002
|
+
# Extract JSON array from the response (handle markdown fences)
|
|
1003
|
+
raw = raw.strip()
|
|
1004
|
+
if raw.startswith("```"):
|
|
1005
|
+
raw = re.sub(r"^```(?:json)?\s*", "", raw)
|
|
1006
|
+
raw = re.sub(r"\s*```$", "", raw)
|
|
1007
|
+
concepts_raw = json.loads(raw)
|
|
1008
|
+
except Exception: # noqa: BLE001
|
|
1009
|
+
return []
|
|
1010
|
+
|
|
1011
|
+
concepts: list[DetectedConcept] = []
|
|
1012
|
+
needs_category: list[DetectedConcept] = []
|
|
1013
|
+
for item in concepts_raw:
|
|
1014
|
+
if not isinstance(item, dict):
|
|
1015
|
+
continue
|
|
1016
|
+
name = str(item.get("name", "")).strip()
|
|
1017
|
+
if not name or name in known_names:
|
|
1018
|
+
continue
|
|
1019
|
+
description = str(item.get("description", ""))
|
|
1020
|
+
decision = str(item.get("decision", "") or "").strip()
|
|
1021
|
+
diagram = str(item.get("diagram", "") or "")
|
|
1022
|
+
raw_category = item.get("category")
|
|
1023
|
+
category = normalize_category(raw_category)
|
|
1024
|
+
raw_label = (
|
|
1025
|
+
str(raw_category).strip() if isinstance(raw_category, str) else ""
|
|
1026
|
+
)
|
|
1027
|
+
if category is None:
|
|
1028
|
+
# Missing or unrecognizable — queue for the corrective retry;
|
|
1029
|
+
# never default-fill.
|
|
1030
|
+
concept = DetectedConcept(
|
|
1031
|
+
name=name,
|
|
1032
|
+
category="",
|
|
1033
|
+
description=description,
|
|
1034
|
+
subcategory=raw_label,
|
|
1035
|
+
source_file=file_path,
|
|
1036
|
+
diagram=diagram,
|
|
1037
|
+
decision=decision,
|
|
1038
|
+
)
|
|
1039
|
+
concepts.append(concept)
|
|
1040
|
+
needs_category.append(concept)
|
|
1041
|
+
continue
|
|
1042
|
+
concepts.append(
|
|
1043
|
+
DetectedConcept(
|
|
1044
|
+
name=name,
|
|
1045
|
+
category=category,
|
|
1046
|
+
description=description,
|
|
1047
|
+
# Keep the model's own label as the human-readable
|
|
1048
|
+
# subcategory when it adds detail beyond the slug.
|
|
1049
|
+
subcategory=(
|
|
1050
|
+
raw_label if raw_label and raw_label.lower() != category else ""
|
|
1051
|
+
),
|
|
1052
|
+
source_file=file_path,
|
|
1053
|
+
diagram=diagram,
|
|
1054
|
+
decision=decision,
|
|
1055
|
+
)
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# Guard: one corrective retry for concepts whose category was
|
|
1059
|
+
# missing or outside the taxonomy, then reject whatever is still
|
|
1060
|
+
# invalid. Responses are never accepted with a default category.
|
|
1061
|
+
if needs_category:
|
|
1062
|
+
_retry_categories(needs_category)
|
|
1063
|
+
valid: list[DetectedConcept] = [
|
|
1064
|
+
c for c in concepts if is_valid_category(c.category)
|
|
1065
|
+
]
|
|
1066
|
+
|
|
1067
|
+
# Identity cache before the expensive thing: a concept whose code
|
|
1068
|
+
# is unchanged reuses its stored diagram instead of a backfill call.
|
|
1069
|
+
if code_hash:
|
|
1070
|
+
for c in valid:
|
|
1071
|
+
if not c.diagram or not is_valid_mermaid(c.diagram):
|
|
1072
|
+
cached = get_cached_diagram(session, concept_slug(c.name), code_hash)
|
|
1073
|
+
if cached:
|
|
1074
|
+
c.diagram = cached
|
|
1075
|
+
|
|
1076
|
+
# Models sometimes skip the diagram field or emit malformed Mermaid —
|
|
1077
|
+
# recover both with one focused follow-up call before giving up on a
|
|
1078
|
+
# visual. Every category may carry a diagram, including abstract.
|
|
1079
|
+
missing = [
|
|
1080
|
+
c for c in valid
|
|
1081
|
+
if not c.diagram or not is_valid_mermaid(c.diagram)
|
|
1082
|
+
]
|
|
1083
|
+
if missing:
|
|
1084
|
+
_backfill_diagrams(missing)
|
|
1085
|
+
|
|
1086
|
+
return valid
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
# ---------------------------------------------------------------------------
|
|
1090
|
+
# Graph node — run detection once per turn for both downstream agents
|
|
1091
|
+
# ---------------------------------------------------------------------------
|
|
1092
|
+
|
|
1093
|
+
def detect_concepts(state: dict[str, Any]) -> dict[str, Any]:
|
|
1094
|
+
"""LangGraph node: detect concepts from the coding agent's tool calls.
|
|
1095
|
+
|
|
1096
|
+
Combines the registry scan with mode-gated LLM detection and writes
|
|
1097
|
+
the merged, deduplicated result to ``state["concepts_detected"]``.
|
|
1098
|
+
The assessment and teacher agents read that shared result instead of
|
|
1099
|
+
re-scanning the tool calls themselves.
|
|
1100
|
+
|
|
1101
|
+
Expects ``state["tool_calls_log"]`` from the coding agent.
|
|
1102
|
+
Expects ``state["concepts"]`` for already-known concepts.
|
|
1103
|
+
Expects ``state["current_mode_config"]`` for mode settings.
|
|
1104
|
+
|
|
1105
|
+
Files whose exact content already produced a stored concept (same
|
|
1106
|
+
path, same content_hash) skip the LLM generation call entirely —
|
|
1107
|
+
the identity cache gates the expensive call itself, not just its
|
|
1108
|
+
backfill or the final write.
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
- ``concepts_detected``: list of dicts with ``name``, ``category``,
|
|
1112
|
+
``description``, ``diagram`` (plus ``source_file``/``line_range``
|
|
1113
|
+
where known)
|
|
1114
|
+
"""
|
|
1115
|
+
tool_calls_log: list[dict[str, Any]] = state.get("tool_calls_log", [])
|
|
1116
|
+
concepts: list[dict[str, Any]] = state.get("concepts", [])
|
|
1117
|
+
mode_config: dict[str, Any] | None = state.get("current_mode_config")
|
|
1118
|
+
session: str = state.get("session", "default")
|
|
1119
|
+
|
|
1120
|
+
# 1. Registry-based detection from write/edit tool calls.
|
|
1121
|
+
detected: list[DetectedConcept] = []
|
|
1122
|
+
seen: set[str] = set()
|
|
1123
|
+
for c in detect_concepts_from_tool_calls(tool_calls_log):
|
|
1124
|
+
if c.name not in seen:
|
|
1125
|
+
seen.add(c.name)
|
|
1126
|
+
detected.append(c)
|
|
1127
|
+
|
|
1128
|
+
# Stamp each tool call's file with the hash of its content — the
|
|
1129
|
+
# store keys its identity cache on (slug, content_hash), so "same
|
|
1130
|
+
# concept, unchanged code" is recognizable both here (cache check)
|
|
1131
|
+
# and downstream (teacher → save_teaching).
|
|
1132
|
+
code_hashes: dict[str, str] = {}
|
|
1133
|
+
for tc in tool_calls_log:
|
|
1134
|
+
fn = tc.get("function", {})
|
|
1135
|
+
name = fn.get("name", "")
|
|
1136
|
+
try:
|
|
1137
|
+
args = json.loads(fn.get("arguments", "{}"))
|
|
1138
|
+
except (json.JSONDecodeError, TypeError):
|
|
1139
|
+
continue
|
|
1140
|
+
if name == "write_file":
|
|
1141
|
+
file_path = args.get("file_path", "")
|
|
1142
|
+
file_content = args.get("content", "")
|
|
1143
|
+
elif name == "edit_file":
|
|
1144
|
+
file_path = args.get("file_path", "")
|
|
1145
|
+
file_content = args.get("new_string", "")
|
|
1146
|
+
else:
|
|
1147
|
+
continue
|
|
1148
|
+
if file_path and file_content:
|
|
1149
|
+
code_hashes[file_path] = content_hash(file_content)
|
|
1150
|
+
|
|
1151
|
+
# 2. LLM detection on file contents from write/edit tool calls
|
|
1152
|
+
# (pattern-based detection misses many concepts like HTML structure,
|
|
1153
|
+
# CSS patterns, DOM APIs, etc.) Mode-gated: skipped when
|
|
1154
|
+
# llm_detection is off (autonomous mode) since it costs an LLM call
|
|
1155
|
+
# per written file.
|
|
1156
|
+
llm_detection: bool = True if mode_config is None else bool(
|
|
1157
|
+
mode_config.get("llm_detection", True)
|
|
1158
|
+
)
|
|
1159
|
+
# Names already found by the registry scan this turn are merged in
|
|
1160
|
+
# here too: the LLM would otherwise detect them again, and a
|
|
1161
|
+
# diagram-less duplicate could even trigger a backfill call — for a
|
|
1162
|
+
# concept the merge below discards anyway.
|
|
1163
|
+
known_names: set[str] = {c["name"] for c in concepts} | seen
|
|
1164
|
+
|
|
1165
|
+
for tc in (tool_calls_log if llm_detection else []):
|
|
1166
|
+
fn = tc.get("function", {})
|
|
1167
|
+
name = fn.get("name", "")
|
|
1168
|
+
try:
|
|
1169
|
+
args = json.loads(fn.get("arguments", "{}"))
|
|
1170
|
+
except (json.JSONDecodeError, TypeError):
|
|
1171
|
+
continue
|
|
1172
|
+
if name == "write_file":
|
|
1173
|
+
file_path = args.get("file_path", "")
|
|
1174
|
+
content = args.get("content", "")
|
|
1175
|
+
elif name == "edit_file":
|
|
1176
|
+
file_path = args.get("file_path", "")
|
|
1177
|
+
content = args.get("new_string", "")
|
|
1178
|
+
else:
|
|
1179
|
+
continue
|
|
1180
|
+
if not content:
|
|
1181
|
+
continue
|
|
1182
|
+
# Generation-time identity cache: identical file content was
|
|
1183
|
+
# already LLM-scanned — skip the expensive generation call
|
|
1184
|
+
# itself, not merely its backfill or the final write. This is
|
|
1185
|
+
# the check that keeps a resurfacing concept with unchanged
|
|
1186
|
+
# code from paying for a detection whose output save_teaching
|
|
1187
|
+
# would only discard.
|
|
1188
|
+
file_hash = content_hash(content)
|
|
1189
|
+
if file_hash and file_scan_cached(session, file_path, file_hash):
|
|
1190
|
+
continue
|
|
1191
|
+
llm_detected = detect_concepts_with_llm(
|
|
1192
|
+
file_path, content, known_names,
|
|
1193
|
+
session=session, code_hash=file_hash,
|
|
1194
|
+
)
|
|
1195
|
+
for c in llm_detected:
|
|
1196
|
+
if c.name not in known_names and c.name not in seen:
|
|
1197
|
+
known_names.add(c.name)
|
|
1198
|
+
seen.add(c.name)
|
|
1199
|
+
detected.append(c)
|
|
1200
|
+
|
|
1201
|
+
return {
|
|
1202
|
+
"concepts_detected": [
|
|
1203
|
+
{
|
|
1204
|
+
"name": c.name,
|
|
1205
|
+
"slug": concept_slug(c.name),
|
|
1206
|
+
"category": c.category,
|
|
1207
|
+
"subcategory": c.subcategory,
|
|
1208
|
+
"description": c.description,
|
|
1209
|
+
"decision": c.decision,
|
|
1210
|
+
"diagram": c.diagram,
|
|
1211
|
+
"source_file": c.source_file,
|
|
1212
|
+
"content_hash": code_hashes.get(c.source_file, ""),
|
|
1213
|
+
"line_range": list(c.line_range),
|
|
1214
|
+
}
|
|
1215
|
+
for c in detected
|
|
1216
|
+
],
|
|
1217
|
+
}
|