99
1010if os .getenv ("GEVENT_SUPPORT" ) == "True" :
1111 from gevent import monkey
12+
1213 monkey .patch_all ()
1314
1415from flask import Flask , render_template , jsonify , request
@@ -70,7 +71,6 @@ def index():
7071 "decisions" : {"title" : "Decisions" },
7172 "corrections" : {"title" : "Corrections" },
7273 "insights" : {"title" : "Insights" },
73- "sessions" : {"title" : "Sessions" },
7474 "failed-approaches" : {"title" : "Failed Approaches" },
7575 "exceptions" : {"title" : "Exceptions" },
7676 "questions" : {"title" : "Questions" },
@@ -81,6 +81,7 @@ def index():
8181
8282def _detail_page (page_type : str ):
8383 """Create detail page handler for a given page type."""
84+
8485 def view ():
8586 project = request .args .get ("project" , "" )
8687 config = _DETAIL_PAGE_CONFIG .get (page_type , {})
@@ -90,6 +91,7 @@ def view():
9091 page_type = page_type ,
9192 title = config .get ("title" , page_type .title ()),
9293 )
94+
9395 view .__name__ = f"{ page_type } _view"
9496 return view
9597
@@ -114,26 +116,34 @@ def metrics():
114116 WITH total_decisions, curated, count(c) as total_corrections
115117 OPTIONAL MATCH (i:Insight {project: $project})
116118 WITH total_decisions, curated, total_corrections, count(i) as total_insights
117- OPTIONAL MATCH (s:Session {project: $project})
118- WITH total_decisions, curated, total_corrections, total_insights, count(s) as total_sessions
119119 OPTIONAL MATCH (f:FailedApproach {project: $project})
120- WITH total_decisions, curated, total_corrections, total_insights, total_sessions, count(f) as total_failed_approaches
120+ WITH total_decisions, curated, total_corrections, total_insights, count(f) as total_failed_approaches
121121 OPTIONAL MATCH (pf:ProjectFact {project: $project})
122+ WITH total_decisions, curated, total_corrections, total_insights, total_failed_approaches, count(pf) as total_project_facts
123+ OPTIONAL MATCH (e:Exception {project: $project})
124+ WITH total_decisions, curated, total_corrections, total_insights, total_failed_approaches, total_project_facts, count(e) as total_exceptions
125+ OPTIONAL MATCH (:Decision {project: $project})-[sup:SUPERSEDES]->(:Decision)
122126 RETURN total_decisions, curated, total_corrections, total_insights,
123- total_sessions, total_failed_approaches, count(pf ) as total_project_facts
127+ total_failed_approaches, total_project_facts, total_exceptions, count(sup ) as supersession_count
124128 """ ,
125129 project = project ,
126130 )
127131 record = result .single ()
128132
129133 total_decisions = record ["total_decisions" ]
130- total_sessions = record ["total_sessions" ]
131134 total_corrections = record ["total_corrections" ]
135+ total_project_facts = record ["total_project_facts" ]
136+ total_exceptions = record ["total_exceptions" ]
137+ supersession_count = record ["supersession_count" ]
132138
133139 reuse_rate = record ["curated" ] / total_decisions if total_decisions > 0 else 0
134- reexplanation_rate = (
135- total_corrections / total_sessions if total_sessions > 0 else 0
140+ evolution_rate = (
141+ supersession_count / total_decisions if total_decisions > 0 else 0
142+ )
143+ exception_rate = (
144+ total_exceptions / total_project_facts if total_project_facts > 0 else 0
136145 )
146+
137147 coefficient = 1.0 + (total_decisions * 0.02 ) + (reuse_rate * 1.0 )
138148 coefficient = min (coefficient , 4.0 )
139149
@@ -143,12 +153,12 @@ def metrics():
143153 "total_decisions" : total_decisions ,
144154 "total_corrections" : total_corrections ,
145155 "total_insights" : record ["total_insights" ],
146- "total_sessions" : total_sessions ,
147156 "total_failed_approaches" : record ["total_failed_approaches" ],
148- "total_project_facts" : record ["total_project_facts" ],
157+ "total_project_facts" : total_project_facts ,
158+ "total_exceptions" : total_exceptions ,
149159 "decision_reuse_rate" : reuse_rate ,
150- "graph_density " : 0.0 ,
151- "reexplanation_rate " : reexplanation_rate ,
160+ "decision_evolution_rate " : evolution_rate ,
161+ "rule_exception_rate " : exception_rate ,
152162 }
153163 )
154164
@@ -194,8 +204,13 @@ def graph():
194204 project = request .args .get ("project" , "" )
195205 limit = int (request .args .get ("limit" , 100 ))
196206 node_types = request .args .getlist ("types" ) or [
197- "Decision" , "Correction" , "Exception" , "Insight" ,
198- "Question" , "FailedApproach" , "ProjectFact"
207+ "Decision" ,
208+ "Correction" ,
209+ "Exception" ,
210+ "Insight" ,
211+ "Question" ,
212+ "FailedApproach" ,
213+ "ProjectFact" ,
199214 ]
200215 driver = getDriver ()
201216
@@ -271,13 +286,8 @@ def graph():
271286 if not record :
272287 return jsonify ({"nodes" : [], "edges" : []})
273288
274- nodes = record ["nodes" ] + [
275- sn for sn in record ["session_nodes" ] if sn ["id" ]
276- ]
277- edges = [
278- e for e in record ["edges" ]
279- if e ["source" ] and e ["target" ]
280- ]
289+ nodes = record ["nodes" ] + [sn for sn in record ["session_nodes" ] if sn ["id" ]]
290+ edges = [e for e in record ["edges" ] if e ["source" ] and e ["target" ]]
281291
282292 for node in nodes :
283293 if node .get ("timestamp" ) and hasattr (node ["timestamp" ], "isoformat" ):
@@ -304,11 +314,13 @@ def proactive_insights():
304314 project = project ,
305315 )
306316 for r in result :
307- insights .append ({
308- "type" : "highly_cited" ,
309- "message" : f"'{ r ['description' ][:40 ]} ...' cited by { r ['cite_count' ]} decisions" ,
310- "node_id" : r ["id" ],
311- })
317+ insights .append (
318+ {
319+ "type" : "highly_cited" ,
320+ "message" : f"'{ r ['description' ][:40 ]} ...' cited by { r ['cite_count' ]} decisions" ,
321+ "node_id" : r ["id" ],
322+ }
323+ )
312324
313325 result = session .run (
314326 """
@@ -321,10 +333,12 @@ def proactive_insights():
321333 project = project ,
322334 )
323335 for r in result :
324- insights .append ({
325- "type" : "exception_hotspot" ,
326- "message" : f"'{ r ['rule' ][:30 ]} ...' has { r ['exception_count' ]} exceptions" ,
327- })
336+ insights .append (
337+ {
338+ "type" : "exception_hotspot" ,
339+ "message" : f"'{ r ['rule' ][:30 ]} ...' has { r ['exception_count' ]} exceptions" ,
340+ }
341+ )
328342
329343 result = session .run (
330344 """
@@ -335,14 +349,100 @@ def proactive_insights():
335349 )
336350 record = result .single ()
337351 if record and record ["correction_count" ] >= 3 :
338- insights .append ({
339- "type" : "correction_pattern" ,
340- "message" : f"{ record ['correction_count' ]} corrections recorded - review for patterns" ,
341- })
352+ insights .append (
353+ {
354+ "type" : "correction_pattern" ,
355+ "message" : f"{ record ['correction_count' ]} corrections recorded - review for patterns" ,
356+ }
357+ )
342358
343359 return jsonify (insights )
344360
345361
362+ @app .route ("/api/patterns" )
363+ def patterns ():
364+ """Get detected patterns: exception clusters, supersession chains, correction hotspots."""
365+ project = request .args .get ("project" , "" )
366+ driver = getDriver ()
367+
368+ result = {
369+ "exception_clusters" : [],
370+ "supersession_chains" : [],
371+ "correction_hotspots" : [],
372+ }
373+
374+ with driver .session () as session :
375+ # Exception clusters: rules with multiple exceptions
376+ clusters = session .run (
377+ """
378+ MATCH (e:Exception {project: $project})
379+ WHERE e.rule_broken IS NOT NULL
380+ WITH e.rule_broken as rule, collect(e) as exceptions, count(*) as count
381+ WHERE count >= 2
382+ RETURN rule, count,
383+ [exc IN exceptions | {id: exc.id, justification: exc.justification, scope: exc.scope}] as exceptions
384+ ORDER BY count DESC
385+ LIMIT 10
386+ """ ,
387+ project = project ,
388+ )
389+ for r in clusters :
390+ result ["exception_clusters" ].append (
391+ {
392+ "rule" : r ["rule" ],
393+ "count" : r ["count" ],
394+ "exceptions" : r ["exceptions" ],
395+ }
396+ )
397+
398+ # Supersession chains: decisions that supersede other decisions
399+ chains = session .run (
400+ """
401+ MATCH path = (d:Decision {project: $project})-[:SUPERSEDES*2..]->(old:Decision)
402+ WITH d, length(path) as chain_length, [n IN nodes(path) | n.description] as chain
403+ RETURN d.id as id, d.description as description, chain_length, chain
404+ ORDER BY chain_length DESC
405+ LIMIT 10
406+ """ ,
407+ project = project ,
408+ )
409+ for r in chains :
410+ result ["supersession_chains" ].append (
411+ {
412+ "id" : r ["id" ],
413+ "description" : r ["description" ],
414+ "chain_length" : r ["chain_length" ],
415+ "chain" : r ["chain" ],
416+ }
417+ )
418+
419+ # Correction hotspots: topics with multiple corrections
420+ hotspots = session .run (
421+ """
422+ MATCH (c:Correction {project: $project})
423+ WHERE c.topics IS NOT NULL AND size(c.topics) > 0
424+ UNWIND c.topics as topic
425+ WITH topic, collect(c) as corrections, count(*) as count
426+ WHERE count >= 2
427+ RETURN topic, count,
428+ [corr IN corrections[0..5] | {id: corr.id, wrong: corr.wrong_belief, right: corr.right_belief}] as corrections
429+ ORDER BY count DESC
430+ LIMIT 10
431+ """ ,
432+ project = project ,
433+ )
434+ for r in hotspots :
435+ result ["correction_hotspots" ].append (
436+ {
437+ "topic" : r ["topic" ],
438+ "count" : r ["count" ],
439+ "corrections" : r ["corrections" ],
440+ }
441+ )
442+
443+ return jsonify (result )
444+
445+
346446@app .route ("/api/decisions" )
347447def decisions ():
348448 project = request .args .get ("project" , "" )
@@ -439,24 +539,6 @@ def failed_approaches():
439539 return jsonify ([serialize_node (dict (r ["f" ])) for r in result ])
440540
441541
442- @app .route ("/api/sessions" )
443- def sessions ():
444- project = request .args .get ("project" , "" )
445- limit = int (request .args .get ("limit" , 50 ))
446- driver = getDriver ()
447-
448- with driver .session () as session :
449- result = session .run (
450- """
451- MATCH (s:Session {project: $project})
452- RETURN s ORDER BY s.started_at DESC LIMIT $limit
453- """ ,
454- project = project ,
455- limit = limit ,
456- )
457- return jsonify ([serialize_node (dict (r ["s" ])) for r in result ])
458-
459-
460542@app .route ("/api/exceptions" )
461543def exceptions ():
462544 project = request .args .get ("project" , "" )
@@ -658,6 +740,7 @@ def import_conversations():
658740 return jsonify ({"error" : f"MCP connection failed: { e } " }), 500
659741 except Exception as e :
660742 import traceback
743+
661744 traceback .print_exc ()
662745 return jsonify ({"error" : str (e )}), 500
663746
0 commit comments