opencandle 0.3.0 → 0.5.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/LICENSE +1 -1
- package/README.md +106 -14
- package/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +11 -3
- package/dist/infra/browser.js +2 -1
- package/dist/infra/browser.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.d.ts +4 -0
- package/dist/infra/rate-limiter.js +5 -1
- 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/manager.d.ts +9 -0
- package/dist/memory/manager.js +28 -11
- package/dist/memory/manager.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 +7 -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/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/onboarding/connect.d.ts +13 -1
- package/dist/onboarding/connect.js +21 -10
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/prompt-user.d.ts +1 -1
- package/dist/onboarding/providers.d.ts +7 -0
- package/dist/onboarding/providers.js +6 -3
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +1 -1
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +391 -21
- 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 -1
- package/dist/pi/setup.js +11 -1
- 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 +40 -3
- package/dist/prompts/context-builder.js +140 -19
- 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/policy-cards.d.ts +13 -0
- package/dist/prompts/policy-cards.js +197 -0
- package/dist/prompts/policy-cards.js.map +1 -0
- package/dist/prompts/sections.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +208 -22
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.js +23 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +8 -1
- package/dist/providers/sec-edgar.js +172 -5
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +2 -0
- package/dist/providers/yahoo-finance.js +203 -35
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +3 -0
- package/dist/routing/classify-intent.js +82 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.js +4 -4
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/entity-extractor.d.ts +1 -0
- package/dist/routing/entity-extractor.js +158 -12
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/index.d.ts +10 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/legacy-rule-router.d.ts +9 -0
- package/dist/routing/legacy-rule-router.js +12 -0
- package/dist/routing/legacy-rule-router.js.map +1 -0
- package/dist/routing/planning.d.ts +54 -0
- package/dist/routing/planning.js +531 -0
- package/dist/routing/planning.js.map +1 -0
- package/dist/routing/route-manifest.d.ts +35 -0
- package/dist/routing/route-manifest.js +221 -0
- package/dist/routing/route-manifest.js.map +1 -0
- 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 +141 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +71 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +11 -0
- package/dist/routing/router.js +638 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/routing/slot-resolver.js +46 -6
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/turn-context.d.ts +44 -0
- package/dist/routing/turn-context.js +45 -0
- package/dist/routing/turn-context.js.map +1 -0
- package/dist/routing/types.d.ts +13 -1
- package/dist/runtime/answer-contracts.d.ts +82 -0
- package/dist/runtime/answer-contracts.js +414 -0
- package/dist/runtime/answer-contracts.js.map +1 -0
- package/dist/runtime/artifact-contracts.d.ts +14 -0
- package/dist/runtime/artifact-contracts.js +57 -0
- package/dist/runtime/artifact-contracts.js.map +1 -0
- package/dist/runtime/planning-evidence.d.ts +99 -0
- package/dist/runtime/planning-evidence.js +445 -0
- package/dist/runtime/planning-evidence.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +81 -3
- package/dist/runtime/session-coordinator.js +201 -17
- 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/store.js +5 -0
- package/dist/sentiment/store.js.map +1 -1
- package/dist/system-prompt.js +23 -12
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +1 -1
- package/dist/tools/fundamentals/company-overview.js +1 -1
- 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 +1 -1
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +1 -1
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.js +1 -1
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/financials.js +1 -1
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
- package/dist/tools/fundamentals/sec-filings.js +19 -2
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +29 -1
- package/dist/tools/index.js +30 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +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/fear-greed.js +1 -1
- package/dist/tools/macro/fear-greed.js.map +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/macro/fred-data.js +29 -5
- 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-history.js +18 -2
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/crypto-price.js +1 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/search-ticker.js +1 -1
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-history.js +1 -1
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/market/stock-quote.js +1 -1
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +0 -1
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +13 -5
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +1 -1
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
- package/dist/tools/portfolio/holdings-overlap.js +105 -0
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/predictions.js +1 -1
- package/dist/tools/portfolio/predictions.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/tracker.js +1 -1
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.js +12 -4
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.js +57 -2
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-search.js +32 -3
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.js +1 -1
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +3 -3
- package/dist/tools/technical/backtest.js +41 -27
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +8 -4
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/options.d.ts +10 -0
- package/dist/types/portfolio.d.ts +27 -0
- package/dist/workflows/compare-assets.js +38 -2
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/options-screener.js +94 -8
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +9 -5
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/gui-session-manager.ts +5 -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 +254 -0
- package/gui/server/prompt-observation.ts +61 -0
- package/gui/server/server.ts +703 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/session-entry-wait.ts +81 -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-Bmp6Knu7.js +1 -0
- package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
- package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +50 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +206 -0
- package/src/config.ts +245 -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 +73 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +192 -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 +205 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +71 -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 +955 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +381 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +315 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +433 -0
- package/src/providers/alpha-vantage.ts +315 -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 +312 -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 +534 -0
- package/src/routing/classify-intent.ts +285 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +291 -0
- package/src/routing/index.ts +70 -0
- package/src/routing/legacy-rule-router.ts +13 -0
- package/src/routing/planning.ts +732 -0
- package/src/routing/route-manifest.ts +287 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +163 -0
- package/src/routing/router-types.ts +87 -0
- package/src/routing/router.ts +712 -0
- package/src/routing/slot-resolver.ts +190 -0
- package/src/routing/turn-context.ts +111 -0
- package/src/routing/types.ts +75 -0
- package/src/runtime/answer-contracts.ts +633 -0
- package/src/runtime/artifact-contracts.ts +76 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/planning-evidence.ts +591 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +472 -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 +118 -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 +84 -0
- package/src/tools/index.ts +91 -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 +80 -0
- package/src/tools/market/crypto-history.ts +67 -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 +81 -0
- package/src/tools/options/option-chain.ts +96 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/holdings-overlap.ts +123 -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 +159 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +316 -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 +183 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +267 -0
- package/src/tools/technical/indicators.ts +256 -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 +52 -0
- package/src/types/portfolio.ts +73 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +75 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +127 -0
- package/src/workflows/portfolio-builder.ts +56 -0
- package/src/workflows/types.ts +4 -0
- package/dist/runtime/index.d.ts +0 -16
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/provider-ids.d.ts +0 -14
- package/dist/runtime/provider-ids.js +0 -14
- package/dist/runtime/provider-ids.js.map +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getSubredditPosts, getPostComments } from "../../providers/reddit.js";
|
|
4
|
+
import { getTwitterSentiment } from "../../providers/twitter.js";
|
|
5
|
+
import { searchWeb } from "../../providers/web-search.js";
|
|
6
|
+
import { getCompanyNews, finnhubDateRange } from "../../providers/finnhub.js";
|
|
7
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
8
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
9
|
+
import { getConfig } from "../../config.js";
|
|
10
|
+
import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
|
|
11
|
+
import { RedditAdapter } from "../../sentiment/adapters/reddit.js";
|
|
12
|
+
import { WebAdapter } from "../../sentiment/adapters/web.js";
|
|
13
|
+
import { FinnhubAdapter, extractTickersFromQuery } from "../../sentiment/adapters/finnhub.js";
|
|
14
|
+
import { getSentimentPipeline } from "../../sentiment/index.js";
|
|
15
|
+
import type { SentinelRecord } from "../../sentiment/types.js";
|
|
16
|
+
import { hasCredential } from "../../onboarding/providers.js";
|
|
17
|
+
import { buildSoftDegradedTag } from "../../onboarding/tool-tags.js";
|
|
18
|
+
|
|
19
|
+
const params = Type.Object({
|
|
20
|
+
query: Type.String({ description: "Ticker or topic for cross-source sentiment summary" }),
|
|
21
|
+
hours: Type.Optional(
|
|
22
|
+
Type.Number({ description: "Lookback window in hours for live fetching. Default: 24" }),
|
|
23
|
+
),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const sentimentSummaryTool: AgentTool<typeof params> = {
|
|
27
|
+
name: "get_sentiment_summary",
|
|
28
|
+
label: "Sentiment Summary",
|
|
29
|
+
description:
|
|
30
|
+
"Cross-source sentiment summary combining Twitter, Reddit, and web/news. Returns per-source scores, aggregate sentiment, and divergence detection.",
|
|
31
|
+
parameters: params,
|
|
32
|
+
async execute(_toolCallId, args) {
|
|
33
|
+
const hours = args.hours ?? 24;
|
|
34
|
+
const config = getConfig();
|
|
35
|
+
const warnings: string[] = [];
|
|
36
|
+
const allRecords: SentinelRecord[] = [];
|
|
37
|
+
|
|
38
|
+
const twitterAdapter = new TwitterAdapter();
|
|
39
|
+
const webAdapter = new WebAdapter();
|
|
40
|
+
const finnhubAdapter = new FinnhubAdapter();
|
|
41
|
+
|
|
42
|
+
// Determine if Finnhub should be included (key configured + ticker in
|
|
43
|
+
// query). `candidateTickers` is extracted unconditionally so we can tell
|
|
44
|
+
// a "no finnhub-mappable ticker in the query" case apart from a "query
|
|
45
|
+
// has tickers but user has no Finnhub key" case — the latter warrants a
|
|
46
|
+
// soft-degraded tag so the LLM surfaces it in the Data gaps section.
|
|
47
|
+
const candidateTickers = extractTickersFromQuery(args.query);
|
|
48
|
+
const finnhubTickers = config.finnhubApiKey ? candidateTickers : [];
|
|
49
|
+
const includeFinnhub = finnhubTickers.length > 0 && Boolean(config.finnhubApiKey);
|
|
50
|
+
const finnhubSoftDegraded =
|
|
51
|
+
candidateTickers.length > 0 && !hasCredential("finnhub");
|
|
52
|
+
|
|
53
|
+
// Finnhub fetch (built separately to avoid mixing promise types in allSettled)
|
|
54
|
+
const finnhubFetch: Promise<import("../../providers/finnhub.js").FinnhubArticle[]> = includeFinnhub
|
|
55
|
+
? (async () => {
|
|
56
|
+
const { from, to } = finnhubDateRange("day");
|
|
57
|
+
const arrays = await Promise.all(
|
|
58
|
+
finnhubTickers.map((sym) => getCompanyNews(sym, from, to, config.finnhubApiKey!)),
|
|
59
|
+
);
|
|
60
|
+
return arrays.flat();
|
|
61
|
+
})()
|
|
62
|
+
: Promise.resolve([]);
|
|
63
|
+
|
|
64
|
+
// Fetch all sources in parallel
|
|
65
|
+
const [twitterResult, redditResults, webResult, finnhubResult] = await Promise.allSettled([
|
|
66
|
+
// Twitter
|
|
67
|
+
wrapProvider("twitter", () => getTwitterSentiment(args.query, 50, hours)),
|
|
68
|
+
// Reddit — cross-subreddit
|
|
69
|
+
fetchRedditCrossSubreddit(args.query, config.sentiment?.defaultSubreddits ?? ["wallstreetbets", "stocks", "investing", "options"]),
|
|
70
|
+
// Web
|
|
71
|
+
searchWeb(args.query, { freshness: "day", limit: 10, category: "news" }),
|
|
72
|
+
// Finnhub — only when includeFinnhub; otherwise resolves to []
|
|
73
|
+
finnhubFetch,
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// Process Twitter
|
|
77
|
+
if (twitterResult.status === "fulfilled" && twitterResult.value.status === "ok") {
|
|
78
|
+
const records = twitterAdapter.mapToRecords(twitterResult.value.data, args.query);
|
|
79
|
+
allRecords.push(...records);
|
|
80
|
+
} else {
|
|
81
|
+
const reason = twitterResult.status === "rejected"
|
|
82
|
+
? twitterResult.reason?.message ?? "unknown error"
|
|
83
|
+
: (twitterResult.value as any).reason ?? "unavailable";
|
|
84
|
+
warnings.push(`Twitter: ${reason}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Process Reddit
|
|
88
|
+
if (redditResults.status === "fulfilled") {
|
|
89
|
+
const { records: redditRecords, warnings: redditWarnings } = redditResults.value;
|
|
90
|
+
allRecords.push(...redditRecords);
|
|
91
|
+
warnings.push(...redditWarnings);
|
|
92
|
+
} else {
|
|
93
|
+
warnings.push(`Reddit: ${redditResults.reason?.message ?? "unknown error"}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process Web
|
|
97
|
+
if (webResult.status === "fulfilled" && webResult.value.status === "ok") {
|
|
98
|
+
const records = webAdapter.mapToRecords(webResult.value.data, args.query);
|
|
99
|
+
allRecords.push(...records);
|
|
100
|
+
} else {
|
|
101
|
+
const reason = webResult.status === "rejected"
|
|
102
|
+
? webResult.reason?.message ?? "unknown error"
|
|
103
|
+
: (webResult.value as any).reason ?? "unavailable";
|
|
104
|
+
warnings.push(`Web: ${reason}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Process Finnhub (only when included — otherwise resolves to empty array anyway)
|
|
108
|
+
if (includeFinnhub) {
|
|
109
|
+
if (finnhubResult.status === "fulfilled") {
|
|
110
|
+
const articles = finnhubResult.value;
|
|
111
|
+
if (articles.length > 0) {
|
|
112
|
+
const records = finnhubAdapter.mapToRecords(articles, args.query);
|
|
113
|
+
allRecords.push(...records);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
warnings.push(`Finnhub: ${finnhubResult.reason?.message ?? "unknown error"}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const softDegradedPrefix = finnhubSoftDegraded
|
|
121
|
+
? `${buildSoftDegradedTag({
|
|
122
|
+
provider: "finnhub",
|
|
123
|
+
fallback: "other-sentiment-sources",
|
|
124
|
+
remediation: "run /connect news to enable Finnhub company news",
|
|
125
|
+
})}\n\n`
|
|
126
|
+
: "";
|
|
127
|
+
|
|
128
|
+
if (allRecords.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `${softDegradedPrefix}⚠ Sentiment summary unavailable for "${args.query}" — no sources returned data.\n${warnings.join("\n")}`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
details: null as any,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Score and index through pipeline
|
|
141
|
+
const pipeline = getSentimentPipeline();
|
|
142
|
+
const result = await pipeline.processRecords(allRecords, args.query);
|
|
143
|
+
|
|
144
|
+
// Group by source (exclude comments from per-source averages)
|
|
145
|
+
const bySource: Record<string, { total: number; count: number }> = {};
|
|
146
|
+
for (const rec of result.fresh) {
|
|
147
|
+
if (rec.metadata.isComment) continue;
|
|
148
|
+
if (!bySource[rec.source]) bySource[rec.source] = { total: 0, count: 0 };
|
|
149
|
+
bySource[rec.source].total += rec.sentiment.score;
|
|
150
|
+
bySource[rec.source].count++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
lines.push(`**Sentiment summary for "${args.query}"** (last ${hours}h):`);
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push("| Source | Score | Count | Signal |");
|
|
157
|
+
lines.push("|--------|-------|-------|--------|");
|
|
158
|
+
|
|
159
|
+
let totalScore = 0;
|
|
160
|
+
let totalCount = 0;
|
|
161
|
+
for (const [source, stats] of Object.entries(bySource)) {
|
|
162
|
+
const avg = stats.count > 0 ? stats.total / stats.count : 0;
|
|
163
|
+
const label = sentimentLabel(avg);
|
|
164
|
+
const sourceName = source === "web" ? "Web/News" : source.charAt(0).toUpperCase() + source.slice(1);
|
|
165
|
+
lines.push(`| ${sourceName} | ${avg >= 0 ? "+" : ""}${avg.toFixed(2)} | ${stats.count} | ${label} |`);
|
|
166
|
+
totalScore += stats.total;
|
|
167
|
+
totalCount += stats.count;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const aggregate = totalCount > 0 ? totalScore / totalCount : 0;
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(`**Aggregate:** ${aggregate >= 0 ? "+" : ""}${aggregate.toFixed(2)} (${sentimentLabel(aggregate)})`);
|
|
173
|
+
|
|
174
|
+
const priceContext = await buildPriceContext(candidateTickers[0], aggregate);
|
|
175
|
+
if (priceContext) {
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(priceContext);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push("Source-coverage risk: sentiment can be noisy and missing sources can skew the signal; treat this as supporting evidence, not a standalone buy/sell input.");
|
|
182
|
+
|
|
183
|
+
// Divergence
|
|
184
|
+
if (result.divergence && result.divergence.detected) {
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(result.divergence.message);
|
|
187
|
+
} else if (result.divergence && !result.divergence.detected) {
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(result.divergence.message);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Trend
|
|
193
|
+
if (result.trend && result.trend.length > 0) {
|
|
194
|
+
const t = result.trend[0];
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.count} records)`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (warnings.length > 0) {
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push(warnings.map((w) => `⚠ ${w}`).join("\n"));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const output = softDegradedPrefix + lines.join("\n");
|
|
205
|
+
return { content: [{ type: "text", text: output }], details: result };
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
async function buildPriceContext(symbol: string | undefined, aggregateSentiment: number): Promise<string | null> {
|
|
210
|
+
if (!symbol) return null;
|
|
211
|
+
try {
|
|
212
|
+
const quote = await getQuote(symbol);
|
|
213
|
+
const sign = quote.changePercent >= 0 ? "+" : "";
|
|
214
|
+
const direction = quote.changePercent > 0 ? "positive" : quote.changePercent < 0 ? "negative" : "flat";
|
|
215
|
+
const sentimentDirection = aggregateSentiment > 0 ? "positive" : aggregateSentiment < 0 ? "negative" : "neutral";
|
|
216
|
+
const relationship = sentimentDirection === "neutral" || direction === "flat" || sentimentDirection === direction
|
|
217
|
+
? "roughly aligns with price action"
|
|
218
|
+
: "diverges from price action";
|
|
219
|
+
const freshnessNote = formatQuoteFreshnessNote(quote.timestamp);
|
|
220
|
+
return `Price context: ${quote.symbol}: $${quote.price.toFixed(2)} (${sign}${quote.changePercent.toFixed(2)}%).${freshnessNote} The ${sentimentDirection} sentiment signal ${relationship}.`;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatQuoteFreshnessNote(timestamp: number | undefined): string {
|
|
227
|
+
if (!timestamp) return "";
|
|
228
|
+
const quoteDate = new Date(timestamp);
|
|
229
|
+
if (Number.isNaN(quoteDate.getTime())) return "";
|
|
230
|
+
|
|
231
|
+
const now = new Date();
|
|
232
|
+
const quoteDay = quoteDate.toLocaleDateString("en-US", { timeZone: "America/New_York" });
|
|
233
|
+
const currentDay = now.toLocaleDateString("en-US", { timeZone: "America/New_York" });
|
|
234
|
+
const quoteStamp = quoteDate.toLocaleString("en-US", {
|
|
235
|
+
dateStyle: "medium",
|
|
236
|
+
timeStyle: "short",
|
|
237
|
+
timeZone: "America/New_York",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
241
|
+
weekday: "long",
|
|
242
|
+
timeZone: "America/New_York",
|
|
243
|
+
}).format(now);
|
|
244
|
+
const isWeekend = weekday === "Saturday" || weekday === "Sunday";
|
|
245
|
+
|
|
246
|
+
if (quoteDay === currentDay) {
|
|
247
|
+
const marketClosedNote = isWeekend
|
|
248
|
+
? " U.S. markets are closed today, so treat this as delayed or last available price context, not active intraday trading."
|
|
249
|
+
: "";
|
|
250
|
+
return ` Quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const marketClosedNote = isWeekend
|
|
254
|
+
? " U.S. markets are closed today, so treat this as last trading-session price action."
|
|
255
|
+
: "";
|
|
256
|
+
|
|
257
|
+
return ` Last available quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function fetchRedditCrossSubreddit(
|
|
261
|
+
query: string,
|
|
262
|
+
subreddits: string[],
|
|
263
|
+
): Promise<{ records: SentinelRecord[]; warnings: string[] }> {
|
|
264
|
+
const adapter = new RedditAdapter();
|
|
265
|
+
const records: SentinelRecord[] = [];
|
|
266
|
+
const warnings: string[] = [];
|
|
267
|
+
const config = getConfig();
|
|
268
|
+
const commentsPerPost = config.sentiment?.commentsPerPost ?? 5;
|
|
269
|
+
|
|
270
|
+
for (const sub of subreddits) {
|
|
271
|
+
const result = await wrapProvider("reddit", () => getSubredditPosts(sub, 25));
|
|
272
|
+
if (result.status === "unavailable") {
|
|
273
|
+
warnings.push(`Reddit r/${sub}: ${result.reason}`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const postRecords = adapter.mapPostsToRecords(result.data, query);
|
|
277
|
+
|
|
278
|
+
// Topic filter
|
|
279
|
+
const queryLower = query.toLowerCase();
|
|
280
|
+
const filtered = postRecords.filter((r) =>
|
|
281
|
+
r.text.toLowerCase().includes(queryLower) ||
|
|
282
|
+
(r.title?.toLowerCase().includes(queryLower) ?? false),
|
|
283
|
+
);
|
|
284
|
+
records.push(...filtered);
|
|
285
|
+
|
|
286
|
+
// Fetch comments for top posts
|
|
287
|
+
const topPosts = [...filtered]
|
|
288
|
+
.sort((a, b) => b.engagement.score - a.engagement.score)
|
|
289
|
+
.slice(0, 3); // fewer per sub since we're searching multiple
|
|
290
|
+
for (const post of topPosts) {
|
|
291
|
+
if ((post.engagement.replies ?? 0) === 0) continue;
|
|
292
|
+
try {
|
|
293
|
+
const comments = await getPostComments(sub, post.sourceId, commentsPerPost);
|
|
294
|
+
records.push(...adapter.mapCommentsToRecords(comments, post.sourceId, sub, query));
|
|
295
|
+
} catch { /* non-fatal */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Deduplicate
|
|
300
|
+
const seen = new Set<string>();
|
|
301
|
+
const deduped = records.filter((r) => {
|
|
302
|
+
if (seen.has(r.sourceId)) return false;
|
|
303
|
+
seen.add(r.sourceId);
|
|
304
|
+
return true;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return { records: deduped, warnings };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function sentimentLabel(score: number): string {
|
|
311
|
+
if (score > 0.3) return "Bullish";
|
|
312
|
+
if (score < -0.3) return "Bearish";
|
|
313
|
+
if (score > 0) return "Leaning Bullish";
|
|
314
|
+
if (score < 0) return "Leaning Bearish";
|
|
315
|
+
return "Neutral";
|
|
316
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { SentimentStore } from "../../sentiment/store.js";
|
|
4
|
+
import { getSentimentStore } from "../../sentiment/index.js";
|
|
5
|
+
import { computeTrend } from "../../sentiment/trends.js";
|
|
6
|
+
|
|
7
|
+
const params = Type.Object({
|
|
8
|
+
query: Type.String({ description: "Ticker or topic to look up sentiment history" }),
|
|
9
|
+
days: Type.Optional(
|
|
10
|
+
Type.Number({ description: "Number of days of history. Default: 7, max: 30" }),
|
|
11
|
+
),
|
|
12
|
+
source: Type.Optional(
|
|
13
|
+
Type.Union([Type.Literal("twitter"), Type.Literal("reddit"), Type.Literal("web"), Type.Literal("finnhub")], {
|
|
14
|
+
description: "Filter to a single source. Default: all sources.",
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
interface TrendToolResult {
|
|
20
|
+
content: Array<{ type: "text"; text: string }>;
|
|
21
|
+
details: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const sentimentTrendTool: AgentTool<typeof params> & {
|
|
25
|
+
executeWithStore: (toolCallId: string, args: { query: string; days?: number; source?: string }, store: SentimentStore) => Promise<TrendToolResult>;
|
|
26
|
+
} = {
|
|
27
|
+
name: "get_sentiment_trend",
|
|
28
|
+
label: "Sentiment Trend",
|
|
29
|
+
description:
|
|
30
|
+
"Query historical sentiment data from the local store. No live API calls — returns trends from previously fetched data. Run a sentiment query first to populate the store.",
|
|
31
|
+
parameters: params,
|
|
32
|
+
async execute(toolCallId, args) {
|
|
33
|
+
const store = getSentimentStore();
|
|
34
|
+
return sentimentTrendTool.executeWithStore(toolCallId, args, store);
|
|
35
|
+
},
|
|
36
|
+
async executeWithStore(_toolCallId, args, store) {
|
|
37
|
+
const days = Math.min(args.days ?? 7, 30);
|
|
38
|
+
const series = store.getTimeSeries(args.query, { days, bucketHours: 24 });
|
|
39
|
+
|
|
40
|
+
if (series.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `No historical sentiment data for "${args.query}". Run a sentiment query first to populate the store.` }],
|
|
43
|
+
details: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const trend = computeTrend(series, (args.source as any) ?? "aggregate");
|
|
48
|
+
|
|
49
|
+
const lines = [
|
|
50
|
+
`**Sentiment trend for "${args.query}"** (${days}d):`,
|
|
51
|
+
"",
|
|
52
|
+
`${trend.sparkline} ${trend.direction} (${trend.delta >= 0 ? "+" : ""}${trend.delta.toFixed(2)})`,
|
|
53
|
+
`Avg: ${trend.avgScore.toFixed(2)} | Records: ${trend.count}`,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { trend, series } };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getTwitterSentiment } from "../../providers/twitter.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import type { TwitterSentimentResult } from "../../types/sentiment.js";
|
|
6
|
+
import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
|
|
7
|
+
import { getSentimentPipeline } from "../../sentiment/index.js";
|
|
8
|
+
|
|
9
|
+
const params = Type.Object({
|
|
10
|
+
query: Type.String({
|
|
11
|
+
description: "Stock ticker (e.g. AAPL) or search term (e.g. 'AAPL earnings call')",
|
|
12
|
+
}),
|
|
13
|
+
limit: Type.Optional(
|
|
14
|
+
Type.Number({ description: "Max tweets to fetch. Default: 50, max: 200" }),
|
|
15
|
+
),
|
|
16
|
+
hours: Type.Optional(
|
|
17
|
+
Type.Number({ description: "Lookback window in hours. Default: 24" }),
|
|
18
|
+
),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const twitterSentimentTool: AgentTool<typeof params, TwitterSentimentResult> = {
|
|
22
|
+
name: "get_twitter_sentiment",
|
|
23
|
+
label: "Twitter Sentiment",
|
|
24
|
+
description:
|
|
25
|
+
"Fetch recent tweets for a stock ticker or search query and compute engagement-weighted sentiment. Returns tweet data, sentiment score, and co-mentioned tickers. Requires a Twitter session via trigger_twitter_login.",
|
|
26
|
+
parameters: params,
|
|
27
|
+
async execute(_toolCallId, args) {
|
|
28
|
+
const limit = Math.min(args.limit ?? 50, 200);
|
|
29
|
+
const hours = args.hours ?? 24;
|
|
30
|
+
|
|
31
|
+
const providerResult = await wrapProvider("twitter", () =>
|
|
32
|
+
getTwitterSentiment(args.query, limit, hours),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (providerResult.status === "unavailable") {
|
|
36
|
+
const isLoginIssue =
|
|
37
|
+
providerResult.reason.includes("No Twitter session") ||
|
|
38
|
+
providerResult.reason.includes("session expired");
|
|
39
|
+
const text = isLoginIssue
|
|
40
|
+
? `⚠ Twitter sentiment unavailable: ${providerResult.reason}\n[LOGIN_NEEDED] Use ask_user to confirm, then call trigger_twitter_login. After success, retry this tool.`
|
|
41
|
+
: `⚠ Twitter sentiment unavailable (${providerResult.reason}).`;
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text }],
|
|
44
|
+
details: null as any,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = providerResult.data;
|
|
49
|
+
|
|
50
|
+
const sentimentLabel =
|
|
51
|
+
result.sentimentScore > 0.3 ? "Bullish" :
|
|
52
|
+
result.sentimentScore < -0.3 ? "Bearish" :
|
|
53
|
+
result.sentimentScore > 0 ? "Leaning Bullish" :
|
|
54
|
+
result.sentimentScore < 0 ? "Leaning Bearish" : "Neutral";
|
|
55
|
+
|
|
56
|
+
const lines = [
|
|
57
|
+
`**Twitter: ${result.query}** — ${result.tweetCount} tweets (last ${hours}h, ${result.fetchedAt})`,
|
|
58
|
+
`Sentiment: ${result.sentimentScore.toFixed(2)} (${sentimentLabel}) | Bullish: ${result.bullishCount} | Bearish: ${result.bearishCount}`,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
if (result.topMentions.length > 0) {
|
|
62
|
+
lines.push(`Co-mentions: ${result.topMentions.map((t) => `$${t}`).join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("| Author | Tweet | ❤️ | 🔁 | 💬 |");
|
|
67
|
+
lines.push("|--------|-------|----|----|----|");
|
|
68
|
+
const top = result.tweets.slice(0, 15);
|
|
69
|
+
for (const tweet of top) {
|
|
70
|
+
const text = tweet.text.replace(/\|/g, "\\|").replace(/\n/g, " ").slice(0, 100);
|
|
71
|
+
lines.push(`| @${tweet.author} | ${text} | ${tweet.likes} | ${tweet.retweets} | ${tweet.replies} |`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (providerResult.stale) {
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push(`⚠ Stale data (cached at ${providerResult.timestamp})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Index in sentiment store and append trend context
|
|
80
|
+
try {
|
|
81
|
+
const adapter = new TwitterAdapter();
|
|
82
|
+
const records = adapter.mapToRecords(result, args.query);
|
|
83
|
+
const pipeline = getSentimentPipeline();
|
|
84
|
+
const pipelineResult = await pipeline.processRecords(records, args.query);
|
|
85
|
+
if (pipelineResult.trend && pipelineResult.trend.length > 0) {
|
|
86
|
+
const t = pipelineResult.trend[0];
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.delta >= 0 ? "+" : ""}${t.delta.toFixed(2)}, ${t.count} records)`);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Sentiment indexing is best-effort — don't fail the tool
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: result };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { searchWeb } from "../../providers/web-search.js";
|
|
4
|
+
import type { WebSearchEnvelope } from "../../types/sentiment.js";
|
|
5
|
+
import { hasCredential } from "../../onboarding/providers.js";
|
|
6
|
+
import { buildSoftDegradedTag } from "../../onboarding/tool-tags.js";
|
|
7
|
+
|
|
8
|
+
const params = Type.Object({
|
|
9
|
+
query: Type.String({ description: "Search query — ticker, topic, or question" }),
|
|
10
|
+
category: Type.Optional(
|
|
11
|
+
Type.Union([Type.Literal("news"), Type.Literal("general")], {
|
|
12
|
+
description: 'Search category. "news" for recent articles, "general" for broader web. Default: "news"',
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
freshness: Type.Optional(
|
|
16
|
+
Type.Union(
|
|
17
|
+
[Type.Literal("hours"), Type.Literal("day"), Type.Literal("week"), Type.Literal("month")],
|
|
18
|
+
{ description: 'Time range filter. Default: "day"' },
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
limit: Type.Optional(
|
|
22
|
+
Type.Number({ description: "Number of results (1-20). Default: 10", minimum: 1, maximum: 20 }),
|
|
23
|
+
),
|
|
24
|
+
provider: Type.Optional(
|
|
25
|
+
Type.Union([Type.Literal("exa"), Type.Literal("brave"), Type.Literal("ddg")], {
|
|
26
|
+
description: "Override search provider (skip cascade). Default: auto (Exa → Brave → DDG)",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function escapeMd(text: string): string {
|
|
32
|
+
return text.replace(/([[\]|])/g, "\\$1");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeUrl(url: string): string {
|
|
36
|
+
if (url.startsWith("https://") || url.startsWith("http://")) return url;
|
|
37
|
+
return `https://${url}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build soft-degradation tags for search providers whose credentials are
|
|
42
|
+
* missing at call time. Returns an empty string when nothing is degraded, or
|
|
43
|
+
* a newline-terminated block of `[OPENCANDLE_SOFT_DEGRADED ...]` tags ready to
|
|
44
|
+
* prepend to the tool result content. The extension's `tool_result` handler
|
|
45
|
+
* records these into the per-turn degradation accumulator; the system prompt
|
|
46
|
+
* instructs the LLM to surface them in a `**Data gaps**` section.
|
|
47
|
+
*
|
|
48
|
+
* Emission rules:
|
|
49
|
+
* - Brave: if `hasCredential("brave") === false` AND the envelope's
|
|
50
|
+
* provider is not `"brave"`, the cascade fell back from Brave.
|
|
51
|
+
* - Exa: if `hasCredential("exa") === false` AND the envelope's provider
|
|
52
|
+
* is `"exa"`, the Exa provider used the keyless MCP path instead of the
|
|
53
|
+
* keyed API. Envelopes served by `ddg` are NOT tagged for Exa (Exa was
|
|
54
|
+
* tried first and failed for a reason unrelated to credentials).
|
|
55
|
+
*/
|
|
56
|
+
function buildSoftDegradedPrefix(data: WebSearchEnvelope): string {
|
|
57
|
+
const tags: string[] = [];
|
|
58
|
+
|
|
59
|
+
if (!hasCredential("brave") && data.provider !== "brave") {
|
|
60
|
+
tags.push(
|
|
61
|
+
buildSoftDegradedTag({
|
|
62
|
+
provider: "brave",
|
|
63
|
+
fallback: data.provider === "exa" ? "exa" : "ddg",
|
|
64
|
+
remediation: "run /connect search to enable Brave",
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!hasCredential("exa") && data.provider === "exa") {
|
|
70
|
+
tags.push(
|
|
71
|
+
buildSoftDegradedTag({
|
|
72
|
+
provider: "exa",
|
|
73
|
+
fallback: "keyless-mcp",
|
|
74
|
+
remediation: "run /connect search to enable keyed Exa",
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return tags.length === 0 ? "" : `${tags.join("\n")}\n\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildOfficialSourceGapPrefix(query: string, data: WebSearchEnvelope): string {
|
|
83
|
+
if (!hasOfficialFedSourceGap(query, data)) return "";
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
"[OPENCANDLE_SOURCE_GAP source=fed_official evidence=missing remediation=\"verify against federalreserve.gov/FOMC before stating Fed announcements\"]",
|
|
87
|
+
"Hard source gap: no official Fed/FOMC source was returned. Do not present meeting announcements, votes, quotes, appointments, leadership changes, or named policy rationales as verified; treat results as market commentary only.",
|
|
88
|
+
"",
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasOfficialFedSourceGap(query: string, data: WebSearchEnvelope): boolean {
|
|
93
|
+
return isFedAnnouncementQuery(query) &&
|
|
94
|
+
!data.results.some((result) => isOfficialFedSource(result.source) || isOfficialFedSource(result.url));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isFedAnnouncementQuery(query: string): boolean {
|
|
98
|
+
const lower = query.toLowerCase();
|
|
99
|
+
const mentionsFed = /\b(?:fed|fomc|federal reserve)\b/.test(lower);
|
|
100
|
+
const asksOfficialFact = /\b(?:announcement|meeting|minutes|statement|decision|vote|chair|governor|appointment|leadership)\b/.test(lower);
|
|
101
|
+
return mentionsFed && asksOfficialFact;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isOfficialFedSource(value: string): boolean {
|
|
105
|
+
const lower = value.toLowerCase();
|
|
106
|
+
return lower.includes("federalreserve.gov") || lower.includes("fomc.gov");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
|
|
110
|
+
name: "search_web",
|
|
111
|
+
label: "Web Search",
|
|
112
|
+
description:
|
|
113
|
+
"Search the web for financial news, earnings context, company events, regulatory developments, or general information. " +
|
|
114
|
+
"NOT for real-time prices, historical data, fundamentals, macro data, SEC filings, or social sentiment — those have dedicated tools.",
|
|
115
|
+
parameters: params,
|
|
116
|
+
|
|
117
|
+
async execute(_toolCallId, args) {
|
|
118
|
+
const query = args.query?.trim();
|
|
119
|
+
if (!query) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: "⚠ Cannot search with an empty query." }],
|
|
122
|
+
details: null as any,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const category = args.category ?? "news";
|
|
127
|
+
const freshness = args.freshness ?? "day";
|
|
128
|
+
const limit = Math.max(1, Math.min(args.limit ?? 10, 20));
|
|
129
|
+
|
|
130
|
+
const provider = args.provider;
|
|
131
|
+
const result = await searchWeb(query, { category, freshness, limit, provider });
|
|
132
|
+
|
|
133
|
+
if (result.status === "unavailable") {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: `⚠ Web search unavailable (${result.reason}).` }],
|
|
136
|
+
details: null as any,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = result.data;
|
|
141
|
+
|
|
142
|
+
if (data.resultCount === 0) {
|
|
143
|
+
const zeroPrefix = buildSoftDegradedPrefix(data);
|
|
144
|
+
const sourceGapPrefix = buildOfficialSourceGapPrefix(query, data);
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: `${zeroPrefix}${sourceGapPrefix}No results found for "${query}" (${category}, past ${freshness}).`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
details: data,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const stalePrefix = result.stale
|
|
157
|
+
? `⚠ Using cached data from ${result.timestamp}\n\n`
|
|
158
|
+
: "";
|
|
159
|
+
|
|
160
|
+
const softDegradedPrefix = buildSoftDegradedPrefix(data);
|
|
161
|
+
const sourceGapPrefix = buildOfficialSourceGapPrefix(query, data);
|
|
162
|
+
const shouldOmitResults = hasOfficialFedSourceGap(query, data);
|
|
163
|
+
|
|
164
|
+
const header = `**Web Search** — ${data.resultCount} results for "${query}" (${category}, past ${freshness}, via ${data.provider})`;
|
|
165
|
+
const items = data.results.map((r) => {
|
|
166
|
+
const title = escapeMd(r.title);
|
|
167
|
+
const snippet = escapeMd(r.snippet);
|
|
168
|
+
const url = safeUrl(r.url);
|
|
169
|
+
const pub = r.published ? `Published: ${r.published}` : "Published: unknown";
|
|
170
|
+
return `• [${title}](${url}) — ${r.source}\n ${snippet}\n ${pub}`;
|
|
171
|
+
});
|
|
172
|
+
const body = shouldOmitResults
|
|
173
|
+
? "Non-official results were omitted from assistant-visible evidence for this Fed/FOMC announcement query. Verify against an official Federal Reserve or FOMC source before naming announcements or personnel changes."
|
|
174
|
+
: items.join("\n\n");
|
|
175
|
+
|
|
176
|
+
const text = `${softDegradedPrefix}${sourceGapPrefix}${stalePrefix}${header}\n\n${body}`;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text }],
|
|
180
|
+
details: data,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
};
|