RayMelius Claude Sonnet 4.6 commited on
Commit
0fc5eec
Β·
1 Parent(s): e4043cb

AI Analyst: add Generate Now button for on-demand insights

Browse files

- Button in AI Analyst panel header calls POST /session/ai_insight
- Dashboard publishes {"action": "generate_insight"} to control topic
- ai_analyst listens and runs LLM call in a background thread
- Button shows "Generating…" / disabled while waiting, re-enables on result
- Fix: control topic key was "command", now correctly uses "action"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

ai_analyst/ai_analyst.py CHANGED
@@ -138,9 +138,21 @@ In 3–4 sentences analyse: activity level, notable price moves or volume spikes
138
  Be specific and data-driven. No headers, no bullet points, plain prose only."""
139
 
140
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ── Kafka consumer (market data) ──────────────────────────────────────────────
142
 
143
- def consume_market_data():
144
  global _running, _suspended
145
  consumer = create_consumer(
146
  topics=[
@@ -162,16 +174,18 @@ def consume_market_data():
162
  if sym:
163
  latest_snapshots[sym] = snap
164
  elif msg.topic == Config.CONTROL_TOPIC:
165
- cmd = msg.value.get("command", "")
166
- if cmd == "start":
167
  _running = True
168
  _suspended = False
169
- elif cmd in ("end", "stop"):
170
  _running = False
171
- elif cmd == "suspend":
172
  _suspended = True
173
- elif cmd == "resume":
174
  _suspended = False
 
 
175
 
176
 
177
  # ── Analysis loop ──────────────────────────────────────────────────────────────
@@ -208,5 +222,5 @@ def analysis_loop(producer):
208
 
209
  if __name__ == "__main__":
210
  producer = create_producer(component_name="AI-Analyst")
211
- threading.Thread(target=consume_market_data, daemon=True).start()
212
  analysis_loop(producer)
 
138
  Be specific and data-driven. No headers, no bullet points, plain prose only."""
139
 
140
 
141
+ def run_immediate_analysis(producer):
142
+ """Called on-demand (button click). Skips the interval check."""
143
+ print("[AI-Analyst] On-demand analysis triggered")
144
+ prompt = build_prompt()
145
+ text = call_llm(prompt)
146
+ if text:
147
+ insight = {"text": text, "timestamp": time.time()}
148
+ producer.send(Config.AI_INSIGHTS_TOPIC, insight)
149
+ producer.flush()
150
+ print(f"[AI-Analyst] On-demand insight published ({len(text)} chars)")
151
+
152
+
153
  # ── Kafka consumer (market data) ──────────────────────────────────────────────
154
 
155
+ def consume_market_data(producer):
156
  global _running, _suspended
157
  consumer = create_consumer(
158
  topics=[
 
174
  if sym:
175
  latest_snapshots[sym] = snap
176
  elif msg.topic == Config.CONTROL_TOPIC:
177
+ action = msg.value.get("action", "")
178
+ if action == "start":
179
  _running = True
180
  _suspended = False
181
+ elif action in ("end", "stop"):
182
  _running = False
183
+ elif action == "suspend":
184
  _suspended = True
185
+ elif action == "resume":
186
  _suspended = False
187
+ elif action == "generate_insight":
188
+ threading.Thread(target=run_immediate_analysis, args=(producer,), daemon=True).start()
189
 
190
 
191
  # ── Analysis loop ──────────────────────────────────────────────────────────────
 
222
 
223
  if __name__ == "__main__":
224
  producer = create_producer(component_name="AI-Analyst")
225
+ threading.Thread(target=consume_market_data, args=(producer,), daemon=True).start()
226
  analysis_loop(producer)
dashboard/dashboard.py CHANGED
@@ -447,6 +447,17 @@ def session_resume():
447
  return jsonify({"status": "error", "error": str(e)}), 500
448
 
449
 
 
 
 
 
 
 
 
 
 
 
 
450
  @app.route("/session/mode", methods=["POST"])
451
  def session_mode():
452
  try:
 
447
  return jsonify({"status": "error", "error": str(e)}), 500
448
 
449
 
450
+ @app.route("/session/ai_insight", methods=["POST"])
451
+ def trigger_ai_insight():
452
+ try:
453
+ p = get_producer()
454
+ p.send(Config.CONTROL_TOPIC, {"action": "generate_insight"})
455
+ p.flush()
456
+ return jsonify({"status": "ok", "message": "Insight generation triggered"})
457
+ except Exception as e:
458
+ return jsonify({"status": "error", "error": str(e)}), 500
459
+
460
+
461
  @app.route("/session/mode", methods=["POST"])
462
  def session_mode():
463
  try:
dashboard/templates/index.html CHANGED
@@ -380,8 +380,12 @@
380
  <div class="ai-panel">
381
  <h2 style="margin:0 0 8px; font-size:15px; display:flex; align-items:center; gap:10px;">
382
  AI Analyst
383
- <span id="ai-model-badge" style="font-size:10px; color:#fff; background:#5c6bc0; padding:2px 8px; border-radius:10px; font-weight:normal;"></span>
384
- <span id="ai-status" style="font-size:11px; color:#999; font-weight:normal; margin-left:4px;">waiting for first insight…</span>
 
 
 
 
385
  </h2>
386
  <div id="ai-insights-list" style="max-height:220px; overflow-y:auto;">
387
  <div class="insight-card" style="color:#bbb; border-left-color:#ddd; background:#fafafa;" id="ai-placeholder">
@@ -428,6 +432,21 @@
428
  // Selected order state
429
  let selectedOrder = null;
430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  function addInsight(insight) {
432
  const list = document.getElementById("ai-insights-list");
433
  const ph = document.getElementById("ai-placeholder");
@@ -1096,6 +1115,9 @@
1096
  addInsight(insight);
1097
  document.getElementById("ai-status").textContent =
1098
  "Last update: " + new Date().toLocaleTimeString();
 
 
 
1099
  });
1100
 
1101
  eventSource.onerror = () => {
 
380
  <div class="ai-panel">
381
  <h2 style="margin:0 0 8px; font-size:15px; display:flex; align-items:center; gap:10px;">
382
  AI Analyst
383
+ <button id="ai-generate-btn" onclick="triggerAIInsight()"
384
+ style="padding:3px 12px; background:#5c6bc0; color:#fff; border:none; border-radius:12px;
385
+ font-size:11px; font-weight:bold; cursor:pointer;">
386
+ Generate Now
387
+ </button>
388
+ <span id="ai-status" style="font-size:11px; color:#999; font-weight:normal;">waiting for first insight…</span>
389
  </h2>
390
  <div id="ai-insights-list" style="max-height:220px; overflow-y:auto;">
391
  <div class="insight-card" style="color:#bbb; border-left-color:#ddd; background:#fafafa;" id="ai-placeholder">
 
432
  // Selected order state
433
  let selectedOrder = null;
434
 
435
+ async function triggerAIInsight() {
436
+ const btn = document.getElementById("ai-generate-btn");
437
+ btn.disabled = true;
438
+ btn.textContent = "Generating…";
439
+ document.getElementById("ai-status").textContent = "generating insight…";
440
+ try {
441
+ await fetch("/session/ai_insight", { method: "POST" });
442
+ } catch(e) {}
443
+ // Re-enable after 10s (LLM needs time)
444
+ setTimeout(() => {
445
+ btn.disabled = false;
446
+ btn.textContent = "Generate Now";
447
+ }, 10000);
448
+ }
449
+
450
  function addInsight(insight) {
451
  const list = document.getElementById("ai-insights-list");
452
  const ph = document.getElementById("ai-placeholder");
 
1115
  addInsight(insight);
1116
  document.getElementById("ai-status").textContent =
1117
  "Last update: " + new Date().toLocaleTimeString();
1118
+ const btn = document.getElementById("ai-generate-btn");
1119
+ btn.disabled = false;
1120
+ btn.textContent = "Generate Now";
1121
  });
1122
 
1123
  eventSource.onerror = () => {