opencandle 0.6.0 → 0.8.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 +43 -23
- package/dist/cli-main.d.ts +1 -0
- package/dist/cli-main.js +200 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +14 -194
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/doctor/cli-command.d.ts +1 -0
- package/dist/doctor/cli-command.js +69 -0
- package/dist/doctor/cli-command.js.map +1 -0
- package/dist/doctor/render.d.ts +2 -0
- package/dist/doctor/render.js +32 -0
- package/dist/doctor/render.js.map +1 -0
- package/dist/doctor/report.d.ts +63 -0
- package/dist/doctor/report.js +500 -0
- package/dist/doctor/report.js.map +1 -0
- package/dist/infra/index.d.ts +0 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +8 -0
- package/dist/infra/native-dependencies.js +49 -0
- package/dist/infra/native-dependencies.js.map +1 -1
- package/dist/infra/node-version.js +2 -5
- package/dist/infra/node-version.js.map +1 -1
- package/dist/monitor.d.ts +1 -1
- package/dist/monitor.js +2 -1
- package/dist/monitor.js.map +1 -1
- package/dist/onboarding/connect.d.ts +2 -2
- package/dist/onboarding/connect.js +10 -3
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/provider-status.d.ts +46 -0
- package/dist/onboarding/provider-status.js +256 -0
- package/dist/onboarding/provider-status.js.map +1 -0
- package/dist/onboarding/providers.d.ts +85 -8
- package/dist/onboarding/providers.js +87 -9
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/state.d.ts +1 -0
- package/dist/onboarding/state.js +5 -0
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-tags.d.ts +12 -1
- package/dist/onboarding/tool-tags.js +31 -1
- package/dist/onboarding/tool-tags.js.map +1 -1
- package/dist/onboarding/validation.d.ts +2 -2
- package/dist/onboarding/validation.js.map +1 -1
- package/dist/pi/opencandle-extension.js +91 -15
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session.d.ts +0 -1
- package/dist/pi/session.js +2 -1
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +4 -1
- package/dist/pi/tool-adapter.js +5 -4
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +9 -5
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/prompts/workflow-prompts.js +10 -6
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/external-tool-command.d.ts +17 -0
- package/dist/providers/external-tool-command.js +114 -0
- package/dist/providers/external-tool-command.js.map +1 -0
- package/dist/providers/external-tool-error.d.ts +10 -0
- package/dist/providers/external-tool-error.js +21 -0
- package/dist/providers/external-tool-error.js.map +1 -0
- package/dist/providers/reddit-cli.d.ts +32 -0
- package/dist/providers/reddit-cli.js +172 -0
- package/dist/providers/reddit-cli.js.map +1 -0
- package/dist/providers/reddit.d.ts +1 -1
- package/dist/providers/reddit.js +7 -35
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/twitter-cli.d.ts +36 -0
- package/dist/providers/twitter-cli.js +124 -0
- package/dist/providers/twitter-cli.js.map +1 -0
- package/dist/providers/twitter.d.ts +0 -8
- package/dist/providers/twitter.js +4 -54
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/wrap-provider.js +30 -0
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.js +53 -32
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +7 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/planning.d.ts +1 -1
- package/dist/routing/planning.js +9 -6
- package/dist/routing/planning.js.map +1 -1
- package/dist/routing/router.js +7 -3
- package/dist/routing/router.js.map +1 -1
- package/dist/runtime/answer-contracts.d.ts +1 -1
- package/dist/runtime/answer-contracts.js +12 -1
- package/dist/runtime/answer-contracts.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.js +6 -2
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
- package/dist/sentiment/index.d.ts +1 -0
- package/dist/sentiment/index.js +1 -0
- package/dist/sentiment/index.js.map +1 -1
- package/dist/sentiment/insights.d.ts +17 -0
- package/dist/sentiment/insights.js +206 -0
- package/dist/sentiment/insights.js.map +1 -0
- package/dist/sentiment/pipeline.js +13 -1
- package/dist/sentiment/pipeline.js.map +1 -1
- package/dist/sentiment/scorer.d.ts +2 -0
- package/dist/sentiment/scorer.js +10 -1
- package/dist/sentiment/scorer.js.map +1 -1
- package/dist/sentiment/types.d.ts +2 -0
- package/dist/sentiment/types.js.map +1 -1
- package/dist/system-prompt.js +3 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/index.d.ts +5 -2
- package/dist/tools/index.js +8 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/sentiment/insight-format.d.ts +2 -0
- package/dist/tools/sentiment/insight-format.js +36 -0
- package/dist/tools/sentiment/insight-format.js.map +1 -0
- package/dist/tools/sentiment/query-match.d.ts +3 -0
- package/dist/tools/sentiment/query-match.js +113 -0
- package/dist/tools/sentiment/query-match.js.map +1 -0
- package/dist/tools/sentiment/reddit-sentiment.d.ts +12 -1
- package/dist/tools/sentiment/reddit-sentiment.js +263 -117
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +9 -1
- package/dist/tools/sentiment/sentiment-summary.js +217 -201
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +11 -1
- package/dist/tools/sentiment/twitter-sentiment.js +187 -64
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.js +4 -0
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/types/sentiment.d.ts +52 -0
- package/gui/server/http-routes.ts +419 -0
- package/gui/server/invoke-tool.ts +17 -3
- package/gui/server/model-setup.ts +10 -3
- package/gui/server/projector.ts +6 -2
- package/gui/server/server.ts +30 -275
- package/gui/server/tool-metadata.ts +80 -16
- package/gui/server/ws-hub.ts +19 -0
- package/gui/web/dist/assets/CatalogOverlay-CCVKwBUB.js +1 -0
- package/gui/web/dist/assets/index-Dm4Aom2_.js +69 -0
- package/gui/web/dist/assets/{index-2KZtKBmu.css → index-UzZUg3dx.css} +1 -1
- package/gui/web/dist/index.html +2 -2
- package/package.json +17 -14
- package/src/cli-main.ts +233 -0
- package/src/cli.ts +14 -226
- package/src/config.ts +27 -0
- package/src/doctor/cli-command.ts +83 -0
- package/src/doctor/render.ts +37 -0
- package/src/doctor/report.ts +638 -0
- package/src/infra/index.ts +0 -1
- package/src/infra/native-dependencies.ts +67 -0
- package/src/infra/node-version.ts +2 -5
- package/src/monitor.ts +3 -1
- package/src/onboarding/connect.ts +20 -4
- package/src/onboarding/provider-status.ts +382 -0
- package/src/onboarding/providers.ts +148 -18
- package/src/onboarding/state.ts +9 -0
- package/src/onboarding/tool-tags.ts +45 -2
- package/src/onboarding/validation.ts +2 -2
- package/src/pi/opencandle-extension.ts +115 -17
- package/src/pi/session.ts +2 -1
- package/src/pi/tool-adapter.ts +14 -4
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +13 -5
- package/src/prompts/workflow-prompts.ts +13 -6
- package/src/providers/external-tool-command.ts +164 -0
- package/src/providers/external-tool-error.ts +20 -0
- package/src/providers/reddit-cli.ts +286 -0
- package/src/providers/reddit.ts +7 -63
- package/src/providers/twitter-cli.ts +202 -0
- package/src/providers/twitter.ts +4 -73
- package/src/providers/wrap-provider.ts +34 -0
- package/src/providers/yahoo-finance.ts +65 -32
- package/src/routing/classify-intent.ts +11 -6
- package/src/routing/planning.ts +16 -10
- package/src/routing/router.ts +11 -6
- package/src/runtime/answer-contracts.ts +23 -2
- package/src/runtime/tool-defaults-wrapper.ts +12 -2
- package/src/sentiment/index.ts +1 -0
- package/src/sentiment/insights.ts +269 -0
- package/src/sentiment/pipeline.ts +13 -1
- package/src/sentiment/scorer.ts +12 -1
- package/src/sentiment/types.ts +3 -0
- package/src/system-prompt.ts +3 -7
- package/src/tools/index.ts +9 -8
- package/src/tools/sentiment/insight-format.ts +50 -0
- package/src/tools/sentiment/query-match.ts +117 -0
- package/src/tools/sentiment/reddit-sentiment.ts +354 -141
- package/src/tools/sentiment/sentiment-summary.ts +283 -237
- package/src/tools/sentiment/twitter-sentiment.ts +262 -78
- package/src/tools/sentiment/web-sentiment.ts +4 -0
- package/src/types/sentiment.ts +59 -0
- package/dist/infra/browser.d.ts +0 -35
- package/dist/infra/browser.js +0 -105
- package/dist/infra/browser.js.map +0 -1
- package/dist/tools/interaction/twitter-login.d.ts +0 -8
- package/dist/tools/interaction/twitter-login.js +0 -87
- package/dist/tools/interaction/twitter-login.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +0 -1
- package/gui/web/dist/assets/index-CveNgtDg.js +0 -69
- package/src/infra/browser.ts +0 -113
- package/src/tools/interaction/twitter-login.ts +0 -105
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
// Provider registry — single source of truth for OpenCandle's
|
|
2
|
-
//
|
|
1
|
+
// Provider registry — single source of truth for OpenCandle's third-party
|
|
2
|
+
// data providers. Every setup/status pathway iterates this registry:
|
|
3
3
|
// first-run startup, the `/connect` command, the `tool_result` credential
|
|
4
|
-
// interception handler,
|
|
4
|
+
// interception handler, GUI provider rows, doctor checks, and gap-note
|
|
5
|
+
// generation all read from here.
|
|
5
6
|
//
|
|
6
|
-
// Adding a new
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// the array ever disagree.
|
|
7
|
+
// Adding a new provider is a two-step change: add its id to the relevant
|
|
8
|
+
// literal union below, and add its descriptor to the `PROVIDERS` array. The
|
|
9
|
+
// `satisfies` and exhaustiveness checks keep the registry and id unions aligned.
|
|
10
10
|
|
|
11
11
|
import { getConfig, loadFileConfig } from "../config.js";
|
|
12
12
|
|
|
13
|
-
export type
|
|
13
|
+
export type ApiKeyProviderId = "alpha_vantage" | "fred" | "finnhub" | "brave" | "exa";
|
|
14
|
+
export type ExternalToolProviderId = "twitter" | "reddit";
|
|
15
|
+
export type PublicHttpProviderId = "yahoo";
|
|
16
|
+
export type ProviderId = ApiKeyProviderId | ExternalToolProviderId | PublicHttpProviderId;
|
|
14
17
|
|
|
15
|
-
export type ProviderCategory =
|
|
18
|
+
export type ProviderCategory =
|
|
19
|
+
| "fundamentals"
|
|
20
|
+
| "macro"
|
|
21
|
+
| "news"
|
|
22
|
+
| "web_search"
|
|
23
|
+
| "sentiment"
|
|
24
|
+
| "market";
|
|
16
25
|
|
|
17
26
|
export type ProviderTier = "hard" | "soft";
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
interface BaseProviderDescriptor {
|
|
20
29
|
readonly id: ProviderId;
|
|
30
|
+
readonly kind: "api-key" | "external-tool" | "public-http";
|
|
21
31
|
readonly displayName: string;
|
|
22
32
|
readonly category: ProviderCategory;
|
|
23
33
|
/**
|
|
@@ -30,11 +40,6 @@ export interface ProviderDescriptor {
|
|
|
30
40
|
readonly tier: ProviderTier;
|
|
31
41
|
/** Lowercase friendly aliases accepted by `/connect` in addition to the id. */
|
|
32
42
|
readonly aliases: readonly string[];
|
|
33
|
-
readonly signupUrl: string;
|
|
34
|
-
readonly freeTier: boolean;
|
|
35
|
-
readonly envVar: string;
|
|
36
|
-
/** Nested key path into `OpenCandleFileConfig` where the key is persisted. */
|
|
37
|
-
readonly configPath: readonly string[];
|
|
38
43
|
readonly unlocks: readonly string[];
|
|
39
44
|
/**
|
|
40
45
|
* Human copy describing the degraded experience when missing, or `null`
|
|
@@ -45,11 +50,43 @@ export interface ProviderDescriptor {
|
|
|
45
50
|
readonly instructionsHint: string;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
export interface ApiKeyProviderDescriptor extends BaseProviderDescriptor {
|
|
54
|
+
readonly id: ApiKeyProviderId;
|
|
55
|
+
readonly kind: "api-key";
|
|
56
|
+
readonly signupUrl: string;
|
|
57
|
+
readonly freeTier: boolean;
|
|
58
|
+
readonly envVar: string;
|
|
59
|
+
/** Nested key path into `OpenCandleFileConfig` where the key is persisted. */
|
|
60
|
+
readonly configPath: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ExternalToolProviderDescriptor extends BaseProviderDescriptor {
|
|
64
|
+
readonly id: ExternalToolProviderId;
|
|
65
|
+
readonly kind: "external-tool";
|
|
66
|
+
readonly binary: string;
|
|
67
|
+
readonly installCmd: string;
|
|
68
|
+
readonly sessionSource: "browser-cookies";
|
|
69
|
+
readonly supportedBrowsers?: readonly string[];
|
|
70
|
+
readonly sessionProbeArgs?: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PublicHttpProviderDescriptor extends BaseProviderDescriptor {
|
|
74
|
+
readonly id: PublicHttpProviderId;
|
|
75
|
+
readonly kind: "public-http";
|
|
76
|
+
readonly probeUrl: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ProviderDescriptor =
|
|
80
|
+
| ApiKeyProviderDescriptor
|
|
81
|
+
| ExternalToolProviderDescriptor
|
|
82
|
+
| PublicHttpProviderDescriptor;
|
|
83
|
+
|
|
48
84
|
// Declaration order matters: picker display order, per-workflow prompt priority,
|
|
49
85
|
// getProvidersByCategory/getProvidersByTier iteration order.
|
|
50
86
|
export const PROVIDERS = [
|
|
51
87
|
{
|
|
52
88
|
id: "alpha_vantage",
|
|
89
|
+
kind: "api-key",
|
|
53
90
|
displayName: "Alpha Vantage",
|
|
54
91
|
category: "fundamentals",
|
|
55
92
|
tier: "hard",
|
|
@@ -70,6 +107,7 @@ export const PROVIDERS = [
|
|
|
70
107
|
},
|
|
71
108
|
{
|
|
72
109
|
id: "fred",
|
|
110
|
+
kind: "api-key",
|
|
73
111
|
displayName: "FRED",
|
|
74
112
|
category: "macro",
|
|
75
113
|
tier: "hard",
|
|
@@ -85,6 +123,7 @@ export const PROVIDERS = [
|
|
|
85
123
|
},
|
|
86
124
|
{
|
|
87
125
|
id: "finnhub",
|
|
126
|
+
kind: "api-key",
|
|
88
127
|
displayName: "Finnhub",
|
|
89
128
|
category: "news",
|
|
90
129
|
tier: "soft",
|
|
@@ -104,6 +143,7 @@ export const PROVIDERS = [
|
|
|
104
143
|
},
|
|
105
144
|
{
|
|
106
145
|
id: "brave",
|
|
146
|
+
kind: "api-key",
|
|
107
147
|
displayName: "Brave Search",
|
|
108
148
|
category: "web_search",
|
|
109
149
|
tier: "soft",
|
|
@@ -123,6 +163,7 @@ export const PROVIDERS = [
|
|
|
123
163
|
},
|
|
124
164
|
{
|
|
125
165
|
id: "exa",
|
|
166
|
+
kind: "api-key",
|
|
126
167
|
displayName: "Exa",
|
|
127
168
|
category: "web_search",
|
|
128
169
|
tier: "soft",
|
|
@@ -147,8 +188,64 @@ export const PROVIDERS = [
|
|
|
147
188
|
snoozeDurationDays: 7,
|
|
148
189
|
instructionsHint: "Paid with free tier, signup opens in your browser",
|
|
149
190
|
},
|
|
191
|
+
{
|
|
192
|
+
id: "yahoo",
|
|
193
|
+
kind: "public-http",
|
|
194
|
+
displayName: "Yahoo Finance",
|
|
195
|
+
category: "market",
|
|
196
|
+
tier: "hard",
|
|
197
|
+
aliases: ["yahoo", "yahoo-finance", "market-data", "options"],
|
|
198
|
+
probeUrl: "https://query1.finance.yahoo.com/v8/finance/chart/SPY?interval=1d&range=1d",
|
|
199
|
+
unlocks: ["quotes", "historical prices", "option chains", "market data"],
|
|
200
|
+
fallbackDescription: null,
|
|
201
|
+
snoozeDurationDays: 7,
|
|
202
|
+
instructionsHint: "No account needed; OpenCandle checks public Yahoo Finance reachability",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "twitter",
|
|
206
|
+
kind: "external-tool",
|
|
207
|
+
displayName: "X / Twitter",
|
|
208
|
+
category: "sentiment",
|
|
209
|
+
tier: "soft",
|
|
210
|
+
aliases: ["twitter", "x", "x-sentiment", "twitter-sentiment"],
|
|
211
|
+
binary: "twitter",
|
|
212
|
+
installCmd: "uv tool install twitter-cli",
|
|
213
|
+
sessionSource: "browser-cookies",
|
|
214
|
+
sessionProbeArgs: ["feed", "--max", "1", "--json"],
|
|
215
|
+
supportedBrowsers: ["Chrome", "Arc", "Edge", "Firefox", "Brave"],
|
|
216
|
+
unlocks: ["X/Twitter sentiment", "recent ticker mentions", "social engagement context"],
|
|
217
|
+
fallbackDescription:
|
|
218
|
+
"Sentiment summaries continue with Reddit, web search, and news when X is unavailable",
|
|
219
|
+
snoozeDurationDays: 7,
|
|
220
|
+
instructionsHint: "Install twitter-cli and stay logged into x.com in a supported browser",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "reddit",
|
|
224
|
+
kind: "external-tool",
|
|
225
|
+
displayName: "Reddit",
|
|
226
|
+
category: "sentiment",
|
|
227
|
+
tier: "soft",
|
|
228
|
+
aliases: ["reddit", "reddit-sentiment", "subreddit"],
|
|
229
|
+
binary: "rdt",
|
|
230
|
+
installCmd: "uv tool install rdt-cli",
|
|
231
|
+
sessionSource: "browser-cookies",
|
|
232
|
+
supportedBrowsers: ["Chrome", "Arc", "Edge", "Firefox", "Brave"],
|
|
233
|
+
sessionProbeArgs: ["status", "--json"],
|
|
234
|
+
unlocks: ["Reddit sentiment", "ticker discussion context", "retail investor discussion"],
|
|
235
|
+
fallbackDescription:
|
|
236
|
+
"Sentiment summaries continue with X/Twitter, web search, and news when Reddit is unavailable",
|
|
237
|
+
snoozeDurationDays: 7,
|
|
238
|
+
instructionsHint: "Install rdt-cli and stay logged into reddit.com in a supported browser",
|
|
239
|
+
},
|
|
150
240
|
] as const satisfies readonly ProviderDescriptor[];
|
|
151
241
|
|
|
242
|
+
const _providerIdExhaustivenessCheck: Record<
|
|
243
|
+
| Exclude<ProviderId, (typeof PROVIDERS)[number]["id"]>
|
|
244
|
+
| Exclude<(typeof PROVIDERS)[number]["id"], ProviderId>,
|
|
245
|
+
never
|
|
246
|
+
> = {};
|
|
247
|
+
void _providerIdExhaustivenessCheck;
|
|
248
|
+
|
|
152
249
|
// -----------------------------------------------------------------------------
|
|
153
250
|
// Lookup helpers
|
|
154
251
|
// -----------------------------------------------------------------------------
|
|
@@ -169,6 +266,28 @@ export function listAllProviders(): readonly ProviderDescriptor[] {
|
|
|
169
266
|
return PROVIDERS;
|
|
170
267
|
}
|
|
171
268
|
|
|
269
|
+
export function isApiKeyProvider(
|
|
270
|
+
provider: ProviderDescriptor,
|
|
271
|
+
): provider is ApiKeyProviderDescriptor {
|
|
272
|
+
return provider.kind === "api-key";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function isExternalToolProvider(
|
|
276
|
+
provider: ProviderDescriptor,
|
|
277
|
+
): provider is ExternalToolProviderDescriptor {
|
|
278
|
+
return provider.kind === "external-tool";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function isPublicHttpProvider(
|
|
282
|
+
provider: ProviderDescriptor,
|
|
283
|
+
): provider is PublicHttpProviderDescriptor {
|
|
284
|
+
return provider.kind === "public-http";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function listApiKeyProviders(): readonly ApiKeyProviderDescriptor[] {
|
|
288
|
+
return (PROVIDERS as readonly ProviderDescriptor[]).filter(isApiKeyProvider);
|
|
289
|
+
}
|
|
290
|
+
|
|
172
291
|
export function getProvider(id: ProviderId): ProviderDescriptor {
|
|
173
292
|
const found = byId().get(id);
|
|
174
293
|
if (!found) {
|
|
@@ -209,7 +328,7 @@ function readConfigValueByPath(
|
|
|
209
328
|
// `hasCredential` reads from `getConfig()` so that tests mocking `getConfig`
|
|
210
329
|
// see a consistent view; `getCredentialSource` reads `process.env` +
|
|
211
330
|
// `loadFileConfig` directly because it needs to distinguish env from file.
|
|
212
|
-
const CONFIG_FIELD_BY_ID: Record<
|
|
331
|
+
const CONFIG_FIELD_BY_ID: Record<ApiKeyProviderId, keyof ReturnType<typeof getConfig>> = {
|
|
213
332
|
alpha_vantage: "alphaVantageApiKey",
|
|
214
333
|
fred: "fredApiKey",
|
|
215
334
|
finnhub: "finnhubApiKey",
|
|
@@ -218,7 +337,9 @@ const CONFIG_FIELD_BY_ID: Record<ProviderId, keyof ReturnType<typeof getConfig>>
|
|
|
218
337
|
};
|
|
219
338
|
|
|
220
339
|
export function hasCredential(id: ProviderId): boolean {
|
|
221
|
-
const
|
|
340
|
+
const descriptor = getProvider(id);
|
|
341
|
+
if (!isApiKeyProvider(descriptor)) return false;
|
|
342
|
+
const field = CONFIG_FIELD_BY_ID[descriptor.id];
|
|
222
343
|
const value = getConfig()[field];
|
|
223
344
|
return typeof value === "string" && value.length > 0;
|
|
224
345
|
}
|
|
@@ -231,6 +352,8 @@ export function getCredential(
|
|
|
231
352
|
id: ProviderId,
|
|
232
353
|
): { source: "env" | "file"; value: string } | { source: "absent"; value?: undefined } {
|
|
233
354
|
const descriptor = getProvider(id);
|
|
355
|
+
if (!isApiKeyProvider(descriptor)) return { source: "absent" };
|
|
356
|
+
|
|
234
357
|
const envValue = process.env[descriptor.envVar];
|
|
235
358
|
if (envValue && envValue.length > 0) return { source: "env", value: envValue };
|
|
236
359
|
|
|
@@ -265,7 +388,14 @@ export function resolveProviderFromArgument(
|
|
|
265
388
|
// 3. Category match: if the needle matches a category name, return the
|
|
266
389
|
// providers in that category. One match → single descriptor. Multiple
|
|
267
390
|
// matches → array (triggers the sub-picker in the /connect handler).
|
|
268
|
-
const categories: readonly ProviderCategory[] = [
|
|
391
|
+
const categories: readonly ProviderCategory[] = [
|
|
392
|
+
"fundamentals",
|
|
393
|
+
"macro",
|
|
394
|
+
"news",
|
|
395
|
+
"web_search",
|
|
396
|
+
"sentiment",
|
|
397
|
+
"market",
|
|
398
|
+
];
|
|
269
399
|
const normalizedCategory = needle.replace("-", "_");
|
|
270
400
|
if ((categories as readonly string[]).includes(normalizedCategory)) {
|
|
271
401
|
const group = getProvidersByCategory(normalizedCategory as ProviderCategory);
|
package/src/onboarding/state.ts
CHANGED
|
@@ -155,6 +155,15 @@ export function markProviderNeverAsk(state: OnboardingState, id: ProviderId): On
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
export function clearProviderOnboardingEntry(
|
|
159
|
+
state: OnboardingState,
|
|
160
|
+
id: ProviderId,
|
|
161
|
+
): OnboardingState {
|
|
162
|
+
const providers = { ...state.providers };
|
|
163
|
+
delete providers[id];
|
|
164
|
+
return { ...state, providers };
|
|
165
|
+
}
|
|
166
|
+
|
|
158
167
|
export function markWelcomeShown(state: OnboardingState): OnboardingState {
|
|
159
168
|
return { ...state, welcomeShownAt: nowIso() };
|
|
160
169
|
}
|
|
@@ -22,6 +22,7 @@ import type { ProviderId } from "./providers.js";
|
|
|
22
22
|
// -----------------------------------------------------------------------------
|
|
23
23
|
|
|
24
24
|
export type CredentialRequiredReason = "missing" | "stale";
|
|
25
|
+
export type ExternalToolRequiredReason = "not_installed" | "session_missing" | "session_stale";
|
|
25
26
|
|
|
26
27
|
export interface CredentialRequiredTagFields {
|
|
27
28
|
provider: ProviderId;
|
|
@@ -48,11 +49,20 @@ export interface ConnectedTagFields {
|
|
|
48
49
|
provider: ProviderId;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
export interface ExternalToolRequiredTagFields {
|
|
53
|
+
provider: ProviderId;
|
|
54
|
+
reason: ExternalToolRequiredReason;
|
|
55
|
+
installCmd: string;
|
|
56
|
+
loginCmd?: string;
|
|
57
|
+
fallback: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
export type ParsedTag =
|
|
52
61
|
| ({ kind: "credential_required" } & CredentialRequiredTagFields)
|
|
53
62
|
| ({ kind: "soft_degraded" } & SoftDegradedTagFields)
|
|
54
63
|
| ({ kind: "skipped" } & SkippedTagFields)
|
|
55
|
-
| ({ kind: "connected" } & ConnectedTagFields)
|
|
64
|
+
| ({ kind: "connected" } & ConnectedTagFields)
|
|
65
|
+
| ({ kind: "external_tool_required" } & ExternalToolRequiredTagFields);
|
|
56
66
|
|
|
57
67
|
// -----------------------------------------------------------------------------
|
|
58
68
|
// Builders
|
|
@@ -106,12 +116,24 @@ export function buildConnectedTag(fields: ConnectedTagFields): string {
|
|
|
106
116
|
return `[OPENCANDLE_CONNECTED provider=${fields.provider}]`;
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
export function buildExternalToolRequiredTag(fields: ExternalToolRequiredTagFields): string {
|
|
120
|
+
const parts: string[] = [
|
|
121
|
+
"[OPENCANDLE_EXTERNAL_TOOL_REQUIRED",
|
|
122
|
+
formatField("provider", fields.provider),
|
|
123
|
+
formatField("reason", fields.reason),
|
|
124
|
+
`installCmd=${quote(fields.installCmd)}`,
|
|
125
|
+
formatField("fallback", fields.fallback ?? "none"),
|
|
126
|
+
];
|
|
127
|
+
if (fields.loginCmd) parts.push(`loginCmd=${quote(fields.loginCmd)}`);
|
|
128
|
+
return `${parts.join(" ")}]`;
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
// -----------------------------------------------------------------------------
|
|
110
132
|
// Parser
|
|
111
133
|
// -----------------------------------------------------------------------------
|
|
112
134
|
|
|
113
135
|
const TAG_LINE_REGEX =
|
|
114
|
-
/\[OPENCANDLE_(CREDENTIAL_REQUIRED|SOFT_DEGRADED|SKIPPED|CONNECTED)([^\]]*)\]/;
|
|
136
|
+
/\[OPENCANDLE_(CREDENTIAL_REQUIRED|SOFT_DEGRADED|SKIPPED|CONNECTED|EXTERNAL_TOOL_REQUIRED)([^\]]*)\]/;
|
|
115
137
|
|
|
116
138
|
function parseFields(raw: string): Record<string, string> {
|
|
117
139
|
// Split on `key=value` pairs, respecting quoted values.
|
|
@@ -195,6 +217,27 @@ export function parseToolTag(text: string): ParsedTag | undefined {
|
|
|
195
217
|
return { kind: "connected", provider: provider as ProviderId };
|
|
196
218
|
}
|
|
197
219
|
|
|
220
|
+
case "EXTERNAL_TOOL_REQUIRED": {
|
|
221
|
+
const { provider, reason, installCmd, loginCmd, fallback } = fields;
|
|
222
|
+
if (
|
|
223
|
+
!provider ||
|
|
224
|
+
(reason !== "not_installed" &&
|
|
225
|
+
reason !== "session_missing" &&
|
|
226
|
+
reason !== "session_stale") ||
|
|
227
|
+
!installCmd
|
|
228
|
+
) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
kind: "external_tool_required",
|
|
233
|
+
provider: provider as ProviderId,
|
|
234
|
+
reason,
|
|
235
|
+
installCmd,
|
|
236
|
+
loginCmd,
|
|
237
|
+
fallback: fallback === undefined || fallback === "none" ? null : fallback,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
198
241
|
default:
|
|
199
242
|
return undefined;
|
|
200
243
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// All validators share a 5-second timeout via `AbortSignal.timeout` so the
|
|
21
21
|
// connect flow cannot hang if a provider is unreachable.
|
|
22
22
|
|
|
23
|
-
import type {
|
|
23
|
+
import type { ApiKeyProviderId } from "./providers.js";
|
|
24
24
|
|
|
25
25
|
const VALIDATION_TIMEOUT_MS = 5_000;
|
|
26
26
|
|
|
@@ -107,7 +107,7 @@ function classifyAlphaVantageBody(body: string): ValidationResult | undefined {
|
|
|
107
107
|
* touch the config file, onboarding state, or any in-memory cache.
|
|
108
108
|
*/
|
|
109
109
|
export async function validateCredential(
|
|
110
|
-
providerId:
|
|
110
|
+
providerId: ApiKeyProviderId,
|
|
111
111
|
key: string,
|
|
112
112
|
): Promise<ValidationResult> {
|
|
113
113
|
switch (providerId) {
|
|
@@ -44,7 +44,6 @@ import type {
|
|
|
44
44
|
import { SessionCoordinator } from "../runtime/session-coordinator.js";
|
|
45
45
|
import { generateSessionTitle } from "../runtime/session-title.js";
|
|
46
46
|
import { registerAskUserTool } from "../tools/interaction/ask-user.js";
|
|
47
|
-
import { registerTwitterLoginTool } from "../tools/interaction/twitter-login.js";
|
|
48
47
|
import type { AskUserHandler } from "../types/index.js";
|
|
49
48
|
import {
|
|
50
49
|
buildCompareAssetsWorkflowDefinition,
|
|
@@ -105,11 +104,10 @@ export default function openCandleExtension(
|
|
|
105
104
|
let sessionTitleAttempted = false;
|
|
106
105
|
|
|
107
106
|
// Register tools
|
|
108
|
-
for (const tool of getOpenCandleToolDefinitions()) {
|
|
107
|
+
for (const tool of getOpenCandleToolDefinitions({ askUserHandler: options?.askUserHandler })) {
|
|
109
108
|
pi.registerTool(tool);
|
|
110
109
|
}
|
|
111
110
|
registerAskUserTool(pi, options?.askUserHandler);
|
|
112
|
-
registerTwitterLoginTool(pi);
|
|
113
111
|
|
|
114
112
|
// /analyze command
|
|
115
113
|
pi.registerCommand("analyze", {
|
|
@@ -144,11 +142,10 @@ export default function openCandleExtension(
|
|
|
144
142
|
// `/connect <alias|id|category>` routes to a specific provider (or a
|
|
145
143
|
// sub-picker for multi-provider categories like "search").
|
|
146
144
|
pi.registerCommand("connect", {
|
|
147
|
-
description: "Connect
|
|
145
|
+
description: "Connect an API-key data provider (Alpha Vantage, FRED, Finnhub, Brave, Exa)",
|
|
148
146
|
handler: async (args, ctx) => {
|
|
149
|
-
const {
|
|
150
|
-
"../onboarding/providers.js"
|
|
151
|
-
);
|
|
147
|
+
const { listApiKeyProviders, resolveProviderFromArgument, hasCredential, isApiKeyProvider } =
|
|
148
|
+
await import("../onboarding/providers.js");
|
|
152
149
|
|
|
153
150
|
const formatState = (id: ProviderId): string => {
|
|
154
151
|
const state = loadOnboardingState().providers[id];
|
|
@@ -178,11 +175,11 @@ export default function openCandleExtension(
|
|
|
178
175
|
|
|
179
176
|
if (trimmed === "") {
|
|
180
177
|
// Bare /connect → full picker.
|
|
181
|
-
targetId = await pickProvider(
|
|
178
|
+
targetId = await pickProvider(listApiKeyProviders());
|
|
182
179
|
} else {
|
|
183
180
|
const resolved = resolveProviderFromArgument(trimmed);
|
|
184
181
|
if (!resolved) {
|
|
185
|
-
const all =
|
|
182
|
+
const all = listApiKeyProviders()
|
|
186
183
|
.map((p) => ` ${p.displayName} (${p.aliases.join(", ")})`)
|
|
187
184
|
.join("\n");
|
|
188
185
|
ctx.ui.notify(`Unknown provider: "${trimmed}". Available:\n${all}`, "warning");
|
|
@@ -190,9 +187,25 @@ export default function openCandleExtension(
|
|
|
190
187
|
}
|
|
191
188
|
if (Array.isArray(resolved)) {
|
|
192
189
|
// Multi-provider category — show a sub-picker.
|
|
193
|
-
|
|
190
|
+
const apiKeyProviders = resolved.filter(isApiKeyProvider);
|
|
191
|
+
if (apiKeyProviders.length === 0) {
|
|
192
|
+
ctx.ui.notify(
|
|
193
|
+
`"${trimmed}" does not use API-key setup. Run opencandle doctor for setup status.`,
|
|
194
|
+
"warning",
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
targetId = await pickProvider(apiKeyProviders);
|
|
194
199
|
} else {
|
|
195
|
-
|
|
200
|
+
const descriptor = resolved as ReturnType<typeof getProvider>;
|
|
201
|
+
if (!isApiKeyProvider(descriptor)) {
|
|
202
|
+
ctx.ui.notify(
|
|
203
|
+
`${descriptor.displayName} does not use API-key setup. Run opencandle doctor for setup status.`,
|
|
204
|
+
"warning",
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
targetId = descriptor.id;
|
|
196
209
|
}
|
|
197
210
|
}
|
|
198
211
|
|
|
@@ -389,15 +402,100 @@ export default function openCandleExtension(
|
|
|
389
402
|
}
|
|
390
403
|
}
|
|
391
404
|
|
|
392
|
-
// Second pass: look for
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
// is acted on — subsequent hard-tier prompts are silenced by the
|
|
396
|
-
// per-workflow cap at the decision-function level.
|
|
405
|
+
// Second pass: look for setup-required tags; on match, run the interception
|
|
406
|
+
// decision and either replace the tool result or prompt the user. Only the
|
|
407
|
+
// first setup tag in the content list is acted on.
|
|
397
408
|
for (const block of event.content) {
|
|
398
409
|
if (block.type !== "text") continue;
|
|
399
410
|
const parsed = parseToolTag(block.text);
|
|
400
|
-
if (
|
|
411
|
+
if (
|
|
412
|
+
!parsed ||
|
|
413
|
+
(parsed.kind !== "credential_required" && parsed.kind !== "external_tool_required")
|
|
414
|
+
) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (parsed.kind === "external_tool_required") {
|
|
419
|
+
const state = loadOnboardingState();
|
|
420
|
+
const descriptor = getProvider(parsed.provider);
|
|
421
|
+
if (state.providers[parsed.provider]?.status === "never_ask") {
|
|
422
|
+
return {
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: "text",
|
|
426
|
+
text:
|
|
427
|
+
`${buildSkippedTag({
|
|
428
|
+
provider: parsed.provider,
|
|
429
|
+
reason: "credential_not_provided",
|
|
430
|
+
remediation: `run ${parsed.installCmd} and ${parsed.loginCmd ?? "rdt login"} to enable ${descriptor.displayName} (silenced)`,
|
|
431
|
+
silenced: true,
|
|
432
|
+
})}\n\n` +
|
|
433
|
+
`${descriptor.displayName} data was not fetched because you previously asked not to be reminded about this provider.`,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const setupLabel =
|
|
439
|
+
parsed.reason === "not_installed"
|
|
440
|
+
? `Continue after installing ${descriptor.displayName}`
|
|
441
|
+
: `Continue after logging in to ${descriptor.displayName}`;
|
|
442
|
+
const skipLabel = `Skip ${descriptor.displayName} once`;
|
|
443
|
+
const neverLabel = `Always skip ${descriptor.displayName}`;
|
|
444
|
+
const installOrLogin =
|
|
445
|
+
parsed.reason === "not_installed"
|
|
446
|
+
? `Install command: ${parsed.installCmd}.`
|
|
447
|
+
: `Login command: ${parsed.loginCmd ?? "rdt login"}.`;
|
|
448
|
+
const questionBody =
|
|
449
|
+
`${descriptor.displayName} requires a local external tool before this data can be fetched. ` +
|
|
450
|
+
`${installOrLogin} How would you like to proceed?`;
|
|
451
|
+
|
|
452
|
+
sessionPromptedSet.add(parsed.provider);
|
|
453
|
+
const promptResult = await promptUser(
|
|
454
|
+
ctx,
|
|
455
|
+
{
|
|
456
|
+
question: questionBody,
|
|
457
|
+
questionType: "select",
|
|
458
|
+
options: [setupLabel, skipLabel, neverLabel],
|
|
459
|
+
},
|
|
460
|
+
options?.askUserHandler,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const answer = promptResult.cancelled ? skipLabel : (promptResult.answer ?? skipLabel);
|
|
464
|
+
if (answer.startsWith("Always")) {
|
|
465
|
+
saveOnboardingState(markProviderNeverAsk(state, parsed.provider));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (answer.startsWith("Continue")) {
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: "text",
|
|
473
|
+
text:
|
|
474
|
+
`${buildConnectedTag({ provider: parsed.provider })}\n\n` +
|
|
475
|
+
`${descriptor.displayName} setup was acknowledged. Re-run the original ${descriptor.displayName} request now; if setup is still unavailable, continue without ${descriptor.displayName} and disclose the source gap.`,
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const silenced = answer.startsWith("Always");
|
|
482
|
+
const remediation = silenced
|
|
483
|
+
? `run ${parsed.installCmd} and ${parsed.loginCmd ?? "rdt login"} to enable ${descriptor.displayName} (silenced)`
|
|
484
|
+
: `run ${parsed.installCmd} and ${parsed.loginCmd ?? "rdt login"} to enable ${descriptor.displayName}`;
|
|
485
|
+
return {
|
|
486
|
+
content: [
|
|
487
|
+
{
|
|
488
|
+
type: "text",
|
|
489
|
+
text: `${buildSkippedTag({
|
|
490
|
+
provider: parsed.provider,
|
|
491
|
+
reason: "credential_not_provided",
|
|
492
|
+
remediation,
|
|
493
|
+
silenced,
|
|
494
|
+
})}\n\n${descriptor.displayName} data was omitted per your choice.`,
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
};
|
|
498
|
+
}
|
|
401
499
|
|
|
402
500
|
const state = loadOnboardingState();
|
|
403
501
|
const action = resolveCredentialRequired({
|
package/src/pi/session.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import "../infra/node-version.js";
|
|
2
1
|
import {
|
|
3
2
|
type AuthStorage,
|
|
4
3
|
type CreateAgentSessionResult,
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
type SettingsManager,
|
|
11
10
|
} from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import { loadEnv } from "../config.js";
|
|
12
|
+
import { assertSupportedNodeVersion } from "../infra/node-version.js";
|
|
13
13
|
import type { AskUserHandler } from "../types/index.js";
|
|
14
14
|
import openCandleExtension from "./opencandle-extension.js";
|
|
15
15
|
|
|
@@ -28,6 +28,7 @@ export interface CreateOpenCandleSessionOptions {
|
|
|
28
28
|
export async function createOpenCandleSession(
|
|
29
29
|
options: CreateOpenCandleSessionOptions = {},
|
|
30
30
|
): Promise<CreateAgentSessionResult> {
|
|
31
|
+
assertSupportedNodeVersion();
|
|
31
32
|
loadEnv();
|
|
32
33
|
|
|
33
34
|
const cwd = options.cwd ?? process.cwd();
|
package/src/pi/tool-adapter.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { TSchema } from "@sinclair/typebox";
|
|
|
4
4
|
import { getDefaults } from "../memory/tool-defaults.js";
|
|
5
5
|
import { wrapWithDefaults } from "../runtime/tool-defaults-wrapper.js";
|
|
6
6
|
import { getAllTools } from "../tools/index.js";
|
|
7
|
+
import type { AskUserHandler } from "../types/index.js";
|
|
7
8
|
|
|
8
9
|
export function agentToolToPiTool<TParams extends TSchema, TDetails>(
|
|
9
10
|
tool: AgentTool<TParams, TDetails>,
|
|
@@ -14,14 +15,23 @@ export function agentToolToPiTool<TParams extends TSchema, TDetails>(
|
|
|
14
15
|
description: tool.description,
|
|
15
16
|
promptSnippet: `${tool.name}: ${tool.description}`,
|
|
16
17
|
parameters: tool.parameters,
|
|
17
|
-
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
18
|
-
|
|
18
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
19
|
+
const executeWithContext = tool.execute as unknown as (
|
|
20
|
+
id: string,
|
|
21
|
+
params: unknown,
|
|
22
|
+
signal: AbortSignal | undefined,
|
|
23
|
+
onUpdate: unknown,
|
|
24
|
+
ctx: unknown,
|
|
25
|
+
) => ReturnType<typeof tool.execute>;
|
|
26
|
+
return executeWithContext(toolCallId, params, signal, onUpdate, ctx);
|
|
19
27
|
},
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
|
|
23
|
-
export function getOpenCandleToolDefinitions(
|
|
24
|
-
|
|
31
|
+
export function getOpenCandleToolDefinitions(
|
|
32
|
+
options: { askUserHandler?: AskUserHandler } = {},
|
|
33
|
+
): ToolDefinition[] {
|
|
34
|
+
return getAllTools(options)
|
|
25
35
|
.map((tool) => ({ tool, defaults: safeGetDefaults(tool.name) }))
|
|
26
36
|
.filter(({ defaults }) => defaults.__enabled !== false)
|
|
27
37
|
.map(({ tool, defaults }) => {
|
|
@@ -251,7 +251,7 @@ const SAFETY_RULES = `## Guidelines
|
|
|
251
251
|
- For crypto position-sizing prompts, start with a concrete allocation range and dollar amount, then show drawdown impact on the user's portfolio, a sleep test, dollar-cost averaging, rebalancing rules, position caps, tax tracking, reputable custody/exchange considerations, and emergency-fund/high-interest-debt prerequisites. Cite the crypto tool-output date or history period and label sparse or unavailable history instead of implying unsupported precision.
|
|
252
252
|
- For rate-cut market-pricing questions, use get_economic_data for the current Fed funds backdrop and search_web for CME FedWatch / Federal Funds futures probabilities before naming what the market is pricing. Distinguish historical Fed rates from futures-implied expectations.
|
|
253
253
|
- For backtest_strategy results, report strategy return, buy-and-hold return, outperformance, trade count, win rate, and max drawdown. Include risk-adjusted metrics such as Sharpe or Sortino when available; otherwise say they were unavailable. Explain why the strategy worked or failed in the tested regime and discuss trading costs/slippage when the user asks whether the edge is practical. Do not reduce a backtest answer to return-only.
|
|
254
|
-
- For sentiment-only prompts, final answer must include the direction and strength of the sentiment signal, the score scale when available, missing sources, why those missing sources matter for the user's question, the source-coverage risk, low sample counts, and how those gaps downgrade confidence. For ticker-specific sentiment prompts, call get_stock_quote before the final answer and state whether sentiment diverges from price action.
|
|
254
|
+
- For sentiment-only prompts, final answer must include the direction and strength of the sentiment signal, the score scale when available, the key positive and negative drivers behind the signal, representative source evidence, missing sources, why those missing sources matter for the user's question, the source-coverage risk, low sample counts, and how those gaps downgrade confidence. For ticker-specific sentiment prompts, call get_stock_quote before the final answer and state whether sentiment diverges from price action; if quote data is unavailable, say price-action divergence could not be evaluated.
|
|
255
255
|
- Commit to specifics when asked for entries, targets, stops, allocations, or position sizes. Refusal is not an acceptable output shape.
|
|
256
256
|
- Each committal response carries FOUR things: the specific number or range, a reasoning chain naming the data points you used, a confidence band, and an invalidation level (what would break the thesis).
|
|
257
257
|
- Conceptual education prompts are not committal responses. Do not append "Analyst View", "Commitment", "Reasoning Chain", "Confidence Band", or "Invalidation Level" sections when the user asked for an explanation, definition, or learning framework rather than a trade, allocation, or recommendation.
|