opencandle 0.2.0 → 0.4.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 +110 -87
- package/assets/logo.svg +187 -0
- package/dist/analysts/orchestrator.js +1 -2
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +38 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +34 -5
- package/dist/config.js +29 -8
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +10 -0
- package/dist/infra/browser.js +1 -0
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/cache.d.ts +4 -0
- package/dist/infra/cache.js +4 -0
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +1 -0
- package/dist/infra/native-dependencies.js +10 -0
- package/dist/infra/native-dependencies.js.map +1 -0
- package/dist/infra/node-version.d.ts +2 -0
- package/dist/infra/node-version.js +23 -0
- package/dist/infra/node-version.js.map +1 -0
- package/dist/infra/rate-limiter.js +6 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/sqlite.js +42 -4
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +6 -0
- package/dist/memory/storage.js +3 -3
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.d.ts +8 -0
- package/dist/memory/tool-defaults.js +59 -0
- package/dist/memory/tool-defaults.js.map +1 -0
- package/dist/onboarding/connect.d.ts +35 -0
- package/dist/onboarding/connect.js +118 -0
- package/dist/onboarding/connect.js.map +1 -0
- package/dist/onboarding/credential-interceptor.d.ts +44 -0
- package/dist/onboarding/credential-interceptor.js +72 -0
- package/dist/onboarding/credential-interceptor.js.map +1 -0
- package/dist/onboarding/degradation-accumulator.d.ts +21 -0
- package/dist/onboarding/degradation-accumulator.js +55 -0
- package/dist/onboarding/degradation-accumulator.js.map +1 -0
- package/dist/onboarding/prompt-user.d.ts +23 -0
- package/dist/onboarding/prompt-user.js +61 -0
- package/dist/onboarding/prompt-user.js.map +1 -0
- package/dist/onboarding/providers.d.ts +116 -0
- package/dist/onboarding/providers.js +239 -0
- package/dist/onboarding/providers.js.map +1 -0
- package/dist/onboarding/state.d.ts +31 -2
- package/dist/onboarding/state.js +141 -13
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +34 -0
- package/dist/onboarding/tool-helpers.js +80 -0
- package/dist/onboarding/tool-helpers.js.map +1 -0
- package/dist/onboarding/tool-tags.d.ts +37 -0
- package/dist/onboarding/tool-tags.js +149 -0
- package/dist/onboarding/tool-tags.js.map +1 -0
- package/dist/onboarding/validation.d.ts +19 -0
- package/dist/onboarding/validation.js +117 -0
- package/dist/onboarding/validation.js.map +1 -0
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +488 -13
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-storage.d.ts +2 -0
- package/dist/pi/session-storage.js +5 -0
- package/dist/pi/session-storage.js.map +1 -0
- package/dist/pi/session.d.ts +4 -1
- package/dist/pi/session.js +25 -3
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.d.ts +1 -2
- package/dist/pi/setup.js +67 -120
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +2 -2
- package/dist/pi/tool-adapter.js +14 -1
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +22 -0
- package/dist/prompts/context-builder.js +47 -11
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.d.ts +6 -0
- package/dist/prompts/disclaimer.js +9 -0
- package/dist/prompts/disclaimer.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +39 -5
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.js +20 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/exa-search.d.ts +39 -0
- package/dist/providers/exa-search.js +276 -0
- package/dist/providers/exa-search.js.map +1 -0
- package/dist/providers/finnhub.d.ts +17 -0
- package/dist/providers/finnhub.js +94 -0
- package/dist/providers/finnhub.js.map +1 -0
- package/dist/providers/fred.js +13 -1
- package/dist/providers/fred.js.map +1 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider-credential-error.d.ts +8 -0
- package/dist/providers/provider-credential-error.js +22 -0
- package/dist/providers/provider-credential-error.js.map +1 -0
- package/dist/providers/reddit.d.ts +8 -0
- package/dist/providers/reddit.js +36 -9
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/twitter.js +2 -8
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/web-search.d.ts +17 -0
- package/dist/providers/web-search.js +224 -0
- package/dist/providers/web-search.js.map +1 -0
- package/dist/providers/wrap-provider.d.ts +7 -0
- package/dist/providers/wrap-provider.js +15 -0
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.js +70 -33
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +22 -0
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.js +1 -1
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +3 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/router-llm-client.d.ts +11 -0
- package/dist/routing/router-llm-client.js +42 -0
- package/dist/routing/router-llm-client.js.map +1 -0
- package/dist/routing/router-prompt.d.ts +2 -0
- package/dist/routing/router-prompt.js +138 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +62 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +10 -0
- package/dist/routing/router.js +194 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +63 -4
- package/dist/runtime/session-coordinator.js +155 -4
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
- package/dist/runtime/tool-defaults-wrapper.js +25 -0
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
- package/dist/sentiment/adapters/finnhub.d.ts +7 -0
- package/dist/sentiment/adapters/finnhub.js +39 -0
- package/dist/sentiment/adapters/finnhub.js.map +1 -0
- package/dist/sentiment/adapters/reddit.d.ts +11 -0
- package/dist/sentiment/adapters/reddit.js +54 -0
- package/dist/sentiment/adapters/reddit.js.map +1 -0
- package/dist/sentiment/adapters/twitter.d.ts +9 -0
- package/dist/sentiment/adapters/twitter.js +32 -0
- package/dist/sentiment/adapters/twitter.js.map +1 -0
- package/dist/sentiment/adapters/web.d.ts +9 -0
- package/dist/sentiment/adapters/web.js +40 -0
- package/dist/sentiment/adapters/web.js.map +1 -0
- package/dist/sentiment/index.d.ts +16 -0
- package/dist/sentiment/index.js +44 -0
- package/dist/sentiment/index.js.map +1 -0
- package/dist/sentiment/keywords.d.ts +2 -0
- package/dist/sentiment/keywords.js +9 -0
- package/dist/sentiment/keywords.js.map +1 -0
- package/dist/sentiment/pipeline.d.ts +9 -0
- package/dist/sentiment/pipeline.js +57 -0
- package/dist/sentiment/pipeline.js.map +1 -0
- package/dist/sentiment/scorer.d.ts +9 -0
- package/dist/sentiment/scorer.js +64 -0
- package/dist/sentiment/scorer.js.map +1 -0
- package/dist/sentiment/store.d.ts +24 -0
- package/dist/sentiment/store.js +182 -0
- package/dist/sentiment/store.js.map +1 -0
- package/dist/sentiment/trends.d.ts +13 -0
- package/dist/sentiment/trends.js +73 -0
- package/dist/sentiment/trends.js.map +1 -0
- package/dist/sentiment/types.d.ts +66 -0
- package/dist/sentiment/types.js +54 -0
- package/dist/sentiment/types.js.map +1 -0
- package/dist/system-prompt.js +29 -13
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +4 -2
- package/dist/tools/fundamentals/company-overview.js +27 -27
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/comps.js +45 -45
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +82 -82
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +4 -2
- package/dist/tools/fundamentals/earnings.js +25 -25
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +4 -2
- package/dist/tools/fundamentals/financials.js +23 -23
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
- package/dist/tools/index.d.ts +28 -1
- package/dist/tools/index.js +35 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/ask-user.js +28 -64
- package/dist/tools/interaction/ask-user.js.map +1 -1
- package/dist/tools/interaction/twitter-login.d.ts +1 -1
- package/dist/tools/macro/fear-greed.d.ts +1 -1
- package/dist/tools/macro/fred-data.d.ts +4 -2
- package/dist/tools/macro/fred-data.js +26 -26
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +4 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +4 -2
- package/dist/tools/sentiment/reddit-sentiment.js +107 -22
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
- package/dist/tools/sentiment/sentiment-summary.js +230 -0
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
- package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
- package/dist/tools/sentiment/sentiment-trend.js +39 -0
- package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +17 -0
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +11 -0
- package/dist/tools/sentiment/web-search.js +115 -0
- package/dist/tools/sentiment/web-search.js.map +1 -0
- package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
- package/dist/tools/sentiment/web-sentiment.js +66 -0
- package/dist/tools/sentiment/web-sentiment.js.map +1 -0
- package/dist/tools/technical/backtest.d.ts +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +7 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/sentiment.d.ts +21 -0
- package/dist/workflows/options-screener.js +7 -2
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +3 -3
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/invoke-tool.ts +89 -0
- package/gui/server/live-chat-event-adapter.ts +181 -0
- package/gui/server/model-setup.ts +100 -0
- package/gui/server/package.json +5 -0
- package/gui/server/projector.ts +212 -0
- package/gui/server/server.ts +592 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/tool-metadata.ts +88 -0
- package/gui/server/websocket.ts +128 -0
- package/gui/server/writer-lock.ts +118 -0
- package/gui/shared/chat-events.ts +118 -0
- package/gui/shared/event-reducer.ts +186 -0
- package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
- package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
- package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +62 -20
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +205 -0
- package/src/config.ts +161 -0
- package/src/index.ts +5 -0
- package/src/infra/browser.ts +111 -0
- package/src/infra/cache.ts +103 -0
- package/src/infra/http-client.ts +68 -0
- package/src/infra/index.ts +18 -0
- package/src/infra/native-dependencies.ts +12 -0
- package/src/infra/node-version.ts +24 -0
- package/src/infra/open-url.ts +28 -0
- package/src/infra/opencandle-paths.ts +64 -0
- package/src/infra/rate-limiter.ts +64 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +159 -0
- package/src/memory/preference-extractor.ts +106 -0
- package/src/memory/retrieval.ts +70 -0
- package/src/memory/sqlite.ts +172 -0
- package/src/memory/storage.ts +204 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +67 -0
- package/src/onboarding/connect.ts +184 -0
- package/src/onboarding/credential-interceptor.ts +134 -0
- package/src/onboarding/degradation-accumulator.ts +79 -0
- package/src/onboarding/prompt-user.ts +85 -0
- package/src/onboarding/providers.ts +315 -0
- package/src/onboarding/state.ts +218 -0
- package/src/onboarding/tool-helpers.ts +111 -0
- package/src/onboarding/tool-tags.ts +201 -0
- package/src/onboarding/validation.ts +158 -0
- package/src/pi/opencandle-extension.ts +724 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +371 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +204 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +279 -0
- package/src/providers/alpha-vantage.ts +292 -0
- package/src/providers/coingecko.ts +96 -0
- package/src/providers/exa-search.ts +373 -0
- package/src/providers/fear-greed.ts +45 -0
- package/src/providers/finnhub.ts +124 -0
- package/src/providers/fred.ts +83 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider-credential-error.ts +23 -0
- package/src/providers/reddit.ts +151 -0
- package/src/providers/sec-edgar.ts +96 -0
- package/src/providers/twitter.ts +173 -0
- package/src/providers/web-search.ts +293 -0
- package/src/providers/with-fallback.ts +41 -0
- package/src/providers/wrap-provider.ts +64 -0
- package/src/providers/yahoo-finance.ts +367 -0
- package/src/routing/classify-intent.ts +194 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +140 -0
- package/src/routing/index.ts +26 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +159 -0
- package/src/routing/router-types.ts +66 -0
- package/src/routing/router.ts +213 -0
- package/src/routing/slot-resolver.ts +152 -0
- package/src/routing/types.ts +63 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/index.ts +55 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-ids.ts +15 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +406 -0
- package/src/runtime/tool-defaults-wrapper.ts +35 -0
- package/src/runtime/validation.ts +214 -0
- package/src/runtime/workflow-events.ts +75 -0
- package/src/runtime/workflow-runner.ts +188 -0
- package/src/runtime/workflow-types.ts +102 -0
- package/src/sentiment/adapters/finnhub.ts +44 -0
- package/src/sentiment/adapters/reddit.ts +65 -0
- package/src/sentiment/adapters/twitter.ts +36 -0
- package/src/sentiment/adapters/web.ts +44 -0
- package/src/sentiment/index.ts +58 -0
- package/src/sentiment/keywords.ts +9 -0
- package/src/sentiment/pipeline.ts +68 -0
- package/src/sentiment/scorer.ts +78 -0
- package/src/sentiment/store.ts +260 -0
- package/src/sentiment/trends.ts +90 -0
- package/src/sentiment/types.ts +108 -0
- package/src/system-prompt.ts +115 -0
- package/src/tool-kit.ts +68 -0
- package/src/tools/AGENTS.md +36 -0
- package/src/tools/fundamentals/company-overview.ts +54 -0
- package/src/tools/fundamentals/comps.ts +156 -0
- package/src/tools/fundamentals/dcf.ts +267 -0
- package/src/tools/fundamentals/earnings.ts +47 -0
- package/src/tools/fundamentals/financials.ts +54 -0
- package/src/tools/fundamentals/sec-filings.ts +61 -0
- package/src/tools/index.ts +88 -0
- package/src/tools/interaction/ask-user.ts +81 -0
- package/src/tools/interaction/twitter-login.ts +93 -0
- package/src/tools/macro/fear-greed.ts +41 -0
- package/src/tools/macro/fred-data.ts +54 -0
- package/src/tools/market/crypto-history.ts +51 -0
- package/src/tools/market/crypto-price.ts +53 -0
- package/src/tools/market/search-ticker.ts +53 -0
- package/src/tools/market/stock-history.ts +79 -0
- package/src/tools/market/stock-quote.ts +64 -0
- package/src/tools/options/greeks.ts +82 -0
- package/src/tools/options/option-chain.ts +91 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/predictions.ts +253 -0
- package/src/tools/portfolio/risk-analysis.ts +134 -0
- package/src/tools/portfolio/tracker.ts +147 -0
- package/src/tools/portfolio/watchlist.ts +153 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +256 -0
- package/src/tools/sentiment/sentiment-trend.ts +58 -0
- package/src/tools/sentiment/twitter-sentiment.ts +96 -0
- package/src/tools/sentiment/web-search.ts +150 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +246 -0
- package/src/tools/technical/indicators.ts +258 -0
- package/src/types/fundamentals.ts +46 -0
- package/src/types/index.ts +20 -0
- package/src/types/macro.ts +27 -0
- package/src/types/market.ts +43 -0
- package/src/types/options.ts +35 -0
- package/src/types/portfolio.ts +41 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +39 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +49 -0
- package/src/workflows/portfolio-builder.ts +52 -0
- package/src/workflows/types.ts +4 -0
- package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
- package/dist/tools/sentiment/news-sentiment.js +0 -55
- package/dist/tools/sentiment/news-sentiment.js.map +0 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
|
|
3
|
+
/** All workflow event types. */
|
|
4
|
+
export type WorkflowEventType =
|
|
5
|
+
| "workflow_started"
|
|
6
|
+
| "slot_resolved"
|
|
7
|
+
| "clarification_asked"
|
|
8
|
+
| "clarification_answered"
|
|
9
|
+
| "step_started"
|
|
10
|
+
| "step_completed"
|
|
11
|
+
| "step_failed"
|
|
12
|
+
| "step_skipped"
|
|
13
|
+
| "tool_called"
|
|
14
|
+
| "tool_failed"
|
|
15
|
+
| "validation_passed"
|
|
16
|
+
| "validation_failed"
|
|
17
|
+
| "workflow_completed"
|
|
18
|
+
| "workflow_cancelled";
|
|
19
|
+
|
|
20
|
+
/** A persisted workflow event row. */
|
|
21
|
+
export interface WorkflowEvent {
|
|
22
|
+
id: number;
|
|
23
|
+
runId: string;
|
|
24
|
+
stepIndex: number;
|
|
25
|
+
eventType: WorkflowEventType;
|
|
26
|
+
payloadJson: string | null;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Append-only workflow event logger backed by SQLite. */
|
|
31
|
+
export class WorkflowEventLogger {
|
|
32
|
+
constructor(private readonly db: Database.Database) {}
|
|
33
|
+
|
|
34
|
+
/** Append a workflow event. */
|
|
35
|
+
log(
|
|
36
|
+
runId: string,
|
|
37
|
+
stepIndex: number,
|
|
38
|
+
eventType: WorkflowEventType,
|
|
39
|
+
payload?: Record<string, unknown>,
|
|
40
|
+
): void {
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const payloadJson = payload ? JSON.stringify(payload) : null;
|
|
43
|
+
this.db
|
|
44
|
+
.prepare(
|
|
45
|
+
`INSERT INTO workflow_events (run_id, step_index, event_type, payload_json, timestamp)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
47
|
+
)
|
|
48
|
+
.run(runId, stepIndex, eventType, payloadJson, now);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Query all events for a given run ID, ordered by timestamp. */
|
|
52
|
+
getEventsByRunId(runId: string): WorkflowEvent[] {
|
|
53
|
+
const rows = this.db
|
|
54
|
+
.prepare(
|
|
55
|
+
"SELECT id, run_id, step_index, event_type, payload_json, timestamp FROM workflow_events WHERE run_id = ? ORDER BY id",
|
|
56
|
+
)
|
|
57
|
+
.all(runId) as Array<{
|
|
58
|
+
id: number;
|
|
59
|
+
run_id: string;
|
|
60
|
+
step_index: number;
|
|
61
|
+
event_type: string;
|
|
62
|
+
payload_json: string | null;
|
|
63
|
+
timestamp: string;
|
|
64
|
+
}>;
|
|
65
|
+
|
|
66
|
+
return rows.map((r) => ({
|
|
67
|
+
id: r.id,
|
|
68
|
+
runId: r.run_id,
|
|
69
|
+
stepIndex: r.step_index,
|
|
70
|
+
eventType: r.event_type as WorkflowEventType,
|
|
71
|
+
payloadJson: r.payload_json,
|
|
72
|
+
timestamp: r.timestamp,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { WorkflowRun, StepOutput, WorkflowStep } from "./workflow-types.js";
|
|
2
|
+
import {
|
|
3
|
+
createWorkflowRun,
|
|
4
|
+
transitionStepStatus,
|
|
5
|
+
} from "./workflow-types.js";
|
|
6
|
+
import type { WorkflowEventLogger } from "./workflow-events.js";
|
|
7
|
+
import type { ProviderTracker } from "./provider-tracker.js";
|
|
8
|
+
import type { EvidenceRecord } from "./evidence.js";
|
|
9
|
+
|
|
10
|
+
/** Function that executes a single workflow step. */
|
|
11
|
+
export type StepExecutor = (
|
|
12
|
+
step: WorkflowStep,
|
|
13
|
+
stepIndex: number,
|
|
14
|
+
priorEvidence: EvidenceRecord[],
|
|
15
|
+
context: StepExecutionContext,
|
|
16
|
+
) => Promise<StepOutput>;
|
|
17
|
+
|
|
18
|
+
/** Context passed to step executors. */
|
|
19
|
+
export interface StepExecutionContext {
|
|
20
|
+
runId: string;
|
|
21
|
+
providerTracker: ProviderTracker;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Options for creating a WorkflowRunner. */
|
|
25
|
+
export interface WorkflowRunnerOptions {
|
|
26
|
+
eventLogger?: WorkflowEventLogger;
|
|
27
|
+
providerTracker?: ProviderTracker;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let runCounter = 0;
|
|
31
|
+
|
|
32
|
+
function generateRunId(): string {
|
|
33
|
+
runCounter += 1;
|
|
34
|
+
return `run_${Date.now()}_${runCounter}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Typed workflow execution engine with run IDs, step definitions,
|
|
39
|
+
* state transitions, cancellation, and event logging.
|
|
40
|
+
*/
|
|
41
|
+
export class WorkflowRunner {
|
|
42
|
+
private readonly eventLogger?: WorkflowEventLogger;
|
|
43
|
+
private readonly providerTracker?: ProviderTracker;
|
|
44
|
+
private activeRun: WorkflowRun | null = null;
|
|
45
|
+
|
|
46
|
+
constructor(options: WorkflowRunnerOptions = {}) {
|
|
47
|
+
this.eventLogger = options.eventLogger;
|
|
48
|
+
this.providerTracker = options.providerTracker;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Get the currently active run, if any. */
|
|
52
|
+
getActiveRun(): WorkflowRun | null {
|
|
53
|
+
return this.activeRun;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start a new workflow run. If a run is already active, it is cancelled first.
|
|
58
|
+
*/
|
|
59
|
+
async start(
|
|
60
|
+
workflowType: string,
|
|
61
|
+
stepDefinitions: Omit<WorkflowStep, "status">[],
|
|
62
|
+
executor: StepExecutor,
|
|
63
|
+
): Promise<WorkflowRun> {
|
|
64
|
+
// Cancel any active run
|
|
65
|
+
if (this.activeRun && this.activeRun.status === "running") {
|
|
66
|
+
this.cancel();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const runId = generateRunId();
|
|
70
|
+
const run = createWorkflowRun(runId, workflowType, stepDefinitions);
|
|
71
|
+
this.activeRun = run;
|
|
72
|
+
run.status = "running";
|
|
73
|
+
|
|
74
|
+
this.providerTracker?.resetAll();
|
|
75
|
+
|
|
76
|
+
this.logEvent(runId, 0, "workflow_started", {
|
|
77
|
+
workflowType,
|
|
78
|
+
stepCount: stepDefinitions.length,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Execute steps
|
|
82
|
+
await this.executeSteps(run, executor);
|
|
83
|
+
|
|
84
|
+
return run;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Cancel the active run. */
|
|
88
|
+
cancel(): void {
|
|
89
|
+
const run = this.activeRun;
|
|
90
|
+
if (!run || run.status !== "running") return;
|
|
91
|
+
|
|
92
|
+
for (let i = run.currentStepIndex; i < run.steps.length; i++) {
|
|
93
|
+
const step = run.steps[i];
|
|
94
|
+
if (step.status === "pending" || step.status === "running") {
|
|
95
|
+
step.status = transitionStepStatus(step.status, "skipped");
|
|
96
|
+
this.logEvent(run.runId, i, "step_skipped", {
|
|
97
|
+
stepType: step.stepType,
|
|
98
|
+
reason: "cancelled",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
run.status = "cancelled";
|
|
104
|
+
this.logEvent(run.runId, run.currentStepIndex, "workflow_cancelled", {
|
|
105
|
+
cancelledAtStep: run.currentStepIndex,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async executeSteps(
|
|
110
|
+
run: WorkflowRun,
|
|
111
|
+
executor: StepExecutor,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
for (let i = 0; i < run.steps.length; i++) {
|
|
114
|
+
// Check if run was cancelled externally
|
|
115
|
+
if (run.status !== "running") return;
|
|
116
|
+
|
|
117
|
+
const step = run.steps[i];
|
|
118
|
+
run.currentStepIndex = i;
|
|
119
|
+
|
|
120
|
+
// Collect all prior evidence
|
|
121
|
+
const priorEvidence: EvidenceRecord[] = [];
|
|
122
|
+
for (const [, output] of run.stepOutputs) {
|
|
123
|
+
priorEvidence.push(...output.evidence);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Transition to running
|
|
127
|
+
step.status = transitionStepStatus(step.status, "running");
|
|
128
|
+
this.logEvent(run.runId, i, "step_started", { stepType: step.stepType });
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const context: StepExecutionContext = {
|
|
132
|
+
runId: run.runId,
|
|
133
|
+
providerTracker: this.providerTracker!,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const output = await executor(step, i, priorEvidence, context);
|
|
137
|
+
|
|
138
|
+
// If run was cancelled during execution, stop without further transitions
|
|
139
|
+
if (run.status !== "running") return;
|
|
140
|
+
|
|
141
|
+
step.status = transitionStepStatus(step.status, "completed");
|
|
142
|
+
run.stepOutputs.set(i, output);
|
|
143
|
+
|
|
144
|
+
this.logEvent(run.runId, i, "step_completed", {
|
|
145
|
+
stepType: step.stepType,
|
|
146
|
+
evidenceCount: output.evidence.length,
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// If run was cancelled during execution, stop without further transitions
|
|
150
|
+
if (run.status !== "running") return;
|
|
151
|
+
|
|
152
|
+
const message = error instanceof Error ? error.message : "unknown_error";
|
|
153
|
+
|
|
154
|
+
if (step.skippable) {
|
|
155
|
+
step.status = transitionStepStatus(step.status, "skipped");
|
|
156
|
+
this.logEvent(run.runId, i, "step_skipped", {
|
|
157
|
+
stepType: step.stepType,
|
|
158
|
+
reason: message,
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
step.status = transitionStepStatus(step.status, "failed");
|
|
162
|
+
this.logEvent(run.runId, i, "step_failed", {
|
|
163
|
+
stepType: step.stepType,
|
|
164
|
+
error: message,
|
|
165
|
+
});
|
|
166
|
+
run.status = "failed";
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (run.status === "running") {
|
|
173
|
+
run.status = "completed";
|
|
174
|
+
this.logEvent(run.runId, run.steps.length - 1, "workflow_completed", {
|
|
175
|
+
workflowType: run.workflowType,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private logEvent(
|
|
181
|
+
runId: string,
|
|
182
|
+
stepIndex: number,
|
|
183
|
+
eventType: string,
|
|
184
|
+
payload: Record<string, unknown>,
|
|
185
|
+
): void {
|
|
186
|
+
this.eventLogger?.log(runId, stepIndex, eventType as any, payload);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { EvidenceRecord } from "./evidence.js";
|
|
2
|
+
|
|
3
|
+
/** Status of a single workflow step. */
|
|
4
|
+
export type StepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
|
|
5
|
+
|
|
6
|
+
/** Overall status of a workflow run. */
|
|
7
|
+
export type RunStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
8
|
+
|
|
9
|
+
/** Valid step status transitions. */
|
|
10
|
+
const VALID_STEP_TRANSITIONS: Record<StepStatus, StepStatus[]> = {
|
|
11
|
+
pending: ["running", "skipped"],
|
|
12
|
+
running: ["completed", "failed", "skipped"],
|
|
13
|
+
completed: [],
|
|
14
|
+
failed: [],
|
|
15
|
+
skipped: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Check whether a step status transition is valid. */
|
|
19
|
+
export function isValidStepTransition(from: StepStatus, to: StepStatus): boolean {
|
|
20
|
+
return VALID_STEP_TRANSITIONS[from].includes(to);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Transition a step status, throwing on invalid transitions. */
|
|
24
|
+
export function transitionStepStatus(from: StepStatus, to: StepStatus): StepStatus {
|
|
25
|
+
if (!isValidStepTransition(from, to)) {
|
|
26
|
+
throw new Error(`Invalid step transition: ${from} → ${to}`);
|
|
27
|
+
}
|
|
28
|
+
return to;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Definition of a single workflow step. */
|
|
32
|
+
export interface WorkflowStep {
|
|
33
|
+
stepType: string;
|
|
34
|
+
description: string;
|
|
35
|
+
requiredInputs: string[];
|
|
36
|
+
expectedOutputs: string[];
|
|
37
|
+
skippable: boolean;
|
|
38
|
+
status: StepStatus;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Output produced by a completed workflow step. */
|
|
42
|
+
export interface StepOutput {
|
|
43
|
+
stepIndex: number;
|
|
44
|
+
stepType: string;
|
|
45
|
+
evidence: EvidenceRecord[];
|
|
46
|
+
rawText?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Analyst signal direction. */
|
|
50
|
+
export type AnalystSignal = "BUY" | "HOLD" | "SELL";
|
|
51
|
+
|
|
52
|
+
/** Structured output from a single analyst role. */
|
|
53
|
+
export interface AnalystOutput {
|
|
54
|
+
role: string;
|
|
55
|
+
signal: AnalystSignal;
|
|
56
|
+
conviction: number;
|
|
57
|
+
thesis: string;
|
|
58
|
+
evidence: EvidenceRecord[];
|
|
59
|
+
rawText?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Debate side in bull/bear adversarial debate. */
|
|
63
|
+
export type DebateSide = "bull" | "bear";
|
|
64
|
+
|
|
65
|
+
/** Structured output from a debate step (eval/test only — not used in live path). */
|
|
66
|
+
export interface DebateOutput {
|
|
67
|
+
side: DebateSide;
|
|
68
|
+
thesis: string;
|
|
69
|
+
keyRisk: string;
|
|
70
|
+
concessions: string[];
|
|
71
|
+
remainingConviction: number;
|
|
72
|
+
evidence: EvidenceRecord[];
|
|
73
|
+
rawText: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A complete workflow run definition and state. */
|
|
77
|
+
export interface WorkflowRun {
|
|
78
|
+
runId: string;
|
|
79
|
+
workflowType: string;
|
|
80
|
+
steps: WorkflowStep[];
|
|
81
|
+
currentStepIndex: number;
|
|
82
|
+
status: RunStatus;
|
|
83
|
+
stepOutputs: Map<number, StepOutput>;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Create a new workflow run with all steps in pending state. */
|
|
88
|
+
export function createWorkflowRun(
|
|
89
|
+
runId: string,
|
|
90
|
+
workflowType: string,
|
|
91
|
+
stepDefinitions: Omit<WorkflowStep, "status">[],
|
|
92
|
+
): WorkflowRun {
|
|
93
|
+
return {
|
|
94
|
+
runId,
|
|
95
|
+
workflowType,
|
|
96
|
+
steps: stepDefinitions.map((def) => ({ ...def, status: "pending" as const })),
|
|
97
|
+
currentStepIndex: 0,
|
|
98
|
+
status: "pending",
|
|
99
|
+
stepOutputs: new Map(),
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { SentinelRecord } from "../types.js";
|
|
3
|
+
import type { FinnhubArticle } from "../../providers/finnhub.js";
|
|
4
|
+
import { extractEntities } from "../../routing/entity-extractor.js";
|
|
5
|
+
|
|
6
|
+
const MAX_TICKERS = 3;
|
|
7
|
+
|
|
8
|
+
export class FinnhubAdapter {
|
|
9
|
+
readonly source = "finnhub" as const;
|
|
10
|
+
|
|
11
|
+
mapToRecords(articles: FinnhubArticle[], query: string): SentinelRecord[] {
|
|
12
|
+
const fetchedAt = new Date().toISOString();
|
|
13
|
+
return articles.map((article) => ({
|
|
14
|
+
id: randomUUID(),
|
|
15
|
+
source: this.source,
|
|
16
|
+
sourceId: String(article.id),
|
|
17
|
+
query,
|
|
18
|
+
title: article.headline,
|
|
19
|
+
text: article.summary,
|
|
20
|
+
author: article.source,
|
|
21
|
+
url: article.url,
|
|
22
|
+
publishedAt: new Date(article.datetime * 1000).toISOString(),
|
|
23
|
+
fetchedAt,
|
|
24
|
+
engagement: {
|
|
25
|
+
score: 0,
|
|
26
|
+
replies: null,
|
|
27
|
+
shares: null,
|
|
28
|
+
views: null,
|
|
29
|
+
},
|
|
30
|
+
sentiment: {
|
|
31
|
+
score: 0,
|
|
32
|
+
confidence: 0,
|
|
33
|
+
method: "keyword" as const,
|
|
34
|
+
tickers: article.related ? article.related.split(",").map((t) => t.trim()).filter(Boolean) : [],
|
|
35
|
+
},
|
|
36
|
+
metadata: { category: article.category },
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function extractTickersFromQuery(query: string): string[] {
|
|
42
|
+
const entities = extractEntities(query);
|
|
43
|
+
return entities.symbols.slice(0, MAX_TICKERS);
|
|
44
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { SentinelRecord, SentimentAdapter } from "../types.js";
|
|
3
|
+
import type { RedditSentimentResult } from "../../types/sentiment.js";
|
|
4
|
+
import type { RedditComment } from "../../providers/reddit.js";
|
|
5
|
+
|
|
6
|
+
export class RedditAdapter implements SentimentAdapter {
|
|
7
|
+
readonly source = "reddit" as const;
|
|
8
|
+
|
|
9
|
+
mapPostsToRecords(result: RedditSentimentResult, query: string): SentinelRecord[] {
|
|
10
|
+
const fetchedAt = result.fetchedAt;
|
|
11
|
+
return result.posts.map((post) => ({
|
|
12
|
+
id: randomUUID(),
|
|
13
|
+
source: this.source,
|
|
14
|
+
sourceId: post.id,
|
|
15
|
+
query,
|
|
16
|
+
title: post.title,
|
|
17
|
+
text: post.selftext ? `${post.title}\n${post.selftext}` : post.title,
|
|
18
|
+
author: post.author,
|
|
19
|
+
url: post.url,
|
|
20
|
+
publishedAt: post.created,
|
|
21
|
+
fetchedAt,
|
|
22
|
+
engagement: {
|
|
23
|
+
score: post.score,
|
|
24
|
+
replies: post.comments,
|
|
25
|
+
shares: null,
|
|
26
|
+
views: null,
|
|
27
|
+
},
|
|
28
|
+
sentiment: { score: 0, confidence: 0, method: "keyword" as const, tickers: [] },
|
|
29
|
+
metadata: { subreddit: result.subreddit },
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
mapCommentsToRecords(
|
|
34
|
+
comments: RedditComment[],
|
|
35
|
+
parentId: string,
|
|
36
|
+
subreddit: string,
|
|
37
|
+
query: string,
|
|
38
|
+
): SentinelRecord[] {
|
|
39
|
+
const fetchedAt = new Date().toISOString();
|
|
40
|
+
return comments.map((comment) => ({
|
|
41
|
+
id: randomUUID(),
|
|
42
|
+
source: this.source,
|
|
43
|
+
sourceId: comment.id,
|
|
44
|
+
query,
|
|
45
|
+
title: null,
|
|
46
|
+
text: comment.body,
|
|
47
|
+
author: comment.author,
|
|
48
|
+
url: comment.permalink,
|
|
49
|
+
publishedAt: null,
|
|
50
|
+
fetchedAt,
|
|
51
|
+
engagement: {
|
|
52
|
+
score: comment.score,
|
|
53
|
+
replies: null,
|
|
54
|
+
shares: null,
|
|
55
|
+
views: null,
|
|
56
|
+
},
|
|
57
|
+
sentiment: { score: 0, confidence: 0, method: "keyword" as const, tickers: [] },
|
|
58
|
+
metadata: { isComment: true, parentId, subreddit },
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async fetch(_query: string, _options?: { hours?: number }): Promise<SentinelRecord[]> {
|
|
63
|
+
throw new Error("Use pipeline.run() instead of adapter.fetch() directly");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { SentinelRecord, SentimentAdapter } from "../types.js";
|
|
3
|
+
import type { TwitterSentimentResult } from "../../types/sentiment.js";
|
|
4
|
+
|
|
5
|
+
export class TwitterAdapter implements SentimentAdapter {
|
|
6
|
+
readonly source = "twitter" as const;
|
|
7
|
+
|
|
8
|
+
mapToRecords(result: TwitterSentimentResult, query: string): SentinelRecord[] {
|
|
9
|
+
const fetchedAt = result.fetchedAt;
|
|
10
|
+
return result.tweets.map((tweet) => ({
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
source: this.source,
|
|
13
|
+
sourceId: tweet.id,
|
|
14
|
+
query,
|
|
15
|
+
title: null,
|
|
16
|
+
text: tweet.text,
|
|
17
|
+
author: tweet.author,
|
|
18
|
+
url: tweet.url,
|
|
19
|
+
publishedAt: tweet.created,
|
|
20
|
+
fetchedAt,
|
|
21
|
+
engagement: {
|
|
22
|
+
score: tweet.likes,
|
|
23
|
+
replies: tweet.replies,
|
|
24
|
+
shares: tweet.retweets,
|
|
25
|
+
views: tweet.views,
|
|
26
|
+
},
|
|
27
|
+
sentiment: { score: 0, confidence: 0, method: "keyword" as const, tickers: [] },
|
|
28
|
+
metadata: {},
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async fetch(_query: string, _options?: { hours?: number }): Promise<SentinelRecord[]> {
|
|
33
|
+
// Actual fetching is done by the pipeline via the provider
|
|
34
|
+
throw new Error("Use pipeline.run() instead of adapter.fetch() directly");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { SentinelRecord, SentimentAdapter } from "../types.js";
|
|
3
|
+
import type { WebSearchEnvelope } from "../../types/sentiment.js";
|
|
4
|
+
|
|
5
|
+
export class WebAdapter implements SentimentAdapter {
|
|
6
|
+
readonly source = "web" as const;
|
|
7
|
+
|
|
8
|
+
mapToRecords(envelope: WebSearchEnvelope, query: string): SentinelRecord[] {
|
|
9
|
+
const fetchedAt = envelope.fetchedAt;
|
|
10
|
+
return envelope.results.map((result) => ({
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
source: this.source,
|
|
13
|
+
sourceId: canonicalizeUrl(result.url),
|
|
14
|
+
query,
|
|
15
|
+
title: result.title,
|
|
16
|
+
text: result.snippet,
|
|
17
|
+
author: result.source,
|
|
18
|
+
url: result.url,
|
|
19
|
+
publishedAt: result.published,
|
|
20
|
+
fetchedAt,
|
|
21
|
+
engagement: {
|
|
22
|
+
score: 0,
|
|
23
|
+
replies: null,
|
|
24
|
+
shares: null,
|
|
25
|
+
views: null,
|
|
26
|
+
},
|
|
27
|
+
sentiment: { score: 0, confidence: 0, method: "keyword" as const, tickers: [] },
|
|
28
|
+
metadata: { category: result.category, provider: envelope.provider },
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async fetch(_query: string, _options?: { hours?: number }): Promise<SentinelRecord[]> {
|
|
33
|
+
throw new Error("Use pipeline.run() instead of adapter.fetch() directly");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canonicalizeUrl(url: string): string {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(url);
|
|
40
|
+
return parsed.origin + parsed.pathname;
|
|
41
|
+
} catch {
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SentinelRecord,
|
|
3
|
+
SentinelEngagement,
|
|
4
|
+
SentinelSentiment,
|
|
5
|
+
SentimentAdapter,
|
|
6
|
+
ScorerOptions,
|
|
7
|
+
TrendBucket,
|
|
8
|
+
TrendResult,
|
|
9
|
+
DivergenceResult,
|
|
10
|
+
SentimentSummary,
|
|
11
|
+
SentimentSource,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
export { isSentinelRecord, SENTIMENT_SOURCES } from "./types.js";
|
|
15
|
+
export { SentimentStore } from "./store.js";
|
|
16
|
+
export { scoreRecords, keywordScore } from "./scorer.js";
|
|
17
|
+
export { SentimentPipeline } from "./pipeline.js";
|
|
18
|
+
export { renderSparkline, computeTrend, computeDivergence } from "./trends.js";
|
|
19
|
+
export { BULLISH_TERMS, BEARISH_TERMS } from "./keywords.js";
|
|
20
|
+
export { TwitterAdapter } from "./adapters/twitter.js";
|
|
21
|
+
export { RedditAdapter } from "./adapters/reddit.js";
|
|
22
|
+
export { WebAdapter } from "./adapters/web.js";
|
|
23
|
+
|
|
24
|
+
import { SentimentStore } from "./store.js";
|
|
25
|
+
import { SentimentPipeline } from "./pipeline.js";
|
|
26
|
+
import { getConfig } from "../config.js";
|
|
27
|
+
import { resolveOpenCandlePath } from "../infra/opencandle-paths.js";
|
|
28
|
+
|
|
29
|
+
let _pipeline: SentimentPipeline | null = null;
|
|
30
|
+
let _store: SentimentStore | null = null;
|
|
31
|
+
|
|
32
|
+
export function getSentimentStore(): SentimentStore {
|
|
33
|
+
if (!_store) {
|
|
34
|
+
const dbPath = resolveOpenCandlePath("sentinel.db");
|
|
35
|
+
_store = new SentimentStore(dbPath);
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
_store.prune(config.sentiment?.retentionDays ?? 30);
|
|
38
|
+
}
|
|
39
|
+
return _store;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getSentimentPipeline(): SentimentPipeline {
|
|
43
|
+
if (!_pipeline) {
|
|
44
|
+
const store = getSentimentStore();
|
|
45
|
+
const config = getConfig();
|
|
46
|
+
_pipeline = new SentimentPipeline(store, config.sentiment!);
|
|
47
|
+
}
|
|
48
|
+
return _pipeline;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** For testing: reset singletons */
|
|
52
|
+
export function _resetSentimentSingletons(): void {
|
|
53
|
+
if (_store) {
|
|
54
|
+
try { _store.close(); } catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
_pipeline = null;
|
|
57
|
+
_store = null;
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const BULLISH_TERMS = [
|
|
2
|
+
"moon", "buy", "undervalued", "breakout", "calls", "bullish",
|
|
3
|
+
"rocket", "diamond hands", "accumulate", "dip buy", "long", "rip", "squeeze",
|
|
4
|
+
] as const;
|
|
5
|
+
|
|
6
|
+
export const BEARISH_TERMS = [
|
|
7
|
+
"crash", "overvalued", "sell", "puts", "bearish", "bubble",
|
|
8
|
+
"dump", "short", "bagholding", "exit", "drill", "tank", "rug",
|
|
9
|
+
] as const;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { SentinelRecord, SentimentSummary, TrendResult, DivergenceResult, SentimentSource } from "./types.js";
|
|
2
|
+
import type { SentimentConfig } from "../config.js";
|
|
3
|
+
import { scoreRecords } from "./scorer.js";
|
|
4
|
+
import { SentimentStore } from "./store.js";
|
|
5
|
+
import { computeTrend, computeDivergence, type SourceStats } from "./trends.js";
|
|
6
|
+
|
|
7
|
+
export class SentimentPipeline {
|
|
8
|
+
constructor(
|
|
9
|
+
private store: SentimentStore,
|
|
10
|
+
private config: SentimentConfig,
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
async processRecords(records: SentinelRecord[], query: string): Promise<SentimentSummary> {
|
|
14
|
+
const warnings: string[] = [];
|
|
15
|
+
|
|
16
|
+
if (records.length === 0) {
|
|
17
|
+
return { fresh: [], trend: null, divergence: null, warnings };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if store had prior data before this fetch
|
|
21
|
+
const priorSeries = this.store.getTimeSeries(query, { days: 30, bucketHours: 24 });
|
|
22
|
+
const hadPriorData = priorSeries.length >= 2;
|
|
23
|
+
|
|
24
|
+
// Score all records
|
|
25
|
+
const scored = scoreRecords(records);
|
|
26
|
+
|
|
27
|
+
// Insert into store
|
|
28
|
+
this.store.insert(scored);
|
|
29
|
+
|
|
30
|
+
// Compute trend from historical data (only if we had prior data)
|
|
31
|
+
let trend: TrendResult[] | null = null;
|
|
32
|
+
if (hadPriorData) {
|
|
33
|
+
const series = this.store.getTimeSeries(query, { days: 7, bucketHours: 24 });
|
|
34
|
+
if (series.length >= 2) {
|
|
35
|
+
trend = [computeTrend(series, "aggregate")];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Compute divergence from fresh records
|
|
40
|
+
let divergence: DivergenceResult | null = null;
|
|
41
|
+
const sourceGroups = groupBySource(scored);
|
|
42
|
+
const sourceStats: { twitter?: SourceStats; reddit?: SourceStats; web?: SourceStats; finnhub?: SourceStats } = {};
|
|
43
|
+
|
|
44
|
+
for (const [source, recs] of Object.entries(sourceGroups)) {
|
|
45
|
+
// Exclude comments from divergence calculation
|
|
46
|
+
const postLevel = recs.filter((r) => !r.metadata.isComment);
|
|
47
|
+
if (postLevel.length > 0) {
|
|
48
|
+
const avg = postLevel.reduce((sum, r) => sum + r.sentiment.score, 0) / postLevel.length;
|
|
49
|
+
sourceStats[source as SentimentSource] = { avg, count: postLevel.length };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Object.keys(sourceStats).length >= 2) {
|
|
54
|
+
divergence = computeDivergence(sourceStats, this.config.divergenceThreshold);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { fresh: scored, trend, divergence, warnings };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function groupBySource(records: SentinelRecord[]): Record<string, SentinelRecord[]> {
|
|
62
|
+
const groups: Record<string, SentinelRecord[]> = {};
|
|
63
|
+
for (const r of records) {
|
|
64
|
+
if (!groups[r.source]) groups[r.source] = [];
|
|
65
|
+
groups[r.source].push(r);
|
|
66
|
+
}
|
|
67
|
+
return groups;
|
|
68
|
+
}
|