opencandle 0.10.0 → 0.11.0
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.
- package/README.md +7 -5
- package/dist/analysts/orchestrator.d.ts +1 -4
- package/dist/analysts/orchestrator.js +15 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/pi/opencandle-extension.js +13 -221
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/route-manifest.js +0 -1
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.js +35 -4
- package/dist/routing/router.js.map +1 -1
- package/dist/runtime/session-coordinator.js +2 -13
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +49 -6
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/gui/server/http-routes.ts +395 -19
- package/gui/server/invoke-tool.ts +164 -16
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +1 -43
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +117 -8
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +61 -6
- package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +1 -0
- package/gui/web/dist/assets/index-BzyqyVnd.css +2 -0
- package/gui/web/dist/assets/{index-B7QAjY5g.js → index-DvMjkOP9.js} +9 -9
- package/gui/web/dist/index.html +2 -2
- package/package.json +6 -3
- package/src/analysts/orchestrator.ts +15 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/memory/sqlite.ts +23 -19
- package/src/pi/opencandle-extension.ts +12 -265
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/route-manifest.ts +0 -1
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +39 -3
- package/src/runtime/session-coordinator.ts +2 -17
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/dcf.ts +57 -7
- package/src/tools/index.ts +0 -3
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/technical/backtest.ts +167 -108
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- package/src/tools/portfolio/predictions.ts +0 -553
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
2
|
|
|
3
3
|
export type AssetType = "equity" | "etf" | "fund" | "crypto" | "index" | "option" | "unknown";
|
|
4
|
-
export type PredictionDirection = "bullish" | "bearish" | "neutral";
|
|
5
|
-
export type PredictionStatus = "open" | "resolved" | "expired" | "cancelled";
|
|
6
4
|
export type AlertScopeType = "instrument" | "watchlist" | "portfolio";
|
|
7
5
|
|
|
8
6
|
export interface InstrumentInput {
|
|
@@ -100,23 +98,6 @@ export interface PortfolioLotRecord {
|
|
|
100
98
|
updatedAt: string;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
export interface PredictionRecord {
|
|
104
|
-
id: number;
|
|
105
|
-
instrumentId: number;
|
|
106
|
-
symbol: string;
|
|
107
|
-
direction: PredictionDirection;
|
|
108
|
-
conviction: number;
|
|
109
|
-
entryPrice: number;
|
|
110
|
-
targetPrice: number | null;
|
|
111
|
-
openedAt: string;
|
|
112
|
-
expiresAt: string;
|
|
113
|
-
status: PredictionStatus;
|
|
114
|
-
resolvedAt: string | null;
|
|
115
|
-
resultJson: string | null;
|
|
116
|
-
createdAt: string;
|
|
117
|
-
updatedAt: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
101
|
export interface AlertRuleRecord {
|
|
121
102
|
id: number;
|
|
122
103
|
scopeType: AlertScopeType;
|
|
@@ -337,23 +318,6 @@ type PortfolioLotRow = {
|
|
|
337
318
|
updated_at: string;
|
|
338
319
|
};
|
|
339
320
|
|
|
340
|
-
type PredictionRow = {
|
|
341
|
-
id: number;
|
|
342
|
-
instrument_id: number;
|
|
343
|
-
symbol: string;
|
|
344
|
-
direction: PredictionDirection;
|
|
345
|
-
conviction: number;
|
|
346
|
-
entry_price: number;
|
|
347
|
-
target_price: number | null;
|
|
348
|
-
opened_at: string;
|
|
349
|
-
expires_at: string;
|
|
350
|
-
status: PredictionStatus;
|
|
351
|
-
resolved_at: string | null;
|
|
352
|
-
result_json: string | null;
|
|
353
|
-
created_at: string;
|
|
354
|
-
updated_at: string;
|
|
355
|
-
};
|
|
356
|
-
|
|
357
321
|
type AlertRuleRow = {
|
|
358
322
|
id: number;
|
|
359
323
|
scope_type: AlertScopeType;
|
|
@@ -877,75 +841,6 @@ export class MarketStateService {
|
|
|
877
841
|
});
|
|
878
842
|
}
|
|
879
843
|
|
|
880
|
-
recordPrediction(params: {
|
|
881
|
-
instrument: InstrumentInput;
|
|
882
|
-
direction: PredictionDirection;
|
|
883
|
-
conviction: number;
|
|
884
|
-
entryPrice: number;
|
|
885
|
-
targetPrice?: number;
|
|
886
|
-
timeframeDays: number;
|
|
887
|
-
now?: Date;
|
|
888
|
-
}): PredictionRecord {
|
|
889
|
-
const tx = this.db.transaction(() => {
|
|
890
|
-
const instrument = this.upsertInstrument(params.instrument);
|
|
891
|
-
const opened = params.now ?? new Date();
|
|
892
|
-
const expires = new Date(opened);
|
|
893
|
-
expires.setDate(expires.getDate() + params.timeframeDays);
|
|
894
|
-
const nowIso = opened.toISOString();
|
|
895
|
-
const result = this.db
|
|
896
|
-
.prepare(
|
|
897
|
-
`INSERT INTO prediction_records (
|
|
898
|
-
instrument_id, direction, conviction, entry_price, target_price,
|
|
899
|
-
opened_at, expires_at, status, created_at, updated_at
|
|
900
|
-
)
|
|
901
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)`,
|
|
902
|
-
)
|
|
903
|
-
.run(
|
|
904
|
-
instrument.id,
|
|
905
|
-
params.direction,
|
|
906
|
-
params.conviction,
|
|
907
|
-
params.entryPrice,
|
|
908
|
-
params.targetPrice ?? null,
|
|
909
|
-
nowIso,
|
|
910
|
-
expires.toISOString(),
|
|
911
|
-
nowIso,
|
|
912
|
-
nowIso,
|
|
913
|
-
);
|
|
914
|
-
return Number(result.lastInsertRowid);
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
return this.getPrediction(tx());
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
listPredictions(): PredictionRecord[] {
|
|
921
|
-
const rows = this.db
|
|
922
|
-
.prepare(
|
|
923
|
-
`SELECT pr.*, i.symbol
|
|
924
|
-
FROM prediction_records pr
|
|
925
|
-
JOIN instruments i ON i.id = pr.instrument_id
|
|
926
|
-
ORDER BY pr.opened_at, pr.id`,
|
|
927
|
-
)
|
|
928
|
-
.all() as PredictionRow[];
|
|
929
|
-
return rows.map(mapPrediction);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
updatePredictionOutcome(params: {
|
|
933
|
-
id: number;
|
|
934
|
-
status: Exclude<PredictionStatus, "open">;
|
|
935
|
-
resolvedAt: string;
|
|
936
|
-
result: unknown;
|
|
937
|
-
}): PredictionRecord {
|
|
938
|
-
const now = new Date().toISOString();
|
|
939
|
-
this.db
|
|
940
|
-
.prepare(
|
|
941
|
-
`UPDATE prediction_records
|
|
942
|
-
SET status = ?, resolved_at = ?, result_json = ?, updated_at = ?
|
|
943
|
-
WHERE id = ?`,
|
|
944
|
-
)
|
|
945
|
-
.run(params.status, params.resolvedAt, JSON.stringify(params.result), now, params.id);
|
|
946
|
-
return this.getPrediction(params.id);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
844
|
createAlertRule(params: {
|
|
950
845
|
scopeType: AlertScopeType;
|
|
951
846
|
scopeId?: number;
|
|
@@ -1998,18 +1893,6 @@ export class MarketStateService {
|
|
|
1998
1893
|
return row == null ? null : mapPortfolioLot(row);
|
|
1999
1894
|
}
|
|
2000
1895
|
|
|
2001
|
-
private getPrediction(id: number): PredictionRecord {
|
|
2002
|
-
const row = this.db
|
|
2003
|
-
.prepare(
|
|
2004
|
-
`SELECT pr.*, i.symbol
|
|
2005
|
-
FROM prediction_records pr
|
|
2006
|
-
JOIN instruments i ON i.id = pr.instrument_id
|
|
2007
|
-
WHERE pr.id = ?`,
|
|
2008
|
-
)
|
|
2009
|
-
.get(id) as PredictionRow;
|
|
2010
|
-
return mapPrediction(row);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
1896
|
getAlertRule(id: number): AlertRuleRecord {
|
|
2014
1897
|
const row = this.db.prepare("SELECT * FROM alert_rules WHERE id = ?").get(id) as AlertRuleRow;
|
|
2015
1898
|
return mapAlertRule(row);
|
|
@@ -2118,25 +2001,6 @@ function mapPortfolioLot(row: PortfolioLotRow): PortfolioLotRecord {
|
|
|
2118
2001
|
};
|
|
2119
2002
|
}
|
|
2120
2003
|
|
|
2121
|
-
function mapPrediction(row: PredictionRow): PredictionRecord {
|
|
2122
|
-
return {
|
|
2123
|
-
id: row.id,
|
|
2124
|
-
instrumentId: row.instrument_id,
|
|
2125
|
-
symbol: row.symbol,
|
|
2126
|
-
direction: row.direction,
|
|
2127
|
-
conviction: row.conviction,
|
|
2128
|
-
entryPrice: row.entry_price,
|
|
2129
|
-
targetPrice: row.target_price,
|
|
2130
|
-
openedAt: row.opened_at,
|
|
2131
|
-
expiresAt: row.expires_at,
|
|
2132
|
-
status: row.status,
|
|
2133
|
-
resolvedAt: row.resolved_at,
|
|
2134
|
-
resultJson: row.result_json,
|
|
2135
|
-
createdAt: row.created_at,
|
|
2136
|
-
updatedAt: row.updated_at,
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
2004
|
function mapAlertRule(row: AlertRuleRow): AlertRuleRecord {
|
|
2141
2005
|
return {
|
|
2142
2006
|
id: row.id,
|
package/src/memory/sqlite.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import { ensureOpenCandleHomeDir, getStateDbPath } from "../infra/opencandle-paths.js";
|
|
5
5
|
|
|
6
|
-
const CURRENT_SCHEMA_VERSION =
|
|
6
|
+
const CURRENT_SCHEMA_VERSION = 8;
|
|
7
7
|
|
|
8
8
|
const CURRENT_SCHEMA = `
|
|
9
9
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -178,23 +178,6 @@ const CURRENT_SCHEMA = `
|
|
|
178
178
|
ON portfolio_lots(source, source_lot_id)
|
|
179
179
|
WHERE source IS NOT NULL AND source_lot_id IS NOT NULL;
|
|
180
180
|
|
|
181
|
-
CREATE TABLE IF NOT EXISTS prediction_records (
|
|
182
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
183
|
-
instrument_id INTEGER NOT NULL,
|
|
184
|
-
direction TEXT NOT NULL,
|
|
185
|
-
conviction REAL NOT NULL,
|
|
186
|
-
entry_price REAL NOT NULL,
|
|
187
|
-
target_price REAL,
|
|
188
|
-
opened_at TEXT NOT NULL,
|
|
189
|
-
expires_at TEXT NOT NULL,
|
|
190
|
-
status TEXT NOT NULL,
|
|
191
|
-
resolved_at TEXT,
|
|
192
|
-
result_json TEXT,
|
|
193
|
-
created_at TEXT NOT NULL,
|
|
194
|
-
updated_at TEXT NOT NULL,
|
|
195
|
-
FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE RESTRICT
|
|
196
|
-
);
|
|
197
|
-
|
|
198
181
|
CREATE TABLE IF NOT EXISTS alert_rules (
|
|
199
182
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
200
183
|
scope_type TEXT NOT NULL,
|
|
@@ -380,14 +363,21 @@ function ensureCurrentSchema(db: Database.Database): void {
|
|
|
380
363
|
return;
|
|
381
364
|
}
|
|
382
365
|
|
|
366
|
+
if (currentVersion === 7) {
|
|
367
|
+
migrateV7ToV8(db);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
383
371
|
if (currentVersion === 6) {
|
|
384
372
|
migrateV6ToV7(db);
|
|
373
|
+
migrateV7ToV8(db);
|
|
385
374
|
return;
|
|
386
375
|
}
|
|
387
376
|
|
|
388
377
|
if (currentVersion === 5) {
|
|
389
378
|
migrateV5ToV6(db);
|
|
390
379
|
migrateV6ToV7(db);
|
|
380
|
+
migrateV7ToV8(db);
|
|
391
381
|
return;
|
|
392
382
|
}
|
|
393
383
|
|
|
@@ -395,6 +385,7 @@ function ensureCurrentSchema(db: Database.Database): void {
|
|
|
395
385
|
migrateV4ToV5(db);
|
|
396
386
|
migrateV5ToV6(db);
|
|
397
387
|
migrateV6ToV7(db);
|
|
388
|
+
migrateV7ToV8(db);
|
|
398
389
|
return;
|
|
399
390
|
}
|
|
400
391
|
|
|
@@ -403,16 +394,19 @@ function ensureCurrentSchema(db: Database.Database): void {
|
|
|
403
394
|
migrateV4ToV5(db);
|
|
404
395
|
migrateV5ToV6(db);
|
|
405
396
|
migrateV6ToV7(db);
|
|
397
|
+
migrateV7ToV8(db);
|
|
406
398
|
return;
|
|
407
399
|
}
|
|
408
400
|
|
|
409
|
-
// Additive v2 → v3 →
|
|
401
|
+
// Additive v2 → v3 → ... → v7 migration without dropping data (v8 drops
|
|
402
|
+
// prediction_records as the explicit removal of the predictions feature).
|
|
410
403
|
if (currentVersion === 2) {
|
|
411
404
|
migrateV2ToV3(db);
|
|
412
405
|
migrateV3ToV4(db);
|
|
413
406
|
migrateV4ToV5(db);
|
|
414
407
|
migrateV5ToV6(db);
|
|
415
408
|
migrateV6ToV7(db);
|
|
409
|
+
migrateV7ToV8(db);
|
|
416
410
|
return;
|
|
417
411
|
}
|
|
418
412
|
|
|
@@ -487,6 +481,16 @@ function migrateV6ToV7(db: Database.Database): void {
|
|
|
487
481
|
|
|
488
482
|
db.exec(CURRENT_SCHEMA);
|
|
489
483
|
|
|
484
|
+
db.prepare("DELETE FROM schema_version").run();
|
|
485
|
+
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(7);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function migrateV7ToV8(db: Database.Database): void {
|
|
489
|
+
// Predictions feature removal: dropping prediction_records is the explicit,
|
|
490
|
+
// documented destructive step for this table; all other rows are preserved.
|
|
491
|
+
db.exec("DROP TABLE IF EXISTS prediction_records");
|
|
492
|
+
db.exec(CURRENT_SCHEMA);
|
|
493
|
+
|
|
490
494
|
db.prepare("DELETE FROM schema_version").run();
|
|
491
495
|
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(CURRENT_SCHEMA_VERSION);
|
|
492
496
|
}
|
|
@@ -25,15 +25,12 @@ import { formatPreflightDropAnnotation, preflightSymbols } from "../prompts/symb
|
|
|
25
25
|
import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
|
|
26
26
|
import {
|
|
27
27
|
buildResolvedTurnContext,
|
|
28
|
-
classifyWithLegacyRules,
|
|
29
28
|
createPiAiRouterClient,
|
|
30
|
-
hasFinanceSignals,
|
|
31
29
|
resolveOptionsScreenerSlots,
|
|
32
30
|
resolvePortfolioSlots,
|
|
33
31
|
route as routeLlm,
|
|
34
32
|
} from "../routing/index.js";
|
|
35
33
|
import type { RouterInputContext, RouterLlmClient, RouterOutput } from "../routing/router-types.js";
|
|
36
|
-
import { disambiguateSymbols } from "../routing/symbol-disambiguator.js";
|
|
37
34
|
import type { ResolvedTurnContext } from "../routing/turn-context.js";
|
|
38
35
|
import type {
|
|
39
36
|
CompareAssetsSlots,
|
|
@@ -118,9 +115,7 @@ export default function openCandleExtension(
|
|
|
118
115
|
ctx.ui.notify("Usage: /analyze <ticker>", "warning");
|
|
119
116
|
return;
|
|
120
117
|
}
|
|
121
|
-
const definition = buildComprehensiveAnalysisDefinition(symbol
|
|
122
|
-
debate: getConfig().debate,
|
|
123
|
-
});
|
|
118
|
+
const definition = buildComprehensiveAnalysisDefinition(symbol);
|
|
124
119
|
coordinator.executeWorkflow(pi, definition, ctx);
|
|
125
120
|
},
|
|
126
121
|
});
|
|
@@ -674,244 +669,26 @@ export default function openCandleExtension(
|
|
|
674
669
|
return undefined;
|
|
675
670
|
});
|
|
676
671
|
|
|
677
|
-
// Input handling —
|
|
672
|
+
// Input handling — the LLM router is the single production routing path.
|
|
678
673
|
pi.on("input", async (event, ctx) => {
|
|
679
674
|
if (event.source === "extension") return;
|
|
680
675
|
coordinator.clearTickerValidationCache();
|
|
681
676
|
|
|
682
|
-
// Check for comprehensive analysis pattern
|
|
677
|
+
// Check for comprehensive analysis pattern before routing.
|
|
683
678
|
const analysis = isAnalysisRequest(event.text);
|
|
684
679
|
if (analysis.match && analysis.symbol) {
|
|
685
|
-
const definition = buildComprehensiveAnalysisDefinition(analysis.symbol
|
|
686
|
-
debate: getConfig().debate,
|
|
687
|
-
});
|
|
688
|
-
const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
|
|
689
|
-
if (prompt) markOriginalInput(event.text);
|
|
690
|
-
return prompt ? { action: "transform", text: prompt } : { action: "handled" };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const mode = getConfig().routerMode;
|
|
694
|
-
if (mode === "llm") {
|
|
695
|
-
const dispatched = await handleLlmRouterTurn(event.text, ctx);
|
|
696
|
-
// Dispatched a workflow → the original user turn is now represented by
|
|
697
|
-
// the workflow's queued prompts; tell Pi not to also forward it.
|
|
698
|
-
// Fallback path (no dispatch) → let Pi pass the user turn through to the
|
|
699
|
-
// main agent, which will run under the router-supplied fallback context.
|
|
700
|
-
return dispatched || undefined;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// --- explicit legacy rules mode (`OPENCANDLE_ROUTER_MODE=rules`) ---
|
|
704
|
-
// Extract and persist user preferences (legacy regex path)
|
|
705
|
-
coordinator.extractAndStorePreferences(event.text);
|
|
706
|
-
const storage = coordinator.getStorage();
|
|
707
|
-
const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
|
|
708
|
-
|
|
709
|
-
// Classify intent
|
|
710
|
-
let classification = classifyWithLegacyRules(event.text);
|
|
711
|
-
const ruleModeDisambiguation = disambiguateRulesModeSymbols(
|
|
712
|
-
event.text,
|
|
713
|
-
classification.entities.symbols,
|
|
714
|
-
);
|
|
715
|
-
appendSymbolDropEntries(ruleModeDisambiguation.dropped, "rules");
|
|
716
|
-
classification = {
|
|
717
|
-
...classification,
|
|
718
|
-
entities: {
|
|
719
|
-
...classification.entities,
|
|
720
|
-
symbols: ruleModeDisambiguation.kept,
|
|
721
|
-
},
|
|
722
|
-
};
|
|
723
|
-
if (
|
|
724
|
-
isComparePrompt(event.text) &&
|
|
725
|
-
ruleModeDisambiguation.dropped.length > 0 &&
|
|
726
|
-
classification.entities.symbols.length < 2
|
|
727
|
-
) {
|
|
728
|
-
pi.appendEntry("opencandle-workflow-aborted", {
|
|
729
|
-
reason: "symbol-disambiguation-insufficient-symbols",
|
|
730
|
-
dropped: ruleModeDisambiguation.dropped,
|
|
731
|
-
validSymbols: classification.entities.symbols,
|
|
732
|
-
});
|
|
733
|
-
const base = coordinator.buildRouterContextBase(ctx.sessionManager);
|
|
734
|
-
const output: RouterOutput = {
|
|
735
|
-
routeKind: "clarification",
|
|
736
|
-
route: "fallback",
|
|
737
|
-
workflow: "compare_assets",
|
|
738
|
-
entities: classification.entities,
|
|
739
|
-
slots: {},
|
|
740
|
-
preference_updates: [],
|
|
741
|
-
missing_required: ["symbols"],
|
|
742
|
-
tool_bundles: ["clarification"],
|
|
743
|
-
diagnostics: ruleModeDisambiguation.dropped.map((drop) => ({
|
|
744
|
-
code: "symbol_dropped",
|
|
745
|
-
message: `${drop.token} dropped: ${drop.reason}`,
|
|
746
|
-
details: {
|
|
747
|
-
token: drop.token,
|
|
748
|
-
reason: drop.reason,
|
|
749
|
-
signalsChecked: drop.signalsChecked,
|
|
750
|
-
source: "rules",
|
|
751
|
-
},
|
|
752
|
-
})),
|
|
753
|
-
reasoning: "rules-mode acronym disambiguation left fewer than two symbols for comparison",
|
|
754
|
-
};
|
|
755
|
-
const resolvedTurnContext = buildResolvedTurnContext({ text: event.text, ...base }, output, {
|
|
756
|
-
availableToolNames: safeGetAllToolNames(),
|
|
757
|
-
planning: {
|
|
758
|
-
migrationStatuses: getConfig().planningMigrationStatuses,
|
|
759
|
-
},
|
|
760
|
-
});
|
|
761
|
-
coordinator.setPendingResolvedTurnContext({
|
|
762
|
-
...resolvedTurnContext,
|
|
763
|
-
diagnostics: [
|
|
764
|
-
...resolvedTurnContext.diagnostics,
|
|
765
|
-
{
|
|
766
|
-
code: "compare_workflow_aborted",
|
|
767
|
-
message:
|
|
768
|
-
"compare workflow needs at least two validated symbols after acronym disambiguation",
|
|
769
|
-
},
|
|
770
|
-
],
|
|
771
|
-
});
|
|
772
|
-
coordinator.setPendingFallbackContext({
|
|
773
|
-
assumptionsBlock: [
|
|
774
|
-
"Assumptions Context:",
|
|
775
|
-
classification.entities.symbols.length > 0
|
|
776
|
-
? ` valid symbols: ${classification.entities.symbols.join(", ")} (user)`
|
|
777
|
-
: " valid symbols: (none)",
|
|
778
|
-
` dropped ambiguous ticker-like tokens: ${ruleModeDisambiguation.dropped.map((d) => d.token).join(", ")} (no positive ticker signal)`,
|
|
779
|
-
].join("\n"),
|
|
780
|
-
missingRequired: ["symbols"],
|
|
781
|
-
extraContext:
|
|
782
|
-
"Dropped ambiguous ticker-like tokens: " +
|
|
783
|
-
`${ruleModeDisambiguation.dropped.map((d) => d.token).join(", ")}. ` +
|
|
784
|
-
"Ask the user which ticker symbols they want compared before calling comparison tools.",
|
|
785
|
-
});
|
|
786
|
-
applyRouteToolScope(resolvedTurnContext);
|
|
787
|
-
return undefined;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (classification.workflow === "portfolio_builder") {
|
|
791
|
-
const resolution = resolvePortfolioSlots(classification.entities, workflowPrefs);
|
|
792
|
-
coordinator.recordWorkflowRun(
|
|
793
|
-
"portfolio_builder",
|
|
794
|
-
classification.entities,
|
|
795
|
-
resolution.resolved,
|
|
796
|
-
resolution.defaultsUsed,
|
|
797
|
-
);
|
|
798
|
-
pi.appendEntry("opencandle-workflow", {
|
|
799
|
-
workflow: "portfolio_builder",
|
|
800
|
-
entities: classification.entities,
|
|
801
|
-
resolved: resolution.resolved,
|
|
802
|
-
});
|
|
803
|
-
const definition = buildPortfolioWorkflowDefinition(resolution);
|
|
804
|
-
const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
|
|
805
|
-
if (prompt) markOriginalInput(event.text);
|
|
806
|
-
return prompt ? { action: "transform", text: prompt } : { action: "handled" };
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (classification.workflow === "options_screener") {
|
|
810
|
-
const resolution = resolveOptionsScreenerSlots(classification.entities, workflowPrefs);
|
|
811
|
-
if (resolution.missingRequired.length === 0) {
|
|
812
|
-
coordinator.recordWorkflowRun(
|
|
813
|
-
"options_screener",
|
|
814
|
-
classification.entities,
|
|
815
|
-
resolution.resolved,
|
|
816
|
-
resolution.defaultsUsed,
|
|
817
|
-
);
|
|
818
|
-
pi.appendEntry("opencandle-workflow", {
|
|
819
|
-
workflow: "options_screener",
|
|
820
|
-
entities: classification.entities,
|
|
821
|
-
resolved: resolution.resolved,
|
|
822
|
-
});
|
|
823
|
-
const definition = buildOptionsScreenerWorkflowDefinition(resolution);
|
|
824
|
-
const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
|
|
825
|
-
if (prompt) markOriginalInput(event.text);
|
|
826
|
-
return prompt ? { action: "transform", text: prompt } : { action: "handled" };
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (
|
|
831
|
-
classification.workflow === "compare_assets" &&
|
|
832
|
-
classification.entities.symbols.length >= 2
|
|
833
|
-
) {
|
|
834
|
-
const resolution: SlotResolution<CompareAssetsSlots> = {
|
|
835
|
-
resolved: {
|
|
836
|
-
symbols: classification.entities.symbols,
|
|
837
|
-
metrics: classification.entities.compareMetrics,
|
|
838
|
-
timeHorizon: classification.entities.timeHorizon,
|
|
839
|
-
budget: classification.entities.budget,
|
|
840
|
-
assetScope: classification.entities.assetScope,
|
|
841
|
-
},
|
|
842
|
-
sources: {
|
|
843
|
-
symbols: "user",
|
|
844
|
-
...(classification.entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
|
|
845
|
-
...(classification.entities.compareMetrics ? { metrics: "user" as const } : {}),
|
|
846
|
-
...(classification.entities.budget !== undefined ? { budget: "user" as const } : {}),
|
|
847
|
-
...(classification.entities.assetScope ? { assetScope: "user" as const } : {}),
|
|
848
|
-
},
|
|
849
|
-
defaultsUsed: [],
|
|
850
|
-
missingRequired: [],
|
|
851
|
-
};
|
|
852
|
-
coordinator.recordWorkflowRun(
|
|
853
|
-
"compare_assets",
|
|
854
|
-
classification.entities,
|
|
855
|
-
resolution.resolved,
|
|
856
|
-
resolution.defaultsUsed,
|
|
857
|
-
);
|
|
858
|
-
pi.appendEntry("opencandle-workflow", {
|
|
859
|
-
workflow: "compare_assets",
|
|
860
|
-
symbols: classification.entities.symbols,
|
|
861
|
-
});
|
|
862
|
-
const preflight = await preflightCompareResolution(resolution);
|
|
863
|
-
if (!preflight) {
|
|
864
|
-
coordinator.recordWorkflowRun(
|
|
865
|
-
"fallback",
|
|
866
|
-
classification.entities,
|
|
867
|
-
resolution.resolved,
|
|
868
|
-
[],
|
|
869
|
-
"clarification",
|
|
870
|
-
);
|
|
871
|
-
coordinator.setPendingFallbackContext({
|
|
872
|
-
assumptionsBlock: [
|
|
873
|
-
"Assumptions Context:",
|
|
874
|
-
` original symbols: ${classification.entities.symbols.join(", ")} (user)`,
|
|
875
|
-
].join("\n"),
|
|
876
|
-
missingRequired: ["symbols"],
|
|
877
|
-
extraContext:
|
|
878
|
-
"Compare workflow aborted because ticker preflight left fewer than two valid symbols. Ask the user to clarify the intended tickers before calling comparison tools.",
|
|
879
|
-
});
|
|
880
|
-
return undefined;
|
|
881
|
-
}
|
|
882
|
-
const definition = buildCompareAssetsWorkflowDefinition(preflight.resolution);
|
|
883
|
-
applyPreflightAnnotation(definition, preflight.dropped);
|
|
680
|
+
const definition = buildComprehensiveAnalysisDefinition(analysis.symbol);
|
|
884
681
|
const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
|
|
885
682
|
if (prompt) markOriginalInput(event.text);
|
|
886
683
|
return prompt ? { action: "transform", text: prompt } : { action: "handled" };
|
|
887
684
|
}
|
|
888
685
|
|
|
889
|
-
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
classification.workflow !== "unclassified" ||
|
|
896
|
-
classification.entities.symbols.length > 0 ||
|
|
897
|
-
hasFinanceSignals(event.text);
|
|
898
|
-
if (isFinanceFallback) {
|
|
899
|
-
coordinator.recordWorkflowRun("fallback", classification.entities, {}, [], "agent_task");
|
|
900
|
-
coordinator.setPendingFallbackContext({
|
|
901
|
-
assumptionsBlock: "",
|
|
902
|
-
missingRequired: [],
|
|
903
|
-
extraContext:
|
|
904
|
-
classification.entities.symbols.length > 0
|
|
905
|
-
? `Rules-router extracted symbols: ${classification.entities.symbols.join(", ")}.`
|
|
906
|
-
: undefined,
|
|
907
|
-
});
|
|
908
|
-
pi.appendEntry("opencandle-fallback-context", {
|
|
909
|
-
mode: "rules",
|
|
910
|
-
classifiedWorkflow: classification.workflow,
|
|
911
|
-
symbols: classification.entities.symbols,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
return undefined;
|
|
686
|
+
const dispatched = await handleLlmRouterTurn(event.text, ctx);
|
|
687
|
+
// Dispatched a workflow → the original user turn is now represented by
|
|
688
|
+
// the workflow's queued prompts; tell Pi not to also forward it.
|
|
689
|
+
// Fallback path (no dispatch) → let Pi pass the user turn through to the
|
|
690
|
+
// main agent, which will run under the router-supplied fallback context.
|
|
691
|
+
return dispatched || undefined;
|
|
915
692
|
});
|
|
916
693
|
|
|
917
694
|
/**
|
|
@@ -1206,36 +983,6 @@ export default function openCandleExtension(
|
|
|
1206
983
|
}
|
|
1207
984
|
}
|
|
1208
985
|
|
|
1209
|
-
function disambiguateRulesModeSymbols(
|
|
1210
|
-
text: string,
|
|
1211
|
-
extractedSymbols: string[],
|
|
1212
|
-
): {
|
|
1213
|
-
kept: string[];
|
|
1214
|
-
dropped: Array<{ token: string; reason: string; signalsChecked: string[] }>;
|
|
1215
|
-
} {
|
|
1216
|
-
const candidates = mergeSymbols(extractedSymbols, rawTickerLikeTokens(text));
|
|
1217
|
-
const disambiguated = disambiguateSymbols(candidates, text);
|
|
1218
|
-
return {
|
|
1219
|
-
kept: disambiguated.kept.filter((symbol) => extractedSymbols.includes(symbol)),
|
|
1220
|
-
dropped: disambiguated.dropped,
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function rawTickerLikeTokens(text: string): string[] {
|
|
1225
|
-
const tokens: string[] = [];
|
|
1226
|
-
for (const match of text.matchAll(/\$?([A-Za-z]{1,5})\b/g)) {
|
|
1227
|
-
const raw = match[1];
|
|
1228
|
-
if (raw !== raw.toUpperCase()) continue;
|
|
1229
|
-
const token = raw.toUpperCase();
|
|
1230
|
-
if (!tokens.includes(token)) tokens.push(token);
|
|
1231
|
-
}
|
|
1232
|
-
return tokens;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
function isComparePrompt(text: string): boolean {
|
|
1236
|
-
return /\b(?:compare|vs\.?|versus|which\s+is\s+better)\b/i.test(text);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
986
|
async function preflightCompareResolution(
|
|
1240
987
|
resolution: SlotResolution<CompareAssetsSlots>,
|
|
1241
988
|
): Promise<{
|
|
@@ -1436,8 +1183,8 @@ export default function openCandleExtension(
|
|
|
1436
1183
|
ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
|
|
1437
1184
|
): RouterLlmClient | null {
|
|
1438
1185
|
// `ctx.model` is the currently selected pi-ai model. When unset (no auth
|
|
1439
|
-
// configured yet), we skip the router and the main agent
|
|
1440
|
-
//
|
|
1186
|
+
// configured yet), we skip the router and the main agent runs with its
|
|
1187
|
+
// default unrouted flow for the turn.
|
|
1441
1188
|
const model = (ctx as { model?: unknown }).model;
|
|
1442
1189
|
if (!model) return null;
|
|
1443
1190
|
// biome-ignore lint/suspicious/noExplicitAny: Pi typings keep Model generic
|