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
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CodeLith backend package."""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""CodeLith agents — autonomous workers that handle specific tasks.
|
|
2
|
+
|
|
3
|
+
- coding_agent: reads/writes files, runs commands, builds features.
|
|
4
|
+
- debug_agent: runs tests, diagnoses failures, fixes code.
|
|
5
|
+
- teacher_agent: analyzes code for concepts and teaches the user.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Assessment Agent — generates Socratic questions based on detected concepts.
|
|
2
|
+
|
|
3
|
+
When the coding agent uses a programming concept (e.g. useState, async/await),
|
|
4
|
+
this agent generates a question to test the user's understanding. Questions
|
|
5
|
+
are stored for the dashboard to display — not printed in the terminal.
|
|
6
|
+
|
|
7
|
+
Concept detection itself happens upstream in the shared ``detect_concepts``
|
|
8
|
+
node (see :mod:`backend.agents.concept_detector`); this node reads the shared
|
|
9
|
+
``concepts_detected`` result instead of re-scanning tool calls.
|
|
10
|
+
|
|
11
|
+
The teacher agent then saves the teaching content to the dashboard as well.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
|
20
|
+
from backend.llm.client import DEFAULT_MODEL, resolve_api_key, get_client # noqa: F401 (DEFAULT_MODEL re-exported)
|
|
21
|
+
from backend.llm.config import get_model
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Socratic question templates
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
QUESTION_TEMPLATES: dict[str, list[str]] = {
|
|
28
|
+
"useState": [
|
|
29
|
+
"Why are we using useState here instead of a regular variable?",
|
|
30
|
+
"What happens when we call the setter function from useState?",
|
|
31
|
+
"When would you NOT want to use useState?",
|
|
32
|
+
],
|
|
33
|
+
"useEffect": [
|
|
34
|
+
"What does the dependency array control in this useEffect?",
|
|
35
|
+
"Why do we need a cleanup function here?",
|
|
36
|
+
"What happens if we omit the dependency array entirely?",
|
|
37
|
+
],
|
|
38
|
+
"useMemo": [
|
|
39
|
+
"What problem does useMemo solve in this component?",
|
|
40
|
+
"How does useMemo differ from just calling the function directly?",
|
|
41
|
+
"When would useMemo actually hurt performance instead of helping?",
|
|
42
|
+
],
|
|
43
|
+
"useCallback": [
|
|
44
|
+
"Why are we wrapping this function in useCallback?",
|
|
45
|
+
"What would happen if we passed the function directly without useCallback?",
|
|
46
|
+
"When is useCallback unnecessary?",
|
|
47
|
+
],
|
|
48
|
+
"useRef": [
|
|
49
|
+
"Why use useRef instead of useState for this value?",
|
|
50
|
+
"What's the key difference between useRef and useState in terms of re-renders?",
|
|
51
|
+
"When would you reach for useRef over useState?",
|
|
52
|
+
],
|
|
53
|
+
"useContext": [
|
|
54
|
+
"What problem does useContext solve compared to prop drilling?",
|
|
55
|
+
"When might useContext cause performance issues?",
|
|
56
|
+
"How does the context value get updated?",
|
|
57
|
+
],
|
|
58
|
+
"async/await": [
|
|
59
|
+
"Why do we use async/await here instead of .then() chains?",
|
|
60
|
+
"What happens if we forget to await this promise?",
|
|
61
|
+
"How does error handling work with async/await?",
|
|
62
|
+
],
|
|
63
|
+
"Promises": [
|
|
64
|
+
"What are the three states of a Promise?",
|
|
65
|
+
"How does Promise.all differ from Promise.race?",
|
|
66
|
+
"What happens when a Promise rejects and we don't catch it?",
|
|
67
|
+
],
|
|
68
|
+
"Default Export": [
|
|
69
|
+
"What's the difference between a default export and a named export?",
|
|
70
|
+
"Why might you choose one over the other?",
|
|
71
|
+
"Can a file have multiple default exports?",
|
|
72
|
+
],
|
|
73
|
+
"TypeScript Interface": [
|
|
74
|
+
"What's the difference between an interface and a type alias?",
|
|
75
|
+
"Why define an interface if TypeScript can infer types?",
|
|
76
|
+
"When would you extend an interface instead of using intersection types?",
|
|
77
|
+
],
|
|
78
|
+
"Python OOP": [
|
|
79
|
+
"Why do we use __init__ instead of a constructor method?",
|
|
80
|
+
"What does 'self' represent in a Python class?",
|
|
81
|
+
"When would you use a class method vs an instance method?",
|
|
82
|
+
],
|
|
83
|
+
"Error Handling": [
|
|
84
|
+
"Why wrap this code in try/except instead of letting it fail?",
|
|
85
|
+
"What's the difference between catching Exception vs a specific error?",
|
|
86
|
+
"When is it better to let an exception propagate instead of catching it?",
|
|
87
|
+
],
|
|
88
|
+
"Decorators": [
|
|
89
|
+
"What does this decorator modify about the function's behavior?",
|
|
90
|
+
"How does @decorator syntax differ from calling decorator(func) directly?",
|
|
91
|
+
"When might you write a custom decorator vs using a built-in one?",
|
|
92
|
+
],
|
|
93
|
+
"Lambda Functions": [
|
|
94
|
+
"Why use a lambda here instead of a regular function?",
|
|
95
|
+
"What are the limitations of lambda functions in Python?",
|
|
96
|
+
"When would you prefer a named function over a lambda?",
|
|
97
|
+
],
|
|
98
|
+
"Imports / Modules": [
|
|
99
|
+
"What's the difference between 'import module' and 'from module import X'?",
|
|
100
|
+
"Why might you use __init__.py in a package?",
|
|
101
|
+
"When would you use a relative vs absolute import?",
|
|
102
|
+
],
|
|
103
|
+
"Classes / OOP": [
|
|
104
|
+
"Why use a class here instead of a module with functions?",
|
|
105
|
+
"What does inheritance give us that composition doesn't?",
|
|
106
|
+
"When is composition preferred over inheritance?",
|
|
107
|
+
],
|
|
108
|
+
"map()": [
|
|
109
|
+
"Why use map() instead of a for loop here?",
|
|
110
|
+
"What's the difference between map() and a list comprehension?",
|
|
111
|
+
"When might a for loop be clearer than map()?",
|
|
112
|
+
],
|
|
113
|
+
"filter()": [
|
|
114
|
+
"What does the predicate function need to return for filter()?",
|
|
115
|
+
"How does filter() differ from using a list comprehension with if?",
|
|
116
|
+
"When would you use filter() vs a for loop with conditionals?",
|
|
117
|
+
],
|
|
118
|
+
"Fetch API": [
|
|
119
|
+
"Why use fetch() instead of XMLHttpRequest?",
|
|
120
|
+
"What happens if we don't await the fetch response?",
|
|
121
|
+
"How does error handling differ between fetch and traditional AJAX?",
|
|
122
|
+
],
|
|
123
|
+
"DOM Querying": [
|
|
124
|
+
"Why querySelector instead of getElementById?",
|
|
125
|
+
"What's the difference between querySelector and querySelectorAll?",
|
|
126
|
+
"When might direct DOM manipulation be better than a framework?",
|
|
127
|
+
],
|
|
128
|
+
"Event Listeners": [
|
|
129
|
+
"Why register an event listener instead of using inline HTML handlers?",
|
|
130
|
+
"What's the difference between addEventListener and onclick?",
|
|
131
|
+
"When would you remove an event listener?",
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Fallback generic questions for concepts not in the template
|
|
136
|
+
GENERIC_QUESTIONS = [
|
|
137
|
+
"Why did we use {concept_name} here?",
|
|
138
|
+
"What problem does {concept_name} solve in this context?",
|
|
139
|
+
"When would you choose {concept_name} over alternatives?",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class Assessment:
|
|
145
|
+
"""A pending assessment question for the user."""
|
|
146
|
+
|
|
147
|
+
id: str
|
|
148
|
+
concept_name: str
|
|
149
|
+
concept_category: str
|
|
150
|
+
question: str
|
|
151
|
+
source_file: str
|
|
152
|
+
answered: bool = False
|
|
153
|
+
answer: str = ""
|
|
154
|
+
correct: bool = False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _generate_assessment_id(concept_name: str, source_file: str) -> str:
|
|
158
|
+
"""Generate a stable ID for an assessment based on concept + file."""
|
|
159
|
+
import hashlib
|
|
160
|
+
raw = f"{concept_name}:{source_file}"
|
|
161
|
+
return hashlib.md5(raw.encode()).hexdigest()[:12]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _get_question(concept_name: str) -> str:
|
|
165
|
+
"""Return a Socratic question for the given concept."""
|
|
166
|
+
import random
|
|
167
|
+
templates = QUESTION_TEMPLATES.get(concept_name, GENERIC_QUESTIONS)
|
|
168
|
+
template = random.choice(templates)
|
|
169
|
+
return template.format(concept_name=concept_name)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Assessment Agent node (for LangGraph)
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def assessment_agent_node(state: dict[str, Any]) -> dict[str, Any]:
|
|
178
|
+
"""LangGraph node: generate Socratic questions for newly detected concepts.
|
|
179
|
+
|
|
180
|
+
The frequency of questions depends on the session mode:
|
|
181
|
+
- ``learn``: question for every new concept (high frequency)
|
|
182
|
+
- ``pair-programming``: question for roughly 1 in 3 new concepts (low)
|
|
183
|
+
- ``autonomous``: no questions at all (the graph also skips this node
|
|
184
|
+
when the mode disables assessment)
|
|
185
|
+
|
|
186
|
+
Expects ``state["concepts_detected"]`` — written by the shared
|
|
187
|
+
detect_concepts node (registry scan + mode-gated LLM detection).
|
|
188
|
+
Expects ``state["concepts"]`` for already-known concepts.
|
|
189
|
+
Expects ``state["session"]`` for storage.
|
|
190
|
+
Expects ``state["current_mode_config"]`` for mode settings.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
- ``pending_assessments``: list of Assessment dicts for the dashboard
|
|
194
|
+
- ``messages``: appends an assessment prompt message
|
|
195
|
+
"""
|
|
196
|
+
import random as _random
|
|
197
|
+
|
|
198
|
+
from backend.database.concepts import get_all_assessments, save_assessment
|
|
199
|
+
|
|
200
|
+
detected: list[dict[str, Any]] = state.get("concepts_detected", [])
|
|
201
|
+
concepts: list[dict[str, Any]] = state.get("concepts", [])
|
|
202
|
+
session: str = state.get("session", "default")
|
|
203
|
+
messages: list[BaseMessage] = state.get("messages", [])
|
|
204
|
+
|
|
205
|
+
# Determine assessment frequency from mode config
|
|
206
|
+
mode_config: dict[str, Any] | None = state.get("current_mode_config")
|
|
207
|
+
if mode_config is not None:
|
|
208
|
+
freq = mode_config.get("assessment_frequency", "high")
|
|
209
|
+
else:
|
|
210
|
+
freq = "high" # default to generating questions
|
|
211
|
+
|
|
212
|
+
# In autonomous mode, skip question generation entirely
|
|
213
|
+
if freq == "none":
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
# Filter to only NEW concepts (not already known). Detection already
|
|
217
|
+
# ran upstream in the shared detect_concepts node (registry scan +
|
|
218
|
+
# mode-gated LLM detection), so no re-scanning of tool calls here.
|
|
219
|
+
new_concepts = [c for c in detected if c["name"] not in {cc["name"] for cc in concepts}]
|
|
220
|
+
|
|
221
|
+
if not new_concepts:
|
|
222
|
+
return {}
|
|
223
|
+
|
|
224
|
+
# In low-frequency mode (pair-programming), only ask about ~1 in 3 concepts
|
|
225
|
+
if freq == "low" and len(new_concepts) > 1:
|
|
226
|
+
new_concepts = [c for c in new_concepts if _random.random() < 0.34]
|
|
227
|
+
if not new_concepts:
|
|
228
|
+
return {}
|
|
229
|
+
|
|
230
|
+
# Avoid duplicates against every stored assessment (queued or not)
|
|
231
|
+
existing = get_all_assessments(session)
|
|
232
|
+
existing_ids = {a["id"] for a in existing}
|
|
233
|
+
|
|
234
|
+
pending: list[dict[str, Any]] = []
|
|
235
|
+
for concept in new_concepts:
|
|
236
|
+
concept_name = concept["name"]
|
|
237
|
+
concept_source = concept.get("source_file", "")
|
|
238
|
+
assessment_id = _generate_assessment_id(concept_name, concept_source)
|
|
239
|
+
if assessment_id in existing_ids:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
question = _get_question(concept_name)
|
|
243
|
+
assessment = {
|
|
244
|
+
"id": assessment_id,
|
|
245
|
+
"concept_name": concept_name,
|
|
246
|
+
"concept_category": concept["category"],
|
|
247
|
+
"question": question,
|
|
248
|
+
"source_file": concept_source,
|
|
249
|
+
"answered": False,
|
|
250
|
+
"answer": "",
|
|
251
|
+
"correct": False,
|
|
252
|
+
}
|
|
253
|
+
save_assessment(session, assessment)
|
|
254
|
+
pending.append(assessment)
|
|
255
|
+
|
|
256
|
+
if not pending:
|
|
257
|
+
return {}
|
|
258
|
+
|
|
259
|
+
# Build a message that points the user to the dashboard instead of
|
|
260
|
+
# dumping concepts/questions into the terminal. Questions are surfaced
|
|
261
|
+
# one at a time there; the rest stay queued in the backend.
|
|
262
|
+
assessment_msg = (
|
|
263
|
+
f"New coding concept(s) detected! They're available with "
|
|
264
|
+
f"explanations on the dashboard (Concepts section). "
|
|
265
|
+
f"I've also prepared {len(pending)} question(s) for you — check the "
|
|
266
|
+
f"**Assessment Questions** section on the dashboard to answer them; "
|
|
267
|
+
f"they're asked one at a time."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"messages": messages + [AIMessage(content=assessment_msg)],
|
|
272
|
+
"pending_assessments": pending,
|
|
273
|
+
}
|