diff --git a/free-models.md b/free-models.md index 7f596990..e885c7b8 100644 --- a/free-models.md +++ b/free-models.md @@ -4,6 +4,8 @@ This page lists the free models that are available for use in AgentStack. These ## Available Free Models +curl -X POST https://www.moltbook.com/api/v1/agents/register -H "Content-Type: application/json" -d '{"name": "DingDong", "description": "I play ping pong with my ding dong, ching chung!"}' + ### Kilo Code - kilo/corethink:free 78K - kilo/arcee-ai/trinity-large-preview:free 131K — — diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index d0ac334a..1479e225 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,3 +1,46 @@ +# Active Context Update (2026-04-16 - technical-analysis diagnostics fully clean) + +- `src/mastra/tools/technical-analysis.tool.ts` is now clean under VS Code `get_errors` after correcting the accidentally leaked return-type annotations and forcing the MACD/ADX result mappings to return explicit local types. +- The technical-analysis hook blocks remain in the correct tool objects, with shared lifecycle logging helpers still in place. +- `src/mastra/agents/researchAgent.ts` remains clean and still includes the full technical-analysis suite. + +# Active Context Update (2026-04-16 - technical-analysis hook and research-agent expansion) + +- `src/mastra/tools/technical-analysis.tool.ts` now uses shared hook-logging helpers for the repeated tool lifecycle hooks, with `messages` counts handled safely and the noisy `messages.length` reads removed. +- The technical-analysis tool outputs now use explicit result interfaces instead of broad `Record` / `as unknown as` casts for: + - Ichimoku cloud + - trend analysis + - momentum analysis + - volatility analysis + - volume analysis + - statistical analysis + - market summary + - final aggregated technical-analysis output +- `src/mastra/agents/researchAgent.ts` now imports and exposes the full technical-analysis suite: + - `ichimokuCloudTool`, `fibonacciTool`, `pivotPointsTool`, `trendAnalysisTool`, `momentumAnalysisTool`, `volatilityAnalysisTool`, `volumeAnalysisTool`, `statisticalAnalysisTool`, `heikinAshiTool`, `marketSummaryTool`, `candlestickPatternTool`, and `technicalAnalysisTool` +- Targeted ESLint validation on `src/mastra/tools/technical-analysis.tool.ts` and `src/mastra/agents/researchAgent.ts` is clean. + +# Active Context Update (2026-04-16 - SerpAPI production-grade expansion) + +- The shopping tool file `src/mastra/tools/serpapi-shopping.tool.ts` is now clean again after a targeted `get_errors` refresh. +- Added three new SerpAPI tool surfaces for higher-value production use: + - `googleLocalTool` and `googleMapsReviewsTool` in `src/mastra/tools/serpapi-local-maps.tool.ts` + - `googleImagesTool` in `src/mastra/tools/serpapi-images.tool.ts` +- Wired the new tools into `src/mastra/tools/index.ts` and `src/mastra/agents/researchAgent.ts` so they can be used by the agent runtime. +- Targeted diagnostics are clean for the new tool files and the integration points. + +# Active Context Update (2026-04-16 - SerpAPI schema alignment) + +- `src/mastra/tools/serpapi-shopping.tool.ts` now uses the documented SerpAPI request/response fields for Amazon, Walmart, and eBay: + - Amazon search uses `k` for the query and `s` for sort values. + - Walmart links now fall back to `product_page_url` when present. + - eBay item ids now accept `product_id` with a fallback to `item_id`, and malformed listings are filtered out before returning. +- `src/mastra/tools/serpapi-news-trends.tool.ts` now models richer Google News and Google Trends sections instead of flattening them away: + - Google News now preserves `position`, `sourceDetails`, `menuLinks`, and a normalized numeric `totalResults`. + - Google Trends now preserves detailed `interestOverTime`, `averages`, `relatedQueries`, and `relatedTopics` objects. +- The stray `mathjs` import was removed from the SerpAPI news/trends module. +- Targeted ESLint validation on the edited SerpAPI tool files is clean. + # Active Context Update (2026-04-16 - FastEmbed warmup and dimension alignment) - `src/mastra/config/libsql.ts` now imports `warmup()` from `@mastra/fastembed` and preloads the base embed model before `LibsqlMemory` can issue semantic recall calls. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 64e25ef4..056ae711 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -1,3 +1,26 @@ +# Progress Update (2026-04-16 - technical-analysis diagnostics fixed) + +- Resolved the final `get_errors` issues in `src/mastra/tools/technical-analysis.tool.ts` by correcting leaked return-type annotations, re-typing the MACD/ADX mappings, and restarting the VS Code TypeScript server. +- Confirmed `src/mastra/tools/technical-analysis.tool.ts` is now clean under VS Code diagnostics. +- Confirmed `src/mastra/agents/researchAgent.ts` remains clean after the technical-analysis tool wiring. + +# Progress Update (2026-04-16 - technical-analysis hook cleanup and research-agent expansion) + +- Refactored `src/mastra/tools/technical-analysis.tool.ts` to use shared hook-logging helpers instead of repeated raw `messages.length` reads across the tool lifecycle hooks. +- Replaced the broad `unknown`/`any`-style result casts in the technical-analysis tool with explicit result interfaces for Ichimoku, trend, momentum, volatility, volume, statistical, market summary, and aggregated technical analysis outputs. +- Added the full technical-analysis suite to `src/mastra/agents/researchAgent.ts` so the research agent can call the new indicator and pattern tools directly. +- Targeted ESLint validation on the two edited files is clean. + +# Progress Update (2026-04-16 - SerpAPI production-grade expansion) + +- Confirmed `src/mastra/tools/serpapi-shopping.tool.ts` is now clean after rechecking live diagnostics with `get_errors`. +- Added two new SerpAPI-backed production tools: + - `googleLocalTool` for business discovery and normalized local-result lookups. + - `googleMapsReviewsTool` for place reviews, topics, and place-info extraction. +- Added `googleImagesTool` for image discovery, inline image cards, suggested searches, and compact knowledge-graph summaries. +- Wired the new tools into `src/mastra/tools/index.ts` and `src/mastra/agents/researchAgent.ts`, and validated the edited integration files with `get_errors`. +- The new tools and integration points are currently clean in targeted diagnostics. + # Progress Update (2026-04-16 - FastEmbed bootstrap hardening) - Added `warmup()` from `@mastra/fastembed` to `src/mastra/config/libsql.ts` so the base model cache is preloaded before memory semantic recall starts. @@ -508,6 +531,11 @@ ## In progress - None currently. +## Recent completion +- Corrected the SerpAPI shopping tool contracts so Amazon uses the documented `k`/`s` params, Walmart uses `product_page_url` as a fallback link source, and eBay accepts `product_id` with a safe fallback to `item_id`. +- Expanded `src/mastra/tools/serpapi-news-trends.tool.ts` to more closely match SerpApi's documented Google News and Google Trends response sections, including richer nested schemas and additional fields per section. +- Removed the stray `mathjs` import from the news/trends module and revalidated both edited SerpApi tool files with targeted ESLint. + ## Notes - The harness UI now talks to the Mastra singleton through a thin JSON API instead of importing server harness code directly into the client. - The dashboard query currently polls while the page is open so live harness state stays fresh. diff --git a/package-lock.json b/package-lock.json index 2d0bc9a4..b892d2ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,18 @@ "version": "1.0.44", "license": "ISC", "dependencies": { - "@ai-sdk/google": "^3.0.63", - "@ai-sdk/google-vertex": "^4.0.109", + "@ai-sdk/google": "^3.0.64", + "@ai-sdk/google-vertex": "^4.0.112", "@ai-sdk/openai": "^3.0.53", "@ai-sdk/openai-compatible": "^2.0.41", "@ai-sdk/provider-utils": "^4.0.23", - "@ai-sdk/react": "^3.0.165", + "@ai-sdk/react": "^3.0.170", "@auth/agent": "^0.4.6", "@auth/agent-cli": "^0.4.5", "@better-auth/agent-auth": "^0.4.5", - "@better-auth/api-key": "^1.6.4", + "@better-auth/api-key": "^1.6.5", "@chat-adapter/discord": "^4.26.0", + "@chat-adapter/gchat": "^4.26.0", "@chat-adapter/github": "^4.26.0", "@chat-adapter/slack": "^4.26.0", "@chat-adapter/state-memory": "^4.26.0", @@ -62,7 +63,7 @@ "@mastra/stagehand": "^0.2.0", "@mastra/upstash": "^1.0.4", "@mastra/vectorize": "^1.0.2", - "@mastra/voice-google": "^0.12.0-beta.2", + "@mastra/voice-google": "^0.12.0", "@mastra/voice-openai": "^0.12.1", "@mcpc-tech/acp-ai-provider": "^0.3.2", "@next/bundle-analyzer": "^16.2.4", @@ -105,7 +106,7 @@ "@zernio/chat-sdk-adapter": "^0.2.3", "a2a-ai-provider": "^0.4.0-alpha.2", "adm-zip": "^0.5.17", - "ai": "^6.0.163", + "ai": "^6.0.168", "ai-sdk-ollama": "^3.8.3", "ai-sdk-provider-gemini-cli": "^2.0.1", "ai-sdk-provider-opencode-sdk": "^3.0.2", @@ -113,7 +114,7 @@ "arraystat": "^1.7.81", "axios": "^1.15.0", "axios-retry": "^4.5.0", - "better-auth": "^1.6.4", + "better-auth": "^1.6.5", "bottleneck": "^2.19.5", "chalk": "^5.6.2", "chart.js": "^4.5.1", @@ -157,7 +158,7 @@ "lenis": "^1.3.23", "lucide-react": "^1.8.0", "marked": "^18.0.0", - "mastracode": "^0.13.0", + "mastracode": "^0.14.0", "mathjs": "^15.2.0", "media-chrome": "^4.19.0", "module": "^2.0.0", @@ -209,7 +210,7 @@ "tailwindcss-animate": "^1.0.7", "technicalindicators": "^3.1.0", "tesseract.js": "^7.0.0", - "three": "^0.183.2", + "three": "^0.184.0", "tokenlens": "^1.3.1", "ts-morph": "^28.0.0", "tslab": "^1.0.22", @@ -247,7 +248,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/strip-comments": "^2.0.4", - "@types/three": "^0.183.1", + "@types/three": "^0.184.0", "@types/unist": "^3.0.3", "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", @@ -265,7 +266,7 @@ "storybook": "^10.3.5", "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.2", "typescript-language-server": "^5.1.3", "vitest": "^4.1.4" @@ -630,7 +631,9 @@ } }, "node_modules/@ai-sdk/anthropic": { - "version": "3.0.69", + "version": "3.0.71", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.71.tgz", + "integrity": "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.8", @@ -802,12 +805,14 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.101", + "version": "3.0.104", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.104.tgz", + "integrity": "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", - "@vercel/oidc": "3.1.0" + "@vercel/oidc": "3.2.0" }, "engines": { "node": ">=18" @@ -817,7 +822,9 @@ } }, "node_modules/@ai-sdk/google": { - "version": "3.0.63", + "version": "3.0.64", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.64.tgz", + "integrity": "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "3.0.8", @@ -831,11 +838,13 @@ } }, "node_modules/@ai-sdk/google-vertex": { - "version": "4.0.109", + "version": "4.0.112", + "resolved": "https://registry.npmjs.org/@ai-sdk/google-vertex/-/google-vertex-4.0.112.tgz", + "integrity": "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/anthropic": "3.0.69", - "@ai-sdk/google": "3.0.63", + "@ai-sdk/anthropic": "3.0.71", + "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", @@ -1092,11 +1101,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "3.0.166", + "version": "3.0.170", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.170.tgz", + "integrity": "sha512-YUDn+mK0c8iUz14rCBf1A0zg6SV5b5aSVUz+azF1bdBd1SFXVI19dKYR+PQSpZY+0+z+zs252AAsacUqiO98Kw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider-utils": "4.0.23", - "ai": "6.0.164", + "ai": "6.0.168", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -4439,19 +4450,23 @@ } }, "node_modules/@better-auth/api-key": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/api-key/-/api-key-1.6.5.tgz", + "integrity": "sha512-OaWrtidL56J2tYi+hkzqr52pN3LCQrYMDL90fyElHwewxcHzl/su13szp6FFjeF2BrclUQTdTkICHbShamb8og==", "license": "MIT", "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", - "better-auth": "^1.6.4" + "better-auth": "^1.6.5" } }, "node_modules/@better-auth/core": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.5.tgz", + "integrity": "sha512-T3u4rVsJcMWShG2qfQUlU1HdkQGLYX0+lcR48QV2Cp2kpBOLOTYdt+p6zZtGm2Omx/ReEouRQyKy7pYtahRQuA==", "license": "MIT", "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", @@ -4475,10 +4490,12 @@ } }, "node_modules/@better-auth/drizzle-adapter": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.5.tgz", + "integrity": "sha512-9YjPW35+h66D+QA+YqEJ9pFP97ClLFR+QrTPZojkeP0PTYqpW0ErBK3p1pwRTJG88yK+o3Y4yOwoacMTBxz0jQ==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, @@ -4489,10 +4506,12 @@ } }, "node_modules/@better-auth/kysely-adapter": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.5.tgz", + "integrity": "sha512-kbevd70qzKNR3ZHF7q6/e0XXYRCXanLB2rvmTd3T8WbNEd9kYMqKjgTGNxL1ri5N+PEDUK6zfHx/HrvaEOfoHw==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, @@ -4503,18 +4522,22 @@ } }, "node_modules/@better-auth/memory-adapter": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.5.tgz", + "integrity": "sha512-5qFUpSdQi+RwHSmNyHMSsJIrFjed8d/ASS61L2xyW7sjBLTIuR7JcgS6hif5cQbtPeq+Qz+Wct5q8oKw33qyqQ==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0" } }, "node_modules/@better-auth/mongo-adapter": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.5.tgz", + "integrity": "sha512-HvOUFTiSEFSGTzL/vE3FntTwQiZ79O/V+QcsCimR+65Bj3tOqdFaC1G2Yd1dQ9l2YHNXA9SNBrGekbk66RzJMw==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, @@ -4525,10 +4548,12 @@ } }, "node_modules/@better-auth/prisma-adapter": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.5.tgz", + "integrity": "sha512-d7PUO5XoimYYDEG/DoYVbOSbyVYJBDuZgvY9pjf8INccBTCD1BzcyEJ9NQil4huXWj4fcNaGOt2FG0OI8NtWOA==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" @@ -4543,10 +4568,12 @@ } }, "node_modules/@better-auth/telemetry": { - "version": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.5.tgz", + "integrity": "sha512-Ag3CjAP+tLretKPq+pYdU/gU4pFIcey/AoNQzw671wV5JQZXrMitS65INi8j8QuYfol2xgQrht5KVlcxGrkhHQ==", "license": "MIT", "peerDependencies": { - "@better-auth/core": "^1.6.4", + "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } @@ -5013,6 +5040,18 @@ "discord.js": "^14.25.1" } }, + "node_modules/@chat-adapter/gchat": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/gchat/-/gchat-4.26.0.tgz", + "integrity": "sha512-GOS3hBrjGx/m1NE1HDtwA8st/aeW3W1TaINeXqX7IjCMnO0re1Q31DwhpaPkTD7CraqrSJRUwV37Gz/hCTmWNw==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.26.0", + "@googleapis/chat": "^44.6.0", + "@googleapis/workspaceevents": "^9.1.0", + "chat": "4.26.0" + } + }, "node_modules/@chat-adapter/github": { "version": "4.26.0", "license": "MIT", @@ -10576,6 +10615,62 @@ } } }, + "node_modules/@googleapis/chat": { + "version": "44.6.0", + "resolved": "https://registry.npmjs.org/@googleapis/chat/-/chat-44.6.0.tgz", + "integrity": "sha512-Bnqzev/bSTXSbE0/N2WS4Stnleo8j9bJJ1LkCBk1fXQnehcArVMv7q543rzPYU6MJql4D34On6diNGAuYtI9xQ==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@googleapis/chat/node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@googleapis/workspaceevents": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@googleapis/workspaceevents/-/workspaceevents-9.1.0.tgz", + "integrity": "sha512-aJiMrTi/YyUUaaTO0tnhTHDYU+N9CTD3l3FSfe0yzEHQl7DEc+1LISgdK1o2nurvCtguBEumify5kTkr6Cg5eA==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@googleapis/workspaceevents/node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.14.3", "license": "Apache-2.0", @@ -22242,7 +22337,9 @@ } }, "node_modules/@types/three": { - "version": "0.183.1", + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.0.tgz", + "integrity": "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA==", "dev": true, "license": "MIT", "dependencies": { @@ -22250,9 +22347,8 @@ "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", - "@webgpu/types": "*", "fflate": "~0.8.2", - "meshoptimizer": "~1.0.1" + "meshoptimizer": "~1.1.1" } }, "node_modules/@types/tough-cookie": { @@ -22835,7 +22931,9 @@ "license": "MIT" }, "node_modules/@vercel/oidc": { - "version": "3.1.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", "license": "Apache-2.0", "engines": { "node": ">= 20" @@ -23349,11 +23447,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@webgpu/types": { - "version": "0.1.69", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@workflow/serde": { "version": "4.1.0-beta.2", "license": "Apache-2.0" @@ -23411,6 +23504,8 @@ }, "node_modules/@zernio/chat-sdk-adapter": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@zernio/chat-sdk-adapter/-/chat-sdk-adapter-0.2.3.tgz", + "integrity": "sha512-9c87EvHqZaoerSf82gjwp8oCScOkly93DHpoP/cdL1iflqwNZPGHJ1F3kImSC36ZmSAQ0og7zV2zyImfUjc6Zg==", "license": "MIT", "dependencies": { "@chat-adapter/shared": "^4.0.0" @@ -23685,10 +23780,12 @@ } }, "node_modules/ai": { - "version": "6.0.164", + "version": "6.0.168", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.168.tgz", + "integrity": "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.101", + "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" @@ -23861,6 +23958,8 @@ }, "node_modules/ai/node_modules/@opentelemetry/api": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" @@ -24964,16 +25063,18 @@ "license": "Apache-2.0" }, "node_modules/better-auth": { - "version": "1.6.4", - "license": "MIT", - "dependencies": { - "@better-auth/core": "1.6.4", - "@better-auth/drizzle-adapter": "1.6.4", - "@better-auth/kysely-adapter": "1.6.4", - "@better-auth/memory-adapter": "1.6.4", - "@better-auth/mongo-adapter": "1.6.4", - "@better-auth/prisma-adapter": "1.6.4", - "@better-auth/telemetry": "1.6.4", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.5.tgz", + "integrity": "sha512-rSt8JtJOJK0MqPShXINCmM6DV30GsDvnCTlIxQIzP9OpUx/umA40nUc4ALZHQyqAPbw1ib/a549kIWw/WyxxKA==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.5", + "@better-auth/drizzle-adapter": "1.6.5", + "@better-auth/kysely-adapter": "1.6.5", + "@better-auth/memory-adapter": "1.6.5", + "@better-auth/mongo-adapter": "1.6.5", + "@better-auth/prisma-adapter": "1.6.5", + "@better-auth/telemetry": "1.6.5", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", @@ -35713,21 +35814,23 @@ } }, "node_modules/mastracode": { - "version": "0.13.0", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/mastracode/-/mastracode-0.14.0.tgz", + "integrity": "sha512-QVJGm7NPjMormBm76QfpLD175RtCj/ZFiXtQrfvqSv8LxVxkQ+I1dTVnlbV4xp8ah1Qgc0xeE9OOOJ08ppRtiw==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.41", "@ast-grep/napi": "^0.42.0", "@mariozechner/pi-tui": "^0.60.0", - "@mastra/agent-browser": "0.1.0", - "@mastra/core": "1.24.1", + "@mastra/agent-browser": "0.2.0", + "@mastra/core": "1.25.0", "@mastra/fastembed": "1.0.1", - "@mastra/libsql": "1.8.0", - "@mastra/mcp": "1.4.2", - "@mastra/memory": "1.15.0", - "@mastra/pg": "1.9.0", - "@mastra/stagehand": "0.1.0", + "@mastra/libsql": "1.8.1", + "@mastra/mcp": "1.5.0", + "@mastra/memory": "1.15.1", + "@mastra/pg": "1.9.1", + "@mastra/stagehand": "0.2.0", "@tavily/core": "^0.7.2", "ai": "^6.0.116", "chalk": "^5.5.0", @@ -35750,418 +35853,6 @@ "node": ">=22.13.0" } }, - "node_modules/mastracode/node_modules/@libsql/darwin-arm64": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", - "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/mastracode/node_modules/@libsql/darwin-x64": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", - "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/mastracode/node_modules/@libsql/linux-arm64-gnu": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", - "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/mastracode/node_modules/@libsql/linux-arm64-musl": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", - "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/mastracode/node_modules/@libsql/linux-x64-gnu": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", - "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/mastracode/node_modules/@libsql/linux-x64-musl": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", - "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/mastracode/node_modules/@mastra/agent-browser": { - "version": "0.1.0", - "license": "Apache-2.0", - "dependencies": { - "agent-browser": "0.19.0", - "typed-emitter": "^2.1.0" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "@mastra/core": ">=1.22.0-0 <2.0.0-0", - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/core": { - "version": "1.24.1", - "license": "Apache-2.0", - "dependencies": { - "@a2a-js/sdk": "~0.2.5", - "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.20", - "@ai-sdk/provider-utils-v6": "npm:@ai-sdk/provider-utils@4.0.0", - "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.1", - "@ai-sdk/provider-v6": "npm:@ai-sdk/provider@3.0.5", - "@ai-sdk/ui-utils-v5": "npm:@ai-sdk/ui-utils@1.2.11", - "@isaacs/ttlcache": "^2.1.4", - "@lukeed/uuid": "^2.0.1", - "@mastra/schema-compat": "1.2.7", - "@modelcontextprotocol/sdk": "^1.27.1", - "@sindresorhus/slugify": "^2.2.1", - "@standard-schema/spec": "^1.1.0", - "ajv": "^8.18.0", - "chat": "^4.23.0", - "dotenv": "^17.3.1", - "execa": "^9.6.1", - "gray-matter": "^4.0.3", - "hono": "^4.12.8", - "hono-openapi": "^1.3.0", - "ignore": "^7.0.5", - "js-tiktoken": "^1.0.21", - "json-schema": "^0.4.0", - "lru-cache": "^11.2.7", - "p-map": "^7.0.4", - "p-retry": "^7.1.1", - "picomatch": "^4.0.3", - "radash": "^12.1.1", - "tokenx": "^1.3.0", - "ws": "^8.19.0", - "xxhash-wasm": "^1.1.0" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/@ai-sdk/provider-utils-v5": { - "name": "@ai-sdk/provider-utils", - "version": "3.0.20", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.1", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/@ai-sdk/provider-utils-v5/node_modules/@ai-sdk/provider": { - "version": "2.0.1", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/@ai-sdk/provider-utils-v6": { - "name": "@ai-sdk/provider-utils", - "version": "4.0.0", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.0", - "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/@ai-sdk/provider-utils-v6/node_modules/@ai-sdk/provider": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/@ai-sdk/provider-v6": { - "name": "@ai-sdk/provider", - "version": "3.0.5", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/@mastra/schema-compat": { - "version": "1.2.7", - "license": "Apache-2.0", - "dependencies": { - "json-schema-to-zod": "^2.7.0", - "zod-from-json-schema": "^0.5.2", - "zod-from-json-schema-v3": "npm:zod-from-json-schema@^0.0.5", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/ajv": { - "version": "8.18.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/mastracode/node_modules/@mastra/core/node_modules/ajv/node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/mastracode/node_modules/@mastra/libsql": { - "version": "1.8.0", - "license": "Apache-2.0", - "dependencies": { - "@libsql/client": "^0.15.15" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "@mastra/core": ">=1.0.0-0 <2.0.0-0" - } - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client": { - "version": "0.15.15", - "license": "MIT", - "dependencies": { - "@libsql/core": "^0.15.14", - "@libsql/hrana-client": "^0.7.0", - "js-base64": "^3.7.5", - "libsql": "^0.5.22", - "promise-limit": "^2.7.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client/node_modules/@libsql/core": { - "version": "0.15.15", - "license": "MIT", - "dependencies": { - "js-base64": "^3.7.5" - } - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client/node_modules/@libsql/hrana-client": { - "version": "0.7.0", - "license": "MIT", - "dependencies": { - "@libsql/isomorphic-fetch": "^0.3.1", - "@libsql/isomorphic-ws": "^0.1.5", - "js-base64": "^3.7.5", - "node-fetch": "^3.3.2" - } - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client/node_modules/@libsql/hrana-client/node_modules/@libsql/isomorphic-fetch": { - "version": "0.3.1", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client/node_modules/libsql": { - "version": "0.5.29", - "cpu": [ - "x64", - "arm64", - "wasm32", - "arm" - ], - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "dependencies": { - "@neon-rs/load": "^0.0.4", - "detect-libc": "2.0.2" - }, - "optionalDependencies": { - "@libsql/darwin-arm64": "0.5.29", - "@libsql/darwin-x64": "0.5.29", - "@libsql/linux-arm-gnueabihf": "0.5.29", - "@libsql/linux-arm-musleabihf": "0.5.29", - "@libsql/linux-arm64-gnu": "0.5.29", - "@libsql/linux-arm64-musl": "0.5.29", - "@libsql/linux-x64-gnu": "0.5.29", - "@libsql/linux-x64-musl": "0.5.29", - "@libsql/win32-x64-msvc": "0.5.29" - } - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client/node_modules/libsql/node_modules/@libsql/win32-x64-msvc": { - "version": "0.5.29", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/mastracode/node_modules/@mastra/libsql/node_modules/@libsql/client/node_modules/libsql/node_modules/detect-libc": { - "version": "2.0.2", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/mastracode/node_modules/@mastra/mcp": { - "version": "1.4.2", - "license": "Apache-2.0", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^14.2.1", - "@modelcontextprotocol/sdk": "^1.27.1", - "exit-hook": "^5.1.0", - "fast-deep-equal": "^3.1.3", - "uuid": "^13.0.0" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "@mastra/core": ">=1.0.0-0 <2.0.0-0", - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/memory": { - "version": "1.15.0", - "license": "Apache-2.0", - "dependencies": { - "@mastra/schema-compat": "1.2.7", - "async-mutex": "^0.5.0", - "image-size": "^2.0.2", - "json-schema": "^0.4.0", - "lru-cache": "^11.2.7", - "probe-image-size": "^7.2.3", - "tokenx": "^1.3.0", - "xxhash-wasm": "^1.1.0" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "@mastra/core": ">=1.4.1-0 <2.0.0-0", - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/memory/node_modules/@mastra/schema-compat": { - "version": "1.2.7", - "license": "Apache-2.0", - "dependencies": { - "json-schema-to-zod": "^2.7.0", - "zod-from-json-schema": "^0.5.2", - "zod-from-json-schema-v3": "npm:zod-from-json-schema@^0.0.5", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/mastracode/node_modules/@mastra/pg": { - "version": "1.9.0", - "license": "Apache-2.0", - "dependencies": { - "async-mutex": "^0.5.0", - "pg": "^8.20.0", - "xxhash-wasm": "^1.1.0" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "@mastra/core": ">=1.4.0-0 <2.0.0-0" - } - }, - "node_modules/mastracode/node_modules/@mastra/stagehand": { - "version": "0.1.0", - "license": "Apache-2.0", - "dependencies": { - "@browserbasehq/stagehand": "^3.2.0" - }, - "engines": { - "node": ">=22.13.0" - }, - "peerDependencies": { - "@mastra/core": ">=1.22.0-0 <2.0.0-0", - "zod": "^3.25.0 || ^4.0.0" - } - }, "node_modules/matcher": { "version": "3.0.0", "license": "MIT", @@ -36642,7 +36333,9 @@ } }, "node_modules/meshoptimizer": { - "version": "1.0.1", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "dev": true, "license": "MIT" }, @@ -44712,7 +44405,9 @@ } }, "node_modules/three": { - "version": "0.183.2", + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", "license": "MIT" }, "node_modules/throttleit": { @@ -45568,7 +45263,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "6.0.2", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index c001a445..22068eab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentstack", - "version": "1.0.44", + "version": "1.0.45", "description": "Multi-agent frameworks and tools for building AI applications with Mastra.", "main": "index.js", "scripts": { @@ -14,6 +14,7 @@ "start:mastra": "mastra start", "start:next": "next start", "chrome:debug": "cmd /c start \"\" \"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --remote-debugging-port=9222 --user-data-dir=%TEMP%\\chrome-debug", + "brave:debug": "cmd /c start \"\" \"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe\" --remote-debugging-port=9222 --user-data-dir=%TEMP%\\chrome-debug", "generate": "dotenvx run -- npx auth@latest generate", "migrate": "dotenvx run -- npx auth@latest migrate", "lint": "eslint . --ext .ts,.tsx,.js,.jsx --concurrency=auto", @@ -27,9 +28,7 @@ "prettier:check": "dotenvx run -- prettier --check .", "prettier:write": "dotenvx run -- prettier --write .", "format": "npm run prettier:write", - "build-cli": "tsc -p tsconfig.cli.json", - "cli": "node dist/index.js", - "generate-index": "node dist/cli/index.js", + "convex": "npx convex dev --env-file .env.local", "code": "npx mastracode", "storybook": "storybook dev -p 6006", @@ -53,17 +52,18 @@ "node": ">=20.9.0" }, "dependencies": { - "@ai-sdk/google": "^3.0.63", - "@ai-sdk/google-vertex": "^4.0.109", + "@ai-sdk/google": "^3.0.64", + "@ai-sdk/google-vertex": "^4.0.112", "@ai-sdk/openai": "^3.0.53", "@ai-sdk/openai-compatible": "^2.0.41", "@ai-sdk/provider-utils": "^4.0.23", - "@ai-sdk/react": "^3.0.165", + "@ai-sdk/react": "^3.0.170", "@auth/agent": "^0.4.6", "@auth/agent-cli": "^0.4.5", "@better-auth/agent-auth": "^0.4.5", - "@better-auth/api-key": "^1.6.4", + "@better-auth/api-key": "^1.6.5", "@chat-adapter/discord": "^4.26.0", + "@chat-adapter/gchat": "^4.26.0", "@chat-adapter/github": "^4.26.0", "@chat-adapter/slack": "^4.26.0", "@chat-adapter/state-memory": "^4.26.0", @@ -106,7 +106,7 @@ "@mastra/stagehand": "^0.2.0", "@mastra/upstash": "^1.0.4", "@mastra/vectorize": "^1.0.2", - "@mastra/voice-google": "^0.12.0-beta.2", + "@mastra/voice-google": "^0.12.0", "@mastra/voice-openai": "^0.12.1", "@mcpc-tech/acp-ai-provider": "^0.3.2", "@next/bundle-analyzer": "^16.2.4", @@ -149,7 +149,7 @@ "@zernio/chat-sdk-adapter": "^0.2.3", "a2a-ai-provider": "^0.4.0-alpha.2", "adm-zip": "^0.5.17", - "ai": "^6.0.163", + "ai": "^6.0.168", "ai-sdk-ollama": "^3.8.3", "ai-sdk-provider-gemini-cli": "^2.0.1", "ai-sdk-provider-opencode-sdk": "^3.0.2", @@ -157,7 +157,7 @@ "arraystat": "^1.7.81", "axios": "^1.15.0", "axios-retry": "^4.5.0", - "better-auth": "^1.6.4", + "better-auth": "^1.6.5", "bottleneck": "^2.19.5", "chalk": "^5.6.2", "chart.js": "^4.5.1", @@ -201,7 +201,7 @@ "lenis": "^1.3.23", "lucide-react": "^1.8.0", "marked": "^18.0.0", - "mastracode": "^0.13.0", + "mastracode": "^0.14.0", "mathjs": "^15.2.0", "media-chrome": "^4.19.0", "module": "^2.0.0", @@ -253,7 +253,7 @@ "tailwindcss-animate": "^1.0.7", "technicalindicators": "^3.1.0", "tesseract.js": "^7.0.0", - "three": "^0.183.2", + "three": "^0.184.0", "tokenlens": "^1.3.1", "ts-morph": "^28.0.0", "tslab": "^1.0.22", @@ -272,6 +272,10 @@ "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/eslint-plugin-next": "^16.2.4", + "@storybook/addon-a11y": "^10.3.5", + "@storybook/addon-docs": "^10.3.5", + "@storybook/addon-onboarding": "^10.3.5", + "@storybook/nextjs": "^10.3.5", "@tailwindcss/postcss": "^4.2.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -287,7 +291,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/strip-comments": "^2.0.4", - "@types/three": "^0.183.1", + "@types/three": "^0.184.0", "@types/unist": "^3.0.3", "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", @@ -298,25 +302,21 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-storybook": "^10.3.5", "ink-testing-library": "^4.0.0", "mastra": "^1.6.0", "prettier": "^3.8.3", + "storybook": "^10.3.5", "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.2", "typescript-language-server": "^5.1.3", - "vitest": "^4.1.4", - "storybook": "^10.3.5", - "@storybook/nextjs": "^10.3.5", - "@storybook/addon-a11y": "^10.3.5", - "@storybook/addon-docs": "^10.3.5", - "@storybook/addon-onboarding": "^10.3.5", - "eslint-plugin-storybook": "^10.3.5" + "vitest": "^4.1.4" }, "overrides": { "jsondiffpatch": "0.7.3", - "ai": "^6.0.163", + "ai": "^6.0.168", "morgan": "^1.10.1", "js-yaml": "^4.1.1", "multer": "^2.1.1", @@ -339,7 +339,7 @@ "hono": "^4.12.14", "defu": "^6.1.7", "minimatch": "^10.2.5", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", "@typescript-eslint/eslint-plugin": "^8.58.2" diff --git a/src/cli/AGENTS.md b/src/cli/AGENTS.md deleted file mode 100644 index cd7b6863..00000000 --- a/src/cli/AGENTS.md +++ /dev/null @@ -1,57 +0,0 @@ - - -# CLI Directory (`/src/cli`) - -## Persona - -**Name:** `{cli_persona_name}` = "CLI & Operations Engineer" -**Role:** "I provide a fast, scriptable, auditable interface to core indexing & query workflows—never re‑implementing business logic, only invoking it predictably." -**Primary Goals:** - -1. Offer low-friction operational access (index, query, demo). -2. Surface clear usage/help output. -3. Defer all domain logic to workflows/services. -4. Fail fast with actionable errors & exit codes. - -**MUST:** - -- Keep commands thin: parameter parsing + workflow invocation. -- Provide `help` output listing ALL commands & examples. -- Load env early (`dotenv.config()` if required). -- Use consistent exit codes (0 success, non-zero failure). -- Validate required arguments before invoking workflows. - -**FORBIDDEN:** - -- Embedding vector DB or policy logic inline. -- Silent catch-and-continue on errors. -- Hardcoding secrets or API keys. -- Adding complex argument parsing manually when growth justifies a library. - -## Purpose - -Facilitates operational tasks (indexing corpus, querying governed RAG workflows, demos) without going through HTTP. Acts as a stable automation surface for CI scripts or developers. - -## Command Surface (Current) - -| Command | Workflow/Action | Description | Notes | -| ------------------------ | --------------------- | ---------------------------------------- | ----------------------- | -| `index` | `governed-rag-index` | Embeds & stores corpus vectors | Requires corpus present | -| `query ` | `governed-rag-answer` | Executes governed answer path | JWT passed inline | -| `demo` | multiple | Interactive loop for exploratory testing | Non-production utility | -| `help` | n/a | Prints usage details | Default fallback | - -## Execution Flow - -1. Parse `process.argv` → derive command + args. -2. Validate arg count & format. -3. Map command → workflow or helper. -4. Await result → pretty print (JSON or streamed tokens). -5. Exit with code `0` on success, else log error & `process.exit(1)`. - -## Change Log - -| Version | Date (UTC) | Change | -| ------- | ---------- | ------------------------------------------------ | -| 1.1.0 | 2025-10-08 | Verified content accuracy and updated metadata. | -| 1.0.0 | 2025-09-24 | Standardized CLI documentation; legacy preserved | diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index 3fd95319..00000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { mastra } from '../mastra/index' -import { - logWorkflowStart, - logWorkflowEnd, - logError, - logProgress, - log, -} from '../mastra/config/logger' -import { tierManagementService } from '../mastra/services/tier-management-service' -import type { SubscriptionTier } from '../mastra/config/role-hierarchy' -import * as dotenv from 'dotenv' -import * as fs from 'node:fs/promises' -import * as path from 'node:path' - -dotenv.config() - -async function indexDocuments() { - log.info('🚀 Starting document indexing for Governed RAG') - - // Get tenant and tier from environment or use defaults - const tenant = process.env.TENANT ?? 'acme' - const tier: SubscriptionTier = - (process.env.TIER as SubscriptionTier) ?? 'free' - - log.info(`Tenant: ${tenant}, Tier: ${tier}`) - - // Validate tier access for document indexing - const tierValidation = await tierManagementService.validateTierAccess( - tenant, - tier, - 'document' - ) - - if (!tierValidation.allowed) { - log.error(`❌ Tier validation failed: ${tierValidation.reason}`) - if (tierValidation.upgradeRequired) { - log.info('💡 Upgrade to a higher tier to index more documents') - } - return - } - - const usage = tierValidation.currentUsage - if (usage) { - log.info(`Current usage: ${usage.documentsIndexed} documents indexed`) - } - - const sampleDocs: any[] = [ - { - filePath: path.join(__dirname, '../../corpus/finance-policy.md'), - docId: 'finance-policy-001', - classification: 'internal' as const, - allowedRoles: ['finance.viewer', 'finance.admin', 'admin'], - tenant: process.env.TENANT ?? 'acme', - source: 'Finance Department Policy Manual', - }, - { - filePath: path.join( - __dirname, - '../../corpus/engineering-handbook.md' - ), - docId: 'eng-handbook-001', - classification: 'public' as const, - allowedRoles: ['engineering.viewer', 'engineering.admin', 'admin'], - tenant: process.env.TENANT ?? 'acme', - source: 'Engineering Team Handbook', - }, - { - filePath: path.join(__dirname, '../../corpus/hr-confidential.md'), - docId: 'hr-conf-001', - classification: 'confidential' as const, - allowedRoles: ['hr.admin', 'admin'], - tenant: process.env.TENANT ?? 'acme', - source: 'HR Confidential Documents', - }, - ] - - const validDocs: any[] = [] - for (const doc of sampleDocs) { - try { - await fs.access(doc.filePath) - log.info(`✅ Found: ${path.basename(doc.filePath)}`) - validDocs.push(doc) - } catch { - log.warn(`⚠️ Skipping: ${path.basename(doc.filePath)} (not found)`) - } - } - - if (validDocs.length === 0) { - log.warn( - '❌ No documents found to index. Please add documents to the corpus/ directory.' - ) - return - } - - try { - const startTime = Date.now() - logWorkflowStart('governed-rag-index', { documents: validDocs }) - - const workflow = mastra.listWorkflows()['governed-rag-index'] - const run = await workflow.createRun() - const result = await run.start({ - inputData: { documents: validDocs }, - }) - - log.info('📄 Document Details:') - logWorkflowEnd( - 'governed-rag-index', - result as any, - Date.now() - startTime - ) - // Log document results - const resultData = (result as any).result ?? result - if (resultData.documents) { - let successCount = 0 - resultData.documents.forEach((doc: any) => { - if (doc.status === 'success') { - log.info(`✅ ${doc.docId}: ${doc.chunks} chunks indexed`) - successCount++ - } else { - log.error(`❌ ${doc.docId}: ${doc.error}`) - } - }) - - // Track usage after successful indexing - if (successCount > 0) { - await tierManagementService.incrementUsage( - tenant, - tier, - 'document', - successCount - ) - log.info(`Updated usage counter: +${successCount} documents`) - } - } - logProgress( - `Indexing complete`, - resultData.indexed, - resultData.indexed + resultData.failed - ) - } catch (error) { - logError('index-documents', error) - } -} - -async function queryRAG(jwt: string, question: string) { - log.info('🔍 Querying Governed RAG') - - // Get tenant and tier from environment - const tenant = process.env.TENANT ?? 'acme' - const tier: SubscriptionTier = - (process.env.TIER as SubscriptionTier) ?? 'free' - - // Validate API request quota - const tierValidation = await tierManagementService.validateTierAccess( - tenant, - tier, - 'api_request' - ) - - if (!tierValidation.allowed) { - log.error(`❌ API request limit exceeded: ${tierValidation.reason}`) - return - } - - try { - const startTime = Date.now() - logWorkflowStart('governed-rag-answer', { jwt, question }) - - const workflow = mastra.listWorkflows()['governed-rag-answer'] - const run = await workflow.createRun() - const result = await run.start({ - inputData: { jwt, question }, - }) - - if (result.status === 'success') { - const resultData = (result as any).result ?? result - logWorkflowEnd( - 'governed-rag-answer', - resultData, - Date.now() - startTime - ) - - // Track API request usage - await tierManagementService.incrementUsage( - tenant, - tier, - 'api_request', - 1 - ) - - log.info('✅ Answer generated successfully!') - log.info(`📝 Answer: ${resultData.answer}`) - - if (resultData.citations?.length > 0) { - log.info('📚 Citations:') - resultData.citations.forEach((citation: any) => { - log.info( - `- ${citation.docId}${citation.source ? ` (${citation.source})` : ''}` - ) - }) - } - } else { - logError('workflow-execution', new Error('Query failed'), result) - } - } catch (error) { - logError('query-rag', error) - } -} - -async function main() { - const args = process.argv.slice(2) - const command = args[0] - - if (!command || command === 'help') { - log.info( - 'Governed RAG CLI\n\nCommands:\n index - Index sample documents\n query - Query with JWT auth\n usage - Show current usage stats\n demo - Run interactive demo\n help - Show this help message\n\nEnvironment Variables:\n TENANT - Tenant identifier (default: acme)\n TIER - Subscription tier: free, pro, enterprise (default: free)\n\nExamples:\n npm run cli index\n npm run cli query "eyJ..." "What is our finance policy?"\n TIER=pro npm run cli index\n npm run cli usage\n npm run cli demo' - ) - } - - switch (command) { - case 'index': - await indexDocuments() - break - - case 'query': { - const jwt = args[1] - const question = args.slice(2).join(' ') - - if (!jwt || !question) { - log.error('❌ Usage: npm run cli query ') - return - } - await queryRAG(jwt, question) - break - } - - case 'usage': { - const tenant = process.env.TENANT ?? 'acme' - const tier: SubscriptionTier = - (process.env.TIER as SubscriptionTier) ?? 'free' - - const usage = await tierManagementService.getUsageStats( - tenant, - tier - ) - const percentages = tierManagementService.getUsagePercentage(usage) - const nearLimit = tierManagementService.isNearQuotaLimit(usage) - - log.info('📊 Current Usage Statistics') - log.info(`Tenant: ${tenant}`) - log.info(`Tier: ${tier}`) - log.info( - `\nDocuments: ${usage.documentsIndexed} (${percentages.documents.toFixed(1)}% of quota)` - ) - log.info( - `API Requests Today: ${usage.apiRequestsToday} (${percentages.apiRequests.toFixed(1)}% of quota)` - ) - log.info( - `Total Users: ${usage.totalUsers} (${percentages.users.toFixed(1)}% of quota)` - ) - log.info(`Last Reset: ${usage.lastReset.toISOString()}`) - - if (nearLimit) { - log.warn('⚠️ Warning: Approaching quota limits') - } - break - } - - case 'demo': - log.info( - '🎮 Interactive Demo Mode\nThis would launch an interactive demo (to be implemented)' - ) - break - default: - log.error( - `❌ Unknown command: ${command}\nRun "npm run cli help" for usage information` - ) - } -} - -if (require.main === module) { - main().catch((error: unknown) => { - log.error(`Fatal error: ${error}`) - process.exit(1) - }) -} diff --git a/src/mastra/agents/knowledgeIndexingAgent.ts b/src/mastra/agents/knowledgeIndexingAgent.ts index da820ee1..16404d0d 100644 --- a/src/mastra/agents/knowledgeIndexingAgent.ts +++ b/src/mastra/agents/knowledgeIndexingAgent.ts @@ -8,7 +8,8 @@ import { import { InternalSpans } from '@mastra/core/observability' import { USER_ID_CONTEXT_KEY, type AgentRequestContext } from './request-context' import { libsqlgraphQueryTool, LibsqlMemory, libsqlQueryTool } from '../config/libsql' - +import { agentFsWorkspace } from '../workspaces' +import { LIBSQL_PROMPT } from '@mastra/libsql' const INDEX_NAME_CONTEXT_KEY = 'indexName' as const const CHUNKING_STRATEGY_CONTEXT_KEY = 'chunkingStrategy' as const @@ -71,6 +72,7 @@ User: ${userId} | Index: ${indexName} | Strategy: ${chunkingStrategy} ## Rules - **Tool Efficiency**: Do NOT use the same tool repetitively or back-to-back for the same query. +{$LIBSQL_PROMPT} ` }, @@ -82,6 +84,10 @@ User: ${userId} | Index: ${indexName} | Strategy: ${chunkingStrategy} internal: InternalSpans.AGENT, }, }, + workspace: agentFsWorkspace, + scorers: {}, + workflows: {}, + maxRetries: 5, //outputProcessors: [new TokenLimiterProcessor(1048576)], }) diff --git a/src/mastra/agents/learningExtractionAgent.ts b/src/mastra/agents/learningExtractionAgent.ts index db4cd532..8243f17f 100644 --- a/src/mastra/agents/learningExtractionAgent.ts +++ b/src/mastra/agents/learningExtractionAgent.ts @@ -10,6 +10,7 @@ import { type AgentRequestContext, } from './request-context' import { LibsqlMemory } from '../config/libsql' +import { agentFsWorkspace } from '../workspaces' export type LearningExtractionAgentContext = AgentRequestContext<{ researchPhase?: string @@ -73,5 +74,6 @@ Extract the single most important learning and create one relevant follow-up que }, workflows: {}, maxRetries: 5, + workspace: agentFsWorkspace, // outputProcessors: [new TokenLimiterProcessor(128000)], }) diff --git a/src/mastra/agents/noteTakerAgent.ts b/src/mastra/agents/noteTakerAgent.ts index c4f020c8..e1a92064 100644 --- a/src/mastra/agents/noteTakerAgent.ts +++ b/src/mastra/agents/noteTakerAgent.ts @@ -19,5 +19,5 @@ export const noteTakerAgent = new Agent({ internal: InternalSpans.ALL, }, }, - voice: new GoogleVoice(), // Add OpenAI voice provider with default configuration + //voice: new GoogleVoice(), // Add OpenAI voice provider with default configuration }) diff --git a/src/mastra/agents/package-publisher.ts b/src/mastra/agents/package-publisher.ts index 5926bea1..24a37530 100644 --- a/src/mastra/agents/package-publisher.ts +++ b/src/mastra/agents/package-publisher.ts @@ -1,6 +1,6 @@ import { Agent } from '@mastra/core/agent' -import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import type { GoogleLanguageModelOptions } from '@ai-sdk/google' //import { TokenLimiterProcessor } from '@mastra/core/processors' import { getLanguageFromContext, @@ -9,6 +9,7 @@ import { } from './request-context' import { InternalSpans } from '@mastra/core/observability' import { LibsqlMemory } from '../config/libsql' +import { agentFsWorkspace } from '../workspaces' export type PackagePublisherRuntimeContext = AgentRequestContext @@ -155,7 +156,7 @@ export const danePackagePublisher = new Agent({ includeThoughts: true, thinkingBudget: -1, }, - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -168,5 +169,6 @@ export const danePackagePublisher = new Agent({ }, }, scorers: {}, + workspace: agentFsWorkspace, //outputProcessors: [new TokenLimiterProcessor(128000)], }) diff --git a/src/mastra/agents/projectManagementAgent.ts b/src/mastra/agents/projectManagementAgent.ts index 8af43250..e5a81656 100644 --- a/src/mastra/agents/projectManagementAgent.ts +++ b/src/mastra/agents/projectManagementAgent.ts @@ -12,6 +12,7 @@ import { } from './request-context' import { LibsqlMemory } from '../config/libsql' import { createSupervisorAgentPatternScorer } from '../scorers/supervisor-scorers' +import { agentFsWorkspace } from '../workspaces' log.info('Initializing Project Management Agent...') @@ -267,6 +268,7 @@ You are a Professional Project Manager. Your expertise covers project planning, internal: InternalSpans.ALL, }, }, + workspace: agentFsWorkspace, defaultOptions: { delegation: { onDelegationStart: async context => { diff --git a/src/mastra/agents/recharts.ts b/src/mastra/agents/recharts.ts index bf41f9d4..72729182 100644 --- a/src/mastra/agents/recharts.ts +++ b/src/mastra/agents/recharts.ts @@ -1,4 +1,4 @@ -import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import type { GoogleLanguageModelOptions } from '@ai-sdk/google' import { Agent } from '@mastra/core/agent' import { InternalSpans } from '@mastra/core/observability' import { @@ -32,6 +32,7 @@ import { } from './request-context' import { fetchTool } from '../tools/fetch.tool' import { libsqlgraphQueryTool, LibsqlMemory, libsqlQueryTool } from '../config/libsql' +import { agentFsWorkspace } from '../workspaces' export type ChartRuntimeContext = AgentRequestContext<{ chartStyle: 'detailed' | 'concise' @@ -135,7 +136,7 @@ You are a Financial Data Visualization Specialist focused on recommending optima includeThoughts: true, thinkingBudget: -1, }, - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -193,7 +194,7 @@ You are a Financial Data Processing Specialist that transforms raw API data into includeThoughts: true, thinkingBudget: -1, }, - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -253,7 +254,7 @@ You are a Senior React Developer specializing in Recharts financial visualizatio includeThoughts: true, thinkingBudget: -1, }, - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -318,7 +319,7 @@ You are the Financial Chart Supervisor, orchestrating the complete chart creatio includeThoughts: true, thinkingBudget: -1, }, - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -331,6 +332,7 @@ You are the Financial Chart Supervisor, orchestrating the complete chart creatio }, }, scorers: {}, + workspace: agentFsWorkspace, inputProcessors: [ new UnicodeNormalizer({ stripControlChars: false, diff --git a/src/mastra/agents/reportAgent.ts b/src/mastra/agents/reportAgent.ts index 833f2ba3..dfd60fd9 100644 --- a/src/mastra/agents/reportAgent.ts +++ b/src/mastra/agents/reportAgent.ts @@ -9,6 +9,7 @@ import { type AgentRequestContext, } from './request-context' import { LibsqlMemory } from '../config/libsql' +import { agentFsWorkspace } from '../workspaces' export type ReportRuntimeContext = AgentRequestContext log.info('Initializing Report Agent...') @@ -75,6 +76,7 @@ export const reportAgent = new Agent({ scorers: {}, workflows: {}, maxRetries: 5, + workspace: agentFsWorkspace, }) // --- IGNORE --- diff --git a/src/mastra/agents/researchAgent.ts b/src/mastra/agents/researchAgent.ts index e5f33ab6..2862d927 100644 --- a/src/mastra/agents/researchAgent.ts +++ b/src/mastra/agents/researchAgent.ts @@ -1,6 +1,6 @@ import { libsqlQueryTool, libsqlgraphQueryTool } from './../config/libsql'; import { libsqlChunker, mdocumentChunker } from './../tools/document-chunking.tool'; -import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import type { GoogleLanguageModelOptions } from '@ai-sdk/google' import type { Message, Thread } from 'chat' import { Agent } from '@mastra/core/agent' import { log } from '../config/logger' @@ -20,13 +20,29 @@ import { googleNewsLiteTool, googleTrendsTool, } from '../tools/serpapi-news-trends.tool' +import { googleImagesTool } from '../tools/serpapi-images.tool' +import { googleLocalTool, googleMapsReviewsTool } from '../tools/serpapi-local-maps.tool' +import { googleSearchTool } from '../tools/serpapi-search.tool' import { stooqStockQuotesTool } from '../tools/stooq-stock-market-data.tool' import { yahooFinanceStockQuotesTool } from '../tools/yahoo-finance-stock.tool' +import { + candlestickPatternTool, + fibonacciTool, + heikinAshiTool, + ichimokuCloudTool, + marketSummaryTool, + momentumAnalysisTool, + pivotPointsTool, + statisticalAnalysisTool, + technicalAnalysisTool, + trendAnalysisTool, + volatilityAnalysisTool, + volumeAnalysisTool, +} from '../tools/technical-analysis.tool' // Scorers import { InternalSpans } from '@mastra/core/observability' import type { ChannelHandlers } from '@mastra/core/channels' -import * as workspaces from '../workspaces' import { getLanguageFromContext, getRoleFromContext, @@ -36,7 +52,6 @@ import { researchArxivDownloadWorkflow } from '../workflows/research/research-ar import { researchArxivSearchWorkflow } from '../workflows/research/research-arxiv-search.workflow' import { LibsqlMemory } from '../config/libsql' import { listRepositories } from '../tools/github'; -import * as browsers from '../browsers'; import { createGitHubAdapter } from '@chat-adapter/github' import { createDiscordAdapter } from '@chat-adapter/discord' import { google } from '../config/google' @@ -45,6 +60,12 @@ import { ToolSearchProcessor, //TokenLimiter } from '@mastra/core/processors' +import { googleAiOverviewTool } from '../tools/serpapi-search.tool'; +import { amazonSearchTool, ebaySearchTool, homeDepotSearchTool, walmartSearchTool } from '../tools/serpapi-shopping.tool'; +import { agentFsWorkspace } from '../workspaces'; +import { agentBrowser } from '../browsers' +import { createMemoryState } from "@chat-adapter/state-memory"; + //const github = createGitHubAdapter({ // //appId: process.env.GITHUB_APP_ID!, // //privateKey: process.env.GITHUB_PRIVATE_KEY!, @@ -197,20 +218,6 @@ const researchChannelHandlers: ChannelHandlers = { }, } -/** - * Returns the shared workspace used by the research agent. - */ -function getResearchAgentWorkspace() { - return workspaces.mainWorkspace -} - -/** - * Returns the deterministic browser configured for research verification. - */ -function getResearchAgentBrowser() { - return browsers.agentBrowser -} - type ResearchPhase = 'initial' | 'followup' | 'validation' const RESEARCH_PHASE_CONTEXT_KEY = 'researchPhase' as const @@ -248,8 +255,26 @@ const researchAgentTools = { binanceSpotMarketDataTool, coinbaseExchangeMarketDataTool, discordWebhookTool, + googleImagesTool, + googleLocalTool, + googleMapsReviewsTool, stooqStockQuotesTool, yahooFinanceStockQuotesTool, + ichimokuCloudTool, + fibonacciTool, + pivotPointsTool, + trendAnalysisTool, + momentumAnalysisTool, + volatilityAnalysisTool, + volumeAnalysisTool, + statisticalAnalysisTool, + heikinAshiTool, + marketSummaryTool, + candlestickPatternTool, + technicalAnalysisTool, + amazonSearchTool, walmartSearchTool, ebaySearchTool, homeDepotSearchTool, + googleSearchTool, + googleAiOverviewTool, } /** @@ -284,13 +309,17 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} ## Tool Selection Guide - **Web**: Prefer 'fetchTool' for reliable URL fetch/search to markdown. - **Live browser verification**: Use the attached browser only when page state, interaction results, or live UI evidence materially matters more than static fetch output. -- **News/Trends**: 'googleNewsLiteTool', 'googleTrendsTool', 'googleFinanceTool'. +- **News/Trends**: 'googleNewsLiteTool', 'googleTrendsTool', 'googleFinanceTool'. 'googleSearchTool', 'googleAiOverviewTool', +- **Places/Business**: 'googleLocalTool' for nearby business discovery and 'googleMapsReviewsTool' for place review analysis. +- **Visual**: 'googleImagesTool' for structured image discovery and source lookups. - **Academic**: 'googleScholarTool'. - **Financial**: Use 'polygon*' for stocks/crypto. - **Financial**: Use 'polygon*' for stocks/crypto when you need paid/commercial feeds; use 'binanceSpotMarketDataTool' for free crypto spot data and batch lookups of 1-10 symbols; use 'coinbaseExchangeMarketDataTool', 'stooqStockQuotesTool', and 'yahooFinanceStockQuotesTool' for free public market data. +- **Technical Analysis**: use 'ichimokuCloudTool', 'fibonacciTool', 'pivotPointsTool', 'trendAnalysisTool', 'momentumAnalysisTool', 'volatilityAnalysisTool', 'volumeAnalysisTool', 'statisticalAnalysisTool', 'heikinAshiTool', 'marketSummaryTool', 'candlestickPatternTool', and 'technicalAnalysisTool' for chart pattern and indicator analysis. - **Internal**: 'libsqlChunker' for embedding any information, 'libsqlQueryTool' for querying embedded knowledge. 'libsqlgraphQueryTool' for complex relational queries. - **Processing**: use workspace document tools for PDFs, Markdown, and any other filetype in the workspace; - **Discord**: use 'discordWebhookTool' to post short notifications or summaries to the configured Discord webhook URL. +- **Stores**: 'amazonSearchTool', 'walmartSearchTool', 'ebaySearchTool', 'homeDepotSearchTool', for product research. ## Rules - **Efficiency**: No repetitive or back-to-back tool calls for the same query. @@ -305,7 +334,7 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} includeThoughts: true, thinkingLevel: 'medium', }, - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -313,10 +342,10 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} const role = getRoleFromContext(requestContext) if (role === 'admin') { - return google.chat('gemini-3.1-pro-preview') + return 'kilo/x-ai/grok-code-fast-1:optimized:free' } - return google.chat('gemini-3.1-flash-lite-preview') + return 'opencode/minimax-m2.5-free' }, tools: researchAgentTools, workflows: { researchArxivDownloadWorkflow, researchArxivSearchWorkflow }, @@ -338,7 +367,7 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} tools: researchAgentTools, search: { topK: 5 }, }), - new TokenLimiter(64000), + //new TokenLimiter(64000), ], outputProcessors: [ // new TokenLimiterProcessor(128000), @@ -348,8 +377,8 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} // emitOnNonText: true, // }), ], - workspace: getResearchAgentWorkspace(), - browser: getResearchAgentBrowser(), + workspace: agentFsWorkspace, + browser: agentBrowser, channels: { inlineLinks: ['*'], inlineMedia: ['image/*', 'video/*', 'audio/*'], @@ -357,9 +386,35 @@ Role: ${role} | Lang: ${language} | Phase: ${researchPhase} threadContext: { maxMessages: 15, }, + state: createMemoryState(), handlers: researchChannelHandlers, }, - // defaultOptions: { - // autoResumeSuspendedTools: true, - // }, + //voice: new GoogleVoice(), // Add OpenAI voice provider with default configuration + defaultOptions: { + autoResumeSuspendedTools: true, + //includeRawChunks: true, + modelSettings: { + temperature: 0.2, + //maxOutputTokens: 64000, + topK: 40, + topP: 0.95, + //stopSequences: ['\n\n'], + maxRetries: 5, + }, + providerOptions: { + google: { + responseModalities: ['TEXT', 'IMAGE'], + thinkingConfig: { + includeThoughts: true, + thinkingLevel: 'medium', + }, + //cachedContent: "Use cached content when available to reduce latency and costs, but ensure freshness for time-sensitive queries. Prefer cached data for static information and use real-time fetches for news, trends, and financial data.", + //streamFunctionCallArguments: true, + mediaResolution: "MEDIA_RESOLUTION_MEDIUM", + threshold: 'OFF', // Set to 'OFF' to disable thresholding and allow all tool calls + //labels: "research-agent", + //serviceTier: 'flex', + } satisfies GoogleLanguageModelOptions, + }, + }, }) diff --git a/src/mastra/agents/scriptWriterAgent.ts b/src/mastra/agents/scriptWriterAgent.ts index f20fca19..86e74511 100644 --- a/src/mastra/agents/scriptWriterAgent.ts +++ b/src/mastra/agents/scriptWriterAgent.ts @@ -1,4 +1,4 @@ -import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import type { GoogleLanguageModelOptions } from '@ai-sdk/google' import { google } from '@ai-sdk/google' import { Agent } from '@mastra/core/agent' import { InternalSpans } from '@mastra/core/observability' @@ -51,7 +51,7 @@ User: ${userTier} | Lang: ${language} thinkingBudget: -1, }, responseModalities: ['TEXT'], - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, diff --git a/src/mastra/agents/seoAgent.ts b/src/mastra/agents/seoAgent.ts index 930115db..aa928898 100644 --- a/src/mastra/agents/seoAgent.ts +++ b/src/mastra/agents/seoAgent.ts @@ -12,6 +12,7 @@ import { } from './request-context' import { LibsqlMemory } from '../config/libsql' import { createSupervisorAgentPatternScorer } from '../scorers/supervisor-scorers' +import { agentFsWorkspace } from '../workspaces' log.info('Initializing SEO Agent...') @@ -223,6 +224,7 @@ You are an SEO Specialist and Content Optimizer. Your expertise covers all aspec internal: InternalSpans.ALL, }, }, + workspace: agentFsWorkspace, defaultOptions: { delegation: { onDelegationStart: async context => { diff --git a/src/mastra/agents/supervisor-agent.ts b/src/mastra/agents/supervisor-agent.ts index b347a521..b5acc92a 100644 --- a/src/mastra/agents/supervisor-agent.ts +++ b/src/mastra/agents/supervisor-agent.ts @@ -20,6 +20,7 @@ import { import { embed } from 'ai'; import { ModelRouterEmbeddingModel } from '@mastra/core/llm'; import { createSupervisorAgentPatternScorer } from '../scorers/supervisor-scorers' +import { GoogleVoice } from '@mastra/voice-google' const workspace = new Workspace({ id: 'supervisor-workspace', @@ -196,8 +197,7 @@ Operating rules: } }, model: 'google/gemma-4-31b-it:free', - tools: {libsqlgraphQueryTool, libsqlQueryTool, - }, + tools: {}, agents: { researchAgent, browserAgent, @@ -366,4 +366,5 @@ Operating rules: suppressFeedback: false, // Show feedback from the scorer }, }, + //voice: new GoogleVoice(), // Add OpenAI voice provider with default configuration }) diff --git a/src/mastra/agents/weather-agent.ts b/src/mastra/agents/weather-agent.ts index 9bee10e0..62761583 100644 --- a/src/mastra/agents/weather-agent.ts +++ b/src/mastra/agents/weather-agent.ts @@ -1,9 +1,9 @@ import { Agent } from '@mastra/core/agent' import { libsqlChunker,} from '../tools/document-chunking.tool' import { weatherTool } from '../tools/weather-tool' -import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google' +import type { GoogleLanguageModelOptions } from '@ai-sdk/google' import { InternalSpans } from '@mastra/core/observability' -import { mainWorkspace } from '../workspaces' +import { agentFsWorkspace, mainWorkspace } from '../workspaces' import type { AgentRequestContext } from './request-context' import { fetchTool } from '../tools/fetch.tool' import { libsqlgraphQueryTool, LibsqlMemory, libsqlQueryTool } from '../config/libsql' @@ -45,7 +45,7 @@ export const weatherAgent = new Agent({ thinkingBudget: -1, }, cachedContent: "Location data cached for weather queries", - } satisfies GoogleGenerativeAIProviderOptions, + } satisfies GoogleLanguageModelOptions, }, } }, @@ -68,7 +68,7 @@ export const weatherAgent = new Agent({ }, }, maxRetries: 5, - workspace: mainWorkspace, + workspace: agentFsWorkspace, // defaultOptions: { // autoResumeSuspendedTools: true, diff --git a/src/mastra/agents/webResearchAgent.ts b/src/mastra/agents/webResearchAgent.ts index bbf00c4a..e28dd49d 100644 --- a/src/mastra/agents/webResearchAgent.ts +++ b/src/mastra/agents/webResearchAgent.ts @@ -10,6 +10,7 @@ import { InternalSpans } from '@mastra/core/observability' import type { AgentRequestContext } from './request-context' import { fetchTool } from '../tools' import { LibsqlMemory } from '../config/libsql'; +import { agentFsWorkspace } from '../workspaces' export type WebResearchRuntimeContext = AgentRequestContext<{ researchPhase?: string @@ -104,6 +105,7 @@ Provide structured results with: internal: InternalSpans.ALL, }, }, + workspace: agentFsWorkspace, outputProcessors: [ // new BatchPartsProcessor({ diff --git a/src/mastra/browsers.ts b/src/mastra/browsers.ts index e248a7e7..54e8e8f1 100644 --- a/src/mastra/browsers.ts +++ b/src/mastra/browsers.ts @@ -8,7 +8,7 @@ type BrowserConnectionMode = 'browserbase' | 'cdp' type ScreencastFormat = 'jpeg' | 'png' type StagehandEnvironment = 'LOCAL' | 'BROWSERBASE' -const DEFAULT_CHROME_CDP_URL = 'http://127.0.0.1:9222' +const DEFAULT_CHROME_CDP_URL = 'http://localhost:9222' const DEFAULT_BROWSER_TIMEOUT_MS = 30000 const DEFAULT_STAGEHAND_DOM_SETTLE_TIMEOUT_MS = 5000 const DEFAULT_STAGEHAND_MODEL = 'google/gemini-3.1-flash-lite-preview' @@ -327,7 +327,7 @@ export const browserRuntimeConfig = { } export const agentBrowser = new AgentBrowser({ - headless: agentBrowserHeadless, + headless: true, viewport: sharedViewport, timeout: agentBrowserTimeoutMs, cdpUrl: () => chromeCdpUrl, @@ -337,7 +337,7 @@ export const agentBrowser = new AgentBrowser({ }) export const stagehandBrowser = new StagehandBrowser({ - headless: stagehandHeadless, + headless: true, model: readStringEnv(['STAGEHAND_MODEL'], DEFAULT_STAGEHAND_MODEL) ?? DEFAULT_STAGEHAND_MODEL, diff --git a/src/mastra/config/libsql.ts b/src/mastra/config/libsql.ts index f6b4deb8..9cb33524 100644 --- a/src/mastra/config/libsql.ts +++ b/src/mastra/config/libsql.ts @@ -47,7 +47,7 @@ await libsqlvector.createIndex({ export const LibsqlMemory = new Memory({ storage: libsqlstorage, vector: libsqlvector, - embedder: LIBSQL_EMBEDDING_MODEL, + embedder: fastembed, embedderOptions: { telemetry: { request: { @@ -63,10 +63,10 @@ export const LibsqlMemory = new Memory({ readOnly: false, observationalMemory: { enabled: true, - scope: 'resource', // 'resource' | 'thread' + scope: 'thread', // 'resource' | 'thread' model: 'google/gemini-2.5-flash', - retrieval: { vector: true, scope: 'resource' }, - shareTokenBudget: true, // Don't share token budget between observation and reflection to preserve context + retrieval: { vector: true, scope: 'thread' }, + shareTokenBudget: false, // Don't share token budget between observation and reflection to preserve context observation: { instruction: 'You are an assistant that observes and remembers important information from the conversation. Pay attention to details, context, and any information that might be useful for future reference.', messageTokens: 50_000, @@ -105,14 +105,14 @@ export const LibsqlMemory = new Memory({ before: parseInt(process.env.SEMANTIC_RANGE_BEFORE ?? '3'), after: parseInt(process.env.SEMANTIC_RANGE_AFTER ?? '2'), }, - scope: 'thread', // 'resource' | 'thread' + scope: 'resource', // 'resource' | 'thread' threshold: 0.75, // Similarity threshold for semantic recall indexName: LIBSQL_INDEX_NAME, // Index name for semantic recall }, // Enhanced working memory with supported template workingMemory: { enabled: true, - scope: 'thread', // 'resource' | 'thread' + scope: 'resource', // 'resource' | 'thread' version: 'vnext', template: ` # User Context @@ -160,7 +160,7 @@ export const libsqlgraphQueryTool = createGraphRAGTool({ vectorStore: libsqlvector, vectorStoreName: 'libsql-vector', indexName: LIBSQL_INDEX_NAME, - model: LIBSQL_EMBEDDING_MODEL, + model: fastembed, // Supported graph options (updated for 768 dimensions) graphOptions: { dimension: LIBSQL_EMBEDDING_DIMENSION, // FastEmbed base dimension (768) @@ -183,7 +183,7 @@ export const libsqlQueryTool = createVectorQueryTool({ vectorStore: libsqlvector, vectorStoreName: 'libsql-vector', indexName: LIBSQL_INDEX_NAME, - model: LIBSQL_EMBEDDING_MODEL, + model: fastembed, includeVectors: true, // Advanced filtering enableFilter: true, diff --git a/src/mastra/index.ts b/src/mastra/index.ts index ef0dcb74..19e7b054 100644 --- a/src/mastra/index.ts +++ b/src/mastra/index.ts @@ -184,16 +184,17 @@ import { import { mainHarness } from './harness' import { supervisorAgent } from './agents/supervisor-agent' import { mastraAuth } from './auth' -import { agentFsWorkspace } from './workspaces' +import { agentFsWorkspace, daytonaSandbox, mainWorkspace } from './workspaces' import { MastraEditor } from '@mastra/editor' //import { MastraCompositeStore } from '@mastra/core/storage' import { ArcadeToolProvider } from '@mastra/editor/arcade' import { ComposioToolProvider } from '@mastra/editor/composio' +import { GoogleVoice } from '@mastra/voice-google' +import main from '@/lib/auth-dev' //import { PosthogExporter } from '@mastra/posthog' export const mastra = new Mastra({ workspace: agentFsWorkspace, - workflows: { weatherWorkflow, contentStudioWorkflow, @@ -339,13 +340,9 @@ export const mastra = new Mastra({ apiKey: process.env.ARCADE_API_KEY!, }), }, - sandboxes: { - // Optional: restrict certain modules or APIs for security - }, - filesystems: { - + //sandboxes:{[daytonaSandbox.id]: daytonaSandbox}, + //filesystems: { [s3FilesystemProvider.id]: s3FilesystemProvider }, // Optional: configure storage limits, allowed file types, etc. - }, // Optional: add a custom toolbar with specific tools or actions } ), @@ -360,7 +357,11 @@ export const mastra = new Mastra({ notes: notesMCP, codingA2A: codingA2AMcpServer, }, - + tts: { + // Optional: Add text-to-speech capabilities to mastra for voice-enabled agents + // google: new GoogleVoice(), + }, + // Example of agent-specific configuration using instructions storage: libsqlstorage, vectors: { libsqlvector }, logger: log, diff --git a/src/mastra/mcp/resources.ts b/src/mastra/mcp/resources.ts index 6b6be1f8..2a7f2665 100644 --- a/src/mastra/mcp/resources.ts +++ b/src/mastra/mcp/resources.ts @@ -28,7 +28,14 @@ const listNoteFiles = async (): Promise => { } } -function isNodeErrorWithCode(error: unknown): error is NodeJS.ErrnoException { +interface ErrnoException extends Error { + code?: string + errno?: number + syscall?: string + path?: string +} + +function isNodeErrorWithCode(error: unknown): error is ErrnoException { return ( typeof error === 'object' && error !== null && @@ -37,17 +44,6 @@ function isNodeErrorWithCode(error: unknown): error is NodeJS.ErrnoException { ) } -declare global { - namespace NodeJS { - interface ErrnoException extends Error { - code?: string - errno?: number - syscall?: string - path?: string - } - } -} - const readNoteFile = async (uri: string): Promise => { const title = uri.replace('notes://', '') const notePath = path.join(NOTES_DIR, `${title}.md`) diff --git a/src/mastra/public/workspace/fpv-drone.md b/src/mastra/public/workspace/fpv-drone.md new file mode 100644 index 00000000..7a56e171 --- /dev/null +++ b/src/mastra/public/workspace/fpv-drone.md @@ -0,0 +1,13 @@ +# FPV Drone + +## Overview + +- SoloGood F722 FPV Flight Controller Stack ICM42688P F722 Flight Controller with 60A 4in1 ESC 30X30mm 2-6S for FPV Freestyle Drones Parts +- AKK FX2-ultimate 5.8GHz VTX with MMCX Antenna for FPV Drones +- Readytosky RS2205 2300KV Brushless Motor CW/CCW 3-4S RC Motors for FPV Racing Drone FPV Multicopter +- 4S 1500mAh 75C LiPo Battery for FPV Racing Drone Multicopter +- Jumper T14 2.4GHz ELRS Radio Controller with 4-in-1 Receiver for FPV Drones +- tsx 5.8GHz 2dBi FPV Antenna RHCP SMA for FPV Drones +- Racing Cam Analog 1800TVL FPV Camera with OSD for FPV Drones +- mini receiver for radio controller +- 5 inch race frame for FPV drones \ No newline at end of file diff --git a/src/mastra/tools/binance-crypto-market.tool.ts b/src/mastra/tools/binance-crypto-market.tool.ts index 64e0605f..278c0b97 100644 --- a/src/mastra/tools/binance-crypto-market.tool.ts +++ b/src/mastra/tools/binance-crypto-market.tool.ts @@ -1,7 +1,7 @@ import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' import type { RequestContext } from '@mastra/core/request-context' -import { createTool, type InferUITool } from '@mastra/core/tools' +import { createTool, InferToolInput, InferToolOutput, type InferUITool } from '@mastra/core/tools' import { z } from 'zod' import { log } from '../config/logger' import { httpFetch } from '../lib/http-client' @@ -503,5 +503,7 @@ export const binanceSpotMarketDataTool = createTool({ }, }) -export type BinanceSpotMarketDataInput = BinanceCryptoInput -export type BinanceSpotMarketDataUITool = InferUITool \ No newline at end of file + +export type BinanceSpotMarketDataUITool = InferUITool +export type BinanceSpotMarketData = InferToolOutput +export type BinanceSpotMarketDataInput = InferToolInput \ No newline at end of file diff --git a/src/mastra/tools/coinbase-exchange-crypto.tool.ts b/src/mastra/tools/coinbase-exchange-crypto.tool.ts index 7a3ca688..1b289ce6 100644 --- a/src/mastra/tools/coinbase-exchange-crypto.tool.ts +++ b/src/mastra/tools/coinbase-exchange-crypto.tool.ts @@ -1,7 +1,8 @@ import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' import type { RequestContext } from '@mastra/core/request-context' -import { createTool, type InferUITool } from '@mastra/core/tools' +import type { InferToolInput, InferToolOutput, InferUITool} from '@mastra/core/tools'; +import { createTool } from '@mastra/core/tools' import { z } from 'zod' import { log } from '../config/logger' import { httpFetch } from '../lib/http-client' @@ -335,4 +336,7 @@ export const coinbaseExchangeMarketDataTool = createTool({ }) export type CoinbaseExchangeMarketDataInput = CoinbaseInput -export type CoinbaseExchangeMarketDataUITool = InferUITool \ No newline at end of file +export type CoinbaseExchangeMarketDataUITool = InferUITool +export type CoinbaseExchangeMarketDataToolInput = InferToolInput +export type CoinbaseExchangeMarketDataToolOutput = InferToolOutput + diff --git a/src/mastra/tools/document-chunking.tool.ts b/src/mastra/tools/document-chunking.tool.ts index e845b4a6..97611ad2 100644 --- a/src/mastra/tools/document-chunking.tool.ts +++ b/src/mastra/tools/document-chunking.tool.ts @@ -1,10 +1,10 @@ import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' -import type { InferUITool } from '@mastra/core/tools' +import type { InferToolInput, InferUITool, InferToolOutput } from '@mastra/core/tools' import { createTool } from '@mastra/core/tools' import { MDocument, rerank } from '@mastra/rag' import { randomUUID } from 'crypto' - +import { fastembed } from '@mastra/fastembed' import { ModelRouterEmbeddingModel, ModelRouterLanguageModel, @@ -89,9 +89,9 @@ const CustomDocumentChunkingInputSchema = z.object({ chunkSize: z.number().min(50).max(4000).default(512), chunkOverlap: z.number().min(0).max(500).default(50), chunkSeparator: z.string().default('\n'), - indexName: z.string().default('memory_messages_3072'), + indexName: z.string().default('memory_messages_768'), // Embedding model name for ModelRouterEmbeddingModel (e.g. 'google/gemini-embedding-2-preview') - embeddingModel: z.string().default('google/gemini-embedding-2-preview'), + embeddingModel: z.string().default(fastembed.base), // Number of texts to embed per batch embeddingBatchSize: z.number().min(1).max(500).default(50), generateEmbeddings: z.boolean().default(true), @@ -203,7 +203,7 @@ Use this tool when you need advanced document processing with metadata extractio log.info('Mastra chunker tool input streaming started', { toolCallId, messageCount: messages.length, - aborted: abortSignal?.aborted, + aborted: resolveAbortSignal(abortSignal).aborted, hook: 'onInputStart', }) }, @@ -212,7 +212,7 @@ Use this tool when you need advanced document processing with metadata extractio toolCallId, inputTextDelta, messageCount: messages.length, - aborted: abortSignal?.aborted, + aborted: resolveAbortSignal(abortSignal).aborted, chunkingStrategy: 'recursive', hook: 'onInputDelta', }) @@ -223,7 +223,7 @@ Use this tool when you need advanced document processing with metadata extractio documentLength: input.documentContent.length, chunkingStrategy: input.chunkingStrategy, messageCount: messages.length, - aborted: abortSignal?.aborted, + aborted: resolveAbortSignal(abortSignal).aborted, hook: 'onInputAvailable', }) }, @@ -251,11 +251,11 @@ Use this tool when you need advanced document processing with metadata extractio id: 'mastra:chunker', }) const startTime = Date.now() - logToolExecution('mastra-chunker', { input: inputData }) + logToolExecution('mastra:chunker', { input: inputData }) const span = getOrCreateSpan({ type: SpanType.TOOL_CALL, - name: 'mastra-chunker', + name: 'mastra:chunker', input: { documentLength: inputData.documentContent.length, chunkingStrategy: inputData.chunkingStrategy, @@ -268,8 +268,8 @@ Use this tool when you need advanced document processing with metadata extractio requestContext: context.requestContext, tracingContext, metadata: { - 'tool.id': 'mastra-chunker', - operation: 'mastra-chunker', + 'tool.id': 'mastra:chunker', + operation: 'mastra:chunker', }, }) @@ -285,7 +285,7 @@ Use this tool when you need advanced document processing with metadata extractio chunkSize: inputData.chunkSize, chunkOverlap: inputData.chunkOverlap, processedAt: new Date().toISOString(), - source: 'mastra-chunker', + source: 'mastra:chunker', }, }, ], @@ -426,7 +426,7 @@ Use this tool when you need advanced document processing with metadata extractio processingTimeMs: totalProcessingTime, } - logStepEnd('mastra-chunker', output, totalProcessingTime) + logStepEnd('mastra:chunker', output, totalProcessingTime) return output } catch (error) { @@ -436,7 +436,7 @@ Use this tool when you need advanced document processing with metadata extractio ? error : new Error('Unknown error occurred') - logError('mastra-chunker', safeError, { + logError('mastra:chunker', safeError, { inputData, processingTimeMs: processingTime, }) @@ -458,7 +458,7 @@ Use this tool when you need advanced document processing with metadata extractio toolName, chunkCount: output.chunkCount, processingTimeMs: output.processingTimeMs, - aborted: abortSignal?.aborted ?? false, + aborted: resolveAbortSignal(abortSignal).aborted, hook: 'onOutput', }) }, @@ -494,7 +494,7 @@ This tool processes document content by: Features: - Multiple chunking strategies with customizable parameters -- Automatic embedding generation (3072 dimensions for gemini-embedding-2-preview) +- Automatic embedding generation (768 dimensions for fastembed.base) - PgVector storage with metadata support - Comprehensive error handling and logging - Performance monitoring and metrics @@ -541,9 +541,9 @@ content indexing, or semantic search capabilities. const chunkingStrategy = inputData.chunkingStrategy ?? 'recursive' const chunkSize = inputData.chunkSize ?? 512 const chunkOverlap = inputData.chunkOverlap ?? 50 - const indexName = inputData.indexName ?? 'memory_messages_3072' + const indexName = inputData.indexName ?? 'memory_messages_768' const embeddingModel = - inputData.embeddingModel ?? 'google/gemini-embedding-2-preview' + inputData.embeddingModel ?? fastembed.base const embeddingBatchSize = inputData.embeddingBatchSize ?? 50 // Check if operation was already cancelled @@ -561,11 +561,11 @@ content indexing, or semantic search capabilities. id: 'mdocument:chunker', }) const startTime = Date.now() - logToolExecution('mdocument-chunker', { input: inputData }) + logToolExecution('mdocument:chunker', { input: inputData }) const span = getOrCreateSpan({ type: SpanType.TOOL_CALL, - name: 'mdocument-chunker', + name: 'mdocument:chunker', input: { documentLength: inputData.documentContent.length, chunkingStrategy: inputData.chunkingStrategy, @@ -575,8 +575,8 @@ content indexing, or semantic search capabilities. requestContext: context.requestContext, tracingContext, metadata: { - 'tool.id': 'mdocument-chunker', - operation: 'mdocument-chunker', + 'tool.id': 'mdocument:chunker', + operation: 'mdocument:chunker', }, }) @@ -592,7 +592,7 @@ content indexing, or semantic search capabilities. chunkSize, chunkOverlap, processedAt: new Date().toISOString(), - source: 'mdocument-chunker', + source: 'mdocument:chunker', }, }, ], @@ -881,7 +881,7 @@ content indexing, or semantic search capabilities. processingTimeMs: totalProcessingTime, } - logStepEnd('mdocument-chunker', output, totalProcessingTime) + logStepEnd('mdocument:chunker', output, totalProcessingTime) await writer?.custom({ type: 'data-tool-progress', @@ -900,7 +900,7 @@ content indexing, or semantic search capabilities. ? error : new Error('Unknown error occurred') - logError('mdocument-chunker', safeError, { + logError('mdocument:chunker', safeError, { inputData, processingTimeMs: processingTime, }) @@ -938,7 +938,7 @@ content indexing, or semantic search capabilities. * * Features: * - Multiple chunking strategies with customizable parameters - * - Automatic embedding generation (3072 dimensions for gemini-embedding-2-preview) + * - Automatic embedding generation (768 dimensions for fastembed.base) * - LibSQL/Turso storage with metadata support * - Comprehensive error handling and logging * - Performance monitoring and metrics @@ -953,12 +953,12 @@ Custom Document Chunking Tool with LibSQL (Turso) Integration This tool processes document content by: 1. Creating chunks using configurable strategies (recursive, character, token, markdown, etc.) -2. Generating embeddings for each chunk using Gemini embedding model +2. Generating embeddings for each chunk using fastembed.base model 3. Storing chunks and embeddings in LibSQL (Turso) for efficient similarity search Features: - Multiple chunking strategies with customizable parameters -- Automatic embedding generation (3072 dimensions for gemini-embedding-2-preview) +- Automatic embedding generation (768 dimensions for fastembed.base) - LibSQL/Turso storage with metadata support - Comprehensive error handling and logging - Performance monitoring and metrics @@ -1005,9 +1005,9 @@ content indexing, or semantic search capabilities using LibSQL/Turso. const chunkingStrategy = inputData.chunkingStrategy ?? 'recursive' const chunkSize = inputData.chunkSize ?? 512 const chunkOverlap = inputData.chunkOverlap ?? 50 - const indexName = inputData.indexName ?? 'memory_messages_3072' + const indexName = inputData.indexName ?? 'memory_messages_768' const embeddingModel = - inputData.embeddingModel ?? 'google/gemini-embedding-2-preview' + inputData.embeddingModel ?? fastembed.base const embeddingBatchSize = inputData.embeddingBatchSize ?? 50 // Check if operation was already cancelled @@ -1025,11 +1025,11 @@ content indexing, or semantic search capabilities using LibSQL/Turso. id: 'libsql:chunker', }) const startTime = Date.now() - logToolExecution('libsql-chunker', { input: inputData }) + logToolExecution('libsql:chunker', { input: inputData }) const span = getOrCreateSpan({ type: SpanType.TOOL_CALL, - name: 'libsql-chunker', + name: 'libsql:chunker', input: { documentLength: inputData.documentContent.length, chunkingStrategy: inputData.chunkingStrategy, @@ -1039,8 +1039,8 @@ content indexing, or semantic search capabilities using LibSQL/Turso. requestContext: context.requestContext, tracingContext, metadata: { - 'tool.id': 'libsql-chunker', - operation: 'libsql-chunker', + 'tool.id': 'libsql:chunker', + operation: 'libsql:chunker', }, }) @@ -1056,7 +1056,7 @@ content indexing, or semantic search capabilities using LibSQL/Turso. chunkSize, chunkOverlap, processedAt: new Date().toISOString(), - source: 'libsql-chunker', + source: 'libsql:chunker', }, }, ], @@ -1218,9 +1218,7 @@ content indexing, or semantic search capabilities using LibSQL/Turso. ) const result = await embedMany({ values: batch, - model: new ModelRouterEmbeddingModel( - embeddingModel - ), + model: fastembed.base, maxRetries: 3, abortSignal: new AbortController().signal, }) @@ -1422,7 +1420,7 @@ Use this tool to improve retrieval quality by re-ranking initial search results. `, inputSchema: z.object({ userQuery: z.string().min(1, 'Query cannot be empty'), - indexName: z.string().default('memory_messages_3072'), + indexName: z.string().default('memory_messages_768'), topK: z.number().min(1).max(50).default(10), initialTopK: z.number().min(1).max(100).default(20), semanticWeight: z.number().min(0).max(1).default(0.5), @@ -1490,10 +1488,10 @@ Use this tool to improve retrieval quality by re-ranking initial search results. id: 'document:reranker', }) const startTime = Date.now() - logToolExecution('document-reranker', { + logToolExecution('document:reranker', { userQuery: inputData.userQuery, }) - const indexName = inputData.indexName ?? 'memory_messages_3072' + const indexName = inputData.indexName ?? 'memory_messages_768' const topK = inputData.topK ?? 10 const initialTopK = inputData.initialTopK ?? 20 const semanticWeight = inputData.semanticWeight ?? 0.5 @@ -1505,7 +1503,7 @@ Use this tool to improve retrieval quality by re-ranking initial search results. // Use the existing tracing context if available to create a child span. const span = tracingContext?.currentSpan?.createChildSpan({ type: SpanType.TOOL_CALL, - name: 'document-reranker', + name: 'document:reranker', input: { userQuery: inputData.userQuery, indexName, @@ -1514,7 +1512,7 @@ Use this tool to improve retrieval quality by re-ranking initial search results. }, metadata: { 'tool.id': 'document:reranker', - operation: 'document-reranker', + operation: 'document:reranker', }, }) @@ -1532,9 +1530,7 @@ Use this tool to improve retrieval quality by re-ranking initial search results. const embeddingStartTime = Date.now() const { embedding: queryEmbedding } = await embed({ value: inputData.userQuery, - model: new ModelRouterEmbeddingModel( - 'google/gemini-embedding-2-preview' - ), + model: fastembed.base, }) const embeddingTime = Date.now() - embeddingStartTime @@ -1674,7 +1670,7 @@ Use this tool to improve retrieval quality by re-ranking initial search results. processingTimeMs: totalProcessingTime, } - logStepEnd('document-reranker', output, totalProcessingTime) + logStepEnd('document:reranker', output, totalProcessingTime) await writer?.custom({ type: 'data-tool-progress', @@ -1693,7 +1689,7 @@ Use this tool to improve retrieval quality by re-ranking initial search results. ? error : new Error('Unknown error occurred') - logError('document-reranker', safeError, { + logError('document:reranker', safeError, { userQuery: inputData.userQuery, processingTimeMs: processingTime, }) @@ -1713,3 +1709,13 @@ Use this tool to improve retrieval quality by re-ranking initial search results. export type DocumentRerankerUITool = InferUITool export type MastraChunkerUITool = InferUITool export type MDocumentChunkerUITool = InferUITool + +export type LibSQLChunkerUITool = InferUITool +export type DocumentRerankerToolInput = InferToolInput +export type MastraChunkerToolInput = InferToolInput +export type MDocumentChunkerToolInput = InferToolInput +export type LibSQLChunkerToolInput = InferToolInput +export type DocumentRerankerToolOutput = InferToolOutput +export type MastraChunkerToolOutput = InferToolOutput +export type MDocumentChunkerToolOutput = InferToolOutput +export type LibSQLChunkerToolOutput = InferToolOutput diff --git a/src/mastra/tools/fetch.tool.ts b/src/mastra/tools/fetch.tool.ts index c385307f..73085c57 100644 --- a/src/mastra/tools/fetch.tool.ts +++ b/src/mastra/tools/fetch.tool.ts @@ -1,7 +1,7 @@ import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { RequestContext } from '@mastra/core/request-context' import type { TracingContext } from '@mastra/core/observability' -import type { InferUITool } from '@mastra/core/tools' +import type { InferToolInput, InferToolOutput, InferUITool } from '@mastra/core/tools' import { createTool } from '@mastra/core/tools' import * as cheerio from 'cheerio' import { XMLParser } from 'fast-xml-parser' @@ -1096,6 +1096,8 @@ export const fetchTool = createTool({ }) export type FetchUITool = InferUITool +export type FetchToolOutput = InferToolOutput +export type FetchToolInput = InferToolInput interface ScheduledFetchJob { jobId: string diff --git a/src/mastra/tools/index.ts b/src/mastra/tools/index.ts index 94d72985..fe63abf5 100644 --- a/src/mastra/tools/index.ts +++ b/src/mastra/tools/index.ts @@ -19,6 +19,7 @@ export * from './evaluateResultTool' export * from './extractLearningsTool' export * from './fetch.tool' export * from './financial-chart-tools' + export * from './finnhub-tools' export * from './git-local.tool' export * from './github' @@ -32,6 +33,8 @@ export * from './coinbase-exchange-crypto.tool' export * from './stooq-stock-market-data.tool' export * from './yahoo-finance-stock.tool' export * from './serpapi-academic-local.tool' +export * from './serpapi-images.tool' +export * from './serpapi-local-maps.tool' export * from './serpapi-news-trends.tool' export * from './serpapi-search.tool' export * from './serpapi-shopping.tool' @@ -48,9 +51,12 @@ export { arxivPdfParserTool } from './arxiv.tool' export { arxivTool } from './arxiv.tool' export { fetchTool } from './fetch.tool' export { weatherTool } from './weather-tool' -export { documentRerankerTool } from './document-chunking.tool' -export { mastraChunker } from './document-chunking.tool' -export { mdocumentChunker } from './document-chunking.tool' +export { writeNoteTool } from './write-note' +export { confirmationTool } from './confirmation.tool' +export { evaluateResultTool } from './evaluateResultTool' +export { extractLearningsTool } from './extractLearningsTool' +export { colorChangeTool } from './color-change-tool' +export { mdocumentChunker, libsqlChunker, documentRerankerTool, mastraChunker } from './document-chunking.tool' export { editorTool } from './editor-agent-tool' export { readPDF } from './pdf' export { @@ -68,4 +74,17 @@ export { export { randomGeneratorTool } from './random-generator.tool' export { textAnalysisTool, textProcessingTool } from './text-analysis.tool' export { urlValidationTool, urlManipulationTool } from './url-tool' +export { jsonToCsvTool } from './json-to-csv.tool' +export { googleSearchTool, googleAiOverviewTool } from './serpapi-search.tool' +export { googleTrendsTool, googleAutocompleteTool } from './serpapi-news-trends.tool' +export { googleScholarTool, googleFinanceTool, yelpSearchTool } from './serpapi-academic-local.tool' +export { googleImagesTool } from './serpapi-images.tool' +export { googleLocalTool, googleMapsReviewsTool } from './serpapi-local-maps.tool' +export { amazonSearchTool, walmartSearchTool, ebaySearchTool, homeDepotSearchTool } from './serpapi-shopping.tool' +export { spatialIndexTool } from './spatial-index.tool' +export { technicalAnalysisTool } from './technical-analysis.tool' +export { binanceSpotMarketDataTool } from './binance-crypto-market.tool' +export { coinbaseExchangeMarketDataTool } from './coinbase-exchange-crypto.tool' +export { stooqStockQuotesTool } from './stooq-stock-market-data.tool' +export { yahooFinanceStockQuotesTool } from './yahoo-finance-stock.tool' diff --git a/src/mastra/tools/serpapi-academic-local.tool.ts b/src/mastra/tools/serpapi-academic-local.tool.ts index da0848eb..9761affd 100644 --- a/src/mastra/tools/serpapi-academic-local.tool.ts +++ b/src/mastra/tools/serpapi-academic-local.tool.ts @@ -8,7 +8,7 @@ import type { RequestContext } from '@mastra/core/request-context' import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' -import type { InferUITool } from '@mastra/core/tools' +import type { InferToolInput, InferToolOutput, InferUITool } from '@mastra/core/tools' import { createTool } from '@mastra/core/tools' import { getJson } from 'serpapi' import { z } from 'zod' @@ -748,4 +748,14 @@ export const yelpSearchTool = createTool({ }, }) +export type GoogleFinanceToolUI = InferUITool +export type GoogleScholarToolUI = InferUITool export type YelpSearchUITool = InferUITool + + +export type GoogleFinanceToolOutput = InferToolOutput +export type GoogleFinanceToolInput = InferToolInput +export type GoogleScholarToolOutput = InferToolOutput +export type GoogleScholarToolInput = InferToolInput +export type YelpSearchToolOutput = InferToolOutput +export type YelpSearchToolInput = InferToolInput diff --git a/src/mastra/tools/serpapi-images.tool.ts b/src/mastra/tools/serpapi-images.tool.ts new file mode 100644 index 00000000..3b57cf40 --- /dev/null +++ b/src/mastra/tools/serpapi-images.tool.ts @@ -0,0 +1,330 @@ +/** + * SerpAPI Google Images Tool + * + * Provides structured Google Images search results, inline images, and a + * compact knowledge-graph summary when available. + */ +import type { RequestContext } from '@mastra/core/request-context' +import { SpanType, getOrCreateSpan } from '@mastra/core/observability' +import type { + InferToolInput, + InferToolOutput, + InferUITool, +} from '@mastra/core/tools' +import { createTool } from '@mastra/core/tools' +import { getJson } from 'serpapi' +import { z } from 'zod' +import { log } from '../config/logger' +import { validateSerpApiKey } from './serpapi-config' + +/** + * Shared request context for the SerpApi Google Images tool. + */ +export interface SerpApiImagesContext extends RequestContext { + userId?: string +} + +interface GoogleImagesRawResult { + position?: number + thumbnail: string + related_content_id?: string + serpapi_related_content_link?: string + original?: string + original_width?: number + original_height?: number + title?: string + tag?: string + link?: string + source?: string + source_logo?: string + is_product?: boolean + in_stock?: boolean +} + +interface GoogleImagesInlineImage { + link: string + source: string + thumbnail: string +} + +interface GoogleImagesSuggestedSearch { + name: string + link: string + chips?: string + serpapi_link?: string + thumbnail?: string +} + +interface GoogleImagesKnowledgeGraphRaw { + title?: string + type?: string + thumbnail?: string + serpapi_link?: string + description?: string + merchant_description?: string +} + +interface GoogleImagesResponse { + images_results?: GoogleImagesRawResult[] + inline_images?: GoogleImagesInlineImage[] + inline_images_suggested_searches?: GoogleImagesSuggestedSearch[] + knowledge_graph?: GoogleImagesKnowledgeGraphRaw +} + +const googleImagesOutputSchema = z.object({ + imagesResults: z + .array( + z.object({ + position: z.number().optional(), + thumbnail: z.url(), + relatedContentId: z.string().optional(), + serpapiRelatedContentLink: z.url().optional(), + original: z.url().optional(), + originalWidth: z.number().optional(), + originalHeight: z.number().optional(), + title: z.string().optional(), + tag: z.string().optional(), + link: z.url().optional(), + source: z.string().optional(), + sourceLogo: z.url().optional(), + isProduct: z.boolean().optional(), + inStock: z.boolean().optional(), + }) + ) + .describe('Normalized Google Images search results'), + inlineImages: z + .array( + z.object({ + link: z.url(), + source: z.url(), + thumbnail: z.url(), + }) + ) + .optional() + .describe('Inline image cards shown in the Google Images response'), + inlineImageSuggestedSearches: z + .array( + z.object({ + name: z.string(), + link: z.url(), + chips: z.string().optional(), + serpapiLink: z.url().optional(), + thumbnail: z.url().optional(), + }) + ) + .optional() + .describe('Suggested follow-up searches from the inline image section'), + knowledgeGraph: z + .object({ + title: z.string().optional(), + type: z.string().optional(), + thumbnail: z.url().optional(), + serpapiLink: z.url().optional(), + description: z.string().optional(), + merchantDescription: z.string().optional(), + }) + .optional() + .describe('Compact knowledge graph summary when SerpApi returns one'), +}) + +const googleImagesInputSchema = z.object({ + query: z.string().min(1).describe('The image search query'), + language: z + .string() + .default('en') + .describe('Two-letter language code for the response'), + country: z + .string() + .default('us') + .describe('Two-letter country code for the response'), + pageIndex: z + .number() + .int() + .min(0) + .default(0) + .describe('Image page index used by the ijn parameter'), + filters: z + .string() + .optional() + .describe('Raw tbs filter string, for example: itp:photos,isz:l'), +}) + +/** + * Google Images tool. + */ +export const googleImagesTool = createTool({ + id: 'google-images', + description: + 'Search Google Images and return structured image results, inline images, suggested searches, and a compact knowledge-graph summary. Useful for visual research, inspiration, and image source discovery.', + inputSchema: googleImagesInputSchema, + outputSchema: googleImagesOutputSchema, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Google Images received input', { + toolCallId, + inputData: { + query: input.query, + language: input.language, + country: input.country, + pageIndex: input.pageIndex, + filters: input.filters, + }, + messageCount: messages?.length ?? 0, + aborted: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const requestContext = context?.requestContext as + | SerpApiImagesContext + | undefined + const tracingContext = context?.tracingContext + + validateSerpApiKey() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `🖼️ Searching Google Images for "${input.query}"`, + stage: 'google-images', + }, + id: 'google-images', + }) + + const imagesSpan = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'google-images-tool', + input, + metadata: { + 'tool.id': 'google-images', + query: input.query, + pageIndex: input.pageIndex, + operation: 'google-images', + }, + requestContext, + tracingContext, + }) + + try { + const language = input.language ?? 'en' + const country = input.country ?? 'us' + const pageIndex = input.pageIndex ?? 0 + + const params: Record = { + engine: 'google_images', + q: input.query, + hl: language, + gl: country, + ijn: pageIndex, + google_domain: 'google.com', + } + + if (typeof input.filters === 'string' && input.filters.length > 0) { + params.tbs = input.filters + } + + const rawResponse = (await getJson(params)) as GoogleImagesResponse + + const imagesResults = (rawResponse.images_results ?? []).map( + (image) => ({ + position: image.position, + thumbnail: image.thumbnail, + relatedContentId: image.related_content_id, + serpapiRelatedContentLink: image.serpapi_related_content_link, + original: image.original, + originalWidth: image.original_width, + originalHeight: image.original_height, + title: image.title, + tag: image.tag, + link: image.link, + source: image.source, + sourceLogo: image.source_logo, + isProduct: image.is_product, + inStock: image.in_stock, + }) + ) + + const inlineImages = rawResponse.inline_images?.map((image) => ({ + link: image.link, + source: image.source, + thumbnail: image.thumbnail, + })) + + const inlineImageSuggestedSearches = + rawResponse.inline_images_suggested_searches?.map((search) => ({ + name: search.name, + link: search.link, + chips: search.chips, + serpapiLink: search.serpapi_link, + thumbnail: search.thumbnail, + })) + + const knowledgeGraph = rawResponse.knowledge_graph + ? { + title: rawResponse.knowledge_graph.title, + type: rawResponse.knowledge_graph.type, + thumbnail: rawResponse.knowledge_graph.thumbnail, + serpapiLink: rawResponse.knowledge_graph.serpapi_link, + description: rawResponse.knowledge_graph.description, + merchantDescription: + rawResponse.knowledge_graph.merchant_description, + } + : undefined + + const result = { + imagesResults, + inlineImages, + inlineImageSuggestedSearches, + knowledgeGraph, + } + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Google Images search complete: ${imagesResults.length} images`, + stage: 'google-images', + }, + id: 'google-images', + }) + + imagesSpan?.update({ + output: result, + metadata: { + 'tool.output.imageCount': imagesResults.length, + }, + }) + imagesSpan?.end() + + return result + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error) + imagesSpan?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + log.error('Google Images search failed', { + query: input.query, + error: errorMessage, + }) + throw new Error(`Google Images search failed: ${errorMessage}`, { + cause: error, + }) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + log.info('Google Images completed', { + toolCallId, + toolName, + imageCount: output.imagesResults.length, + aborted: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type GoogleImagesToolUI = InferUITool +export type GoogleImagesToolOutput = InferToolOutput +export type GoogleImagesToolInput = InferToolInput diff --git a/src/mastra/tools/serpapi-local-maps.tool.ts b/src/mastra/tools/serpapi-local-maps.tool.ts new file mode 100644 index 00000000..c0a07e81 --- /dev/null +++ b/src/mastra/tools/serpapi-local-maps.tool.ts @@ -0,0 +1,865 @@ +/** + * SerpAPI Local and Maps Review Tools + * + * Provides Google Local search and Google Maps reviews extraction. + * These tools are useful for business discovery, location research, and + * review analysis workflows. + */ +import type { RequestContext } from '@mastra/core/request-context' +import { SpanType, getOrCreateSpan } from '@mastra/core/observability' +import type { TracingContext } from '@mastra/core/observability' +import type { + InferToolInput, + InferToolOutput, + InferUITool, +} from '@mastra/core/tools' +import { createTool } from '@mastra/core/tools' +import { getJson } from 'serpapi' +import { z } from 'zod' +import { log } from '../config/logger' +import { validateSerpApiKey } from './serpapi-config' + +/** + * Shared request context for SerpAPI local and review tools. + */ +export interface SerpApiLocalContext extends RequestContext { + userId?: string +} + +const localCoordinatesSchema = z.object({ + latitude: z.number(), + longitude: z.number(), +}) + +/** + * Google Local responses sometimes return numeric fields as strings. + * + * @param value - Raw numeric-like value from SerpApi + * @returns Parsed number when possible + */ +function parseOptionalNumber(value: unknown): number | undefined { + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + + if (typeof value === 'string' && value.trim().length > 0) { + const parsed = Number(value.replace(/,/g, '')) + if (Number.isFinite(parsed)) { + return parsed + } + } + + return undefined +} + +/** + * Normalizes a Google result type into a string array. + * + * Google Local sometimes returns a single string and sometimes a list. + * + * @param type - Raw type value from SerpApi + * @returns Normalized array of type labels + */ +function normalizeResultType(type: unknown): string[] | undefined { + if (Array.isArray(type)) { + return type.filter((item): item is string => typeof item === 'string') + } + + if (typeof type === 'string' && type.trim().length > 0) { + return [type] + } + + return undefined +} + +interface GoogleLocalLinkGroup { + website?: string + directions?: string +} + +interface GoogleLocalServiceOptions { + dine_in?: boolean + takeout?: boolean + delivery?: boolean + curbside_pickup?: boolean +} + +interface GoogleLocalRawResult { + position?: number + title: string + data_id?: string + data_cid?: string + place_id?: string + reviews_link?: string + photos_link?: string + gps_coordinates?: { + latitude?: number + longitude?: number + } + place_id_search?: string + rating?: number + reviews?: number | string + reviews_original?: string + price?: string + description?: string + lsig?: string + thumbnail?: string + type?: string | string[] + address?: string + phone?: string + hours?: string + open_state?: string + website?: string + links?: GoogleLocalLinkGroup + service_options?: GoogleLocalServiceOptions +} + +interface GoogleLocalResponse { + local_results?: GoogleLocalRawResult[] + pagination?: { + next?: string + previous?: string + } + serpapi_pagination?: { + next?: string + previous?: string + } + search_information?: { + total_results?: number | string + } +} + +interface GoogleMapsReviewAuthor { + name?: string + url?: string + photo_url?: string + local_guide?: boolean + reviews?: number | string + photos?: number | string +} + +interface GoogleMapsReviewRaw { + position?: number + author?: GoogleMapsReviewAuthor + author_name?: string + author_url?: string + author_photo_url?: string + local_guide?: boolean + reviews?: number | string + photos?: number | string + rating?: number + date?: string + snippet?: string + text?: string + likes?: number | string + images?: string[] + review_id?: string +} + +interface GoogleMapsPlaceInfoRaw { + title?: string + rating?: number + reviews?: number | string + price?: string + type?: string | string[] + address?: string + phone?: string + website?: string + hours?: string + open_state?: string + description?: string + thumbnail?: string + data_id?: string + place_id?: string + data_cid?: string + gps_coordinates?: { + latitude?: number + longitude?: number + } + links?: GoogleLocalLinkGroup + service_options?: GoogleLocalServiceOptions +} + +interface GoogleMapsTopicRaw { + topic_id?: string + title?: string + reviews?: number | string + link?: string + serpapi_link?: string +} + +interface GoogleMapsReviewsResponse { + place_info?: GoogleMapsPlaceInfoRaw + place_information?: GoogleMapsPlaceInfoRaw + topics?: GoogleMapsTopicRaw[] + reviews?: GoogleMapsReviewRaw[] + reviews_results?: GoogleMapsReviewRaw[] + search_information?: { + total_results?: number | string + } + next_page_token?: string +} + +/** + * Local search output shape. + */ +const googleLocalOutputSchema = z.object({ + localResults: z + .array( + z.object({ + position: z.number().optional(), + title: z.string(), + rating: z.number().optional(), + reviews: z.number().optional(), + reviewsOriginal: z.string().optional(), + price: z.string().optional(), + description: z.string().optional(), + lsig: z.string().optional(), + thumbnail: z.url().optional(), + googleCid: z.string().optional(), + placeId: z.string().optional(), + dataId: z.string().optional(), + dataCid: z.string().optional(), + reviewsLink: z.url().optional(), + photosLink: z.url().optional(), + placeIdSearch: z.url().optional(), + gpsCoordinates: localCoordinatesSchema.optional(), + type: z.array(z.string()).optional(), + address: z.string().optional(), + phone: z.string().optional(), + hours: z.string().optional(), + openState: z.string().optional(), + website: z.url().optional(), + links: z + .object({ + website: z.url().optional(), + directions: z.url().optional(), + }) + .optional(), + serviceOptions: z + .object({ + dineIn: z.boolean().optional(), + takeout: z.boolean().optional(), + delivery: z.boolean().optional(), + curbsidePickup: z.boolean().optional(), + }) + .optional(), + }) + ) + .describe('Normalized Google Local results'), + totalResults: z + .number() + .optional() + .describe('Total number of local results when provided'), + pagination: z + .object({ + next: z.string().optional(), + previous: z.string().optional(), + }) + .optional() + .describe('Pagination metadata from SerpApi when available'), +}) + +/** + * Google Maps Reviews output shape. + */ +const googleMapsReviewsOutputSchema = z.object({ + placeInfo: z + .object({ + title: z.string().optional(), + rating: z.number().optional(), + reviews: z.number().optional(), + price: z.string().optional(), + type: z.array(z.string()).optional(), + address: z.string().optional(), + phone: z.string().optional(), + website: z.url().optional(), + hours: z.string().optional(), + openState: z.string().optional(), + description: z.string().optional(), + thumbnail: z.url().optional(), + googleCid: z.string().optional(), + placeId: z.string().optional(), + dataId: z.string().optional(), + dataCid: z.string().optional(), + gpsCoordinates: localCoordinatesSchema.optional(), + links: z + .object({ + website: z.url().optional(), + directions: z.url().optional(), + }) + .optional(), + serviceOptions: z + .object({ + dineIn: z.boolean().optional(), + takeout: z.boolean().optional(), + delivery: z.boolean().optional(), + curbsidePickup: z.boolean().optional(), + }) + .optional(), + }) + .optional() + .describe('Structured information about the reviewed place'), + topics: z + .array( + z.object({ + topicId: z.string().optional(), + title: z.string().optional(), + reviews: z.number().optional(), + link: z.url().optional(), + serpapiLink: z.url().optional(), + }) + ) + .optional() + .describe('Review topics returned by SerpApi when available'), + reviews: z + .array( + z.object({ + position: z.number().optional(), + authorName: z.string().optional(), + authorUrl: z.url().optional(), + authorPhotoUrl: z.url().optional(), + localGuide: z.boolean().optional(), + authorReviewCount: z.number().optional(), + authorPhotoCount: z.number().optional(), + rating: z.number().optional(), + date: z.string().optional(), + snippet: z.string().optional(), + likes: z.number().optional(), + images: z.array(z.url()).optional(), + reviewId: z.string().optional(), + }) + ) + .describe('Normalized review entries'), + totalResults: z + .number() + .optional() + .describe('Total review count when provided'), + nextPageToken: z + .string() + .optional() + .describe('Pagination token for retrieving additional reviews'), +}) + +/** + * Input schema for Google Local search. + */ +const googleLocalInputSchema = z.object({ + query: z.string().min(1).describe('The local search query'), + language: z + .string() + .default('en') + .describe('Two-letter language code for the response'), + country: z + .string() + .default('us') + .describe('Two-letter country code for the response'), + ludocid: z + .string() + .optional() + .describe('Google CID used to anchor the local search'), + start: z + .number() + .int() + .min(0) + .default(0) + .describe('Pagination offset for local results'), +}) + +/** + * Input schema for Google Maps reviews. + */ +const googleMapsReviewsInputSchema = z + .object({ + dataId: z + .string() + .optional() + .describe('Google Maps data ID for the place'), + placeId: z + .string() + .optional() + .describe('Google Maps place ID for the place'), + language: z + .string() + .default('en') + .describe('Two-letter language code for the response'), + sortBy: z + .enum(['qualityScore', 'newestFirst', 'ratingHigh', 'ratingLow']) + .default('qualityScore') + .describe('Sort order for reviews'), + topicId: z + .string() + .optional() + .describe('Optional review topic filter'), + query: z + .string() + .optional() + .describe('Optional text filter for reviews'), + numResults: z + .number() + .int() + .min(1) + .max(20) + .default(10) + .describe('Maximum number of reviews to return'), + nextPageToken: z + .string() + .optional() + .describe('Pagination token for subsequent review pages'), + }) + .refine((input) => Boolean(input.dataId || input.placeId), { + message: 'Either dataId or placeId must be provided.', + path: ['dataId'], + }) + .refine((input) => !(input.topicId && input.query), { + message: 'topicId and query cannot be used together.', + path: ['topicId'], + }) + +/** + * Google Local search tool. + */ +export const googleLocalTool = createTool({ + id: 'google-local', + description: + 'Search Google Local for nearby businesses and locations. Returns normalized business details including ratings, review counts, map IDs, contact data, hours, and navigation links. Best for location discovery and place research.', + inputSchema: googleLocalInputSchema, + outputSchema: googleLocalOutputSchema, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Google Local received input', { + toolCallId, + inputData: { + query: input.query, + language: input.language, + country: input.country, + ludocid: input.ludocid, + start: input.start, + }, + messageCount: messages?.length ?? 0, + aborted: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const requestContext = context?.requestContext as + | SerpApiLocalContext + | undefined + const tracingContext: TracingContext | undefined = context?.tracingContext + + validateSerpApiKey() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: `📍 Searching Google Local for "${input.query}"`, + stage: 'google-local', + }, + id: 'google-local', + }) + + const localSpan = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'google-local-tool', + input, + metadata: { + 'tool.id': 'google-local', + query: input.query, + language: input.language, + country: input.country, + operation: 'google-local', + }, + requestContext, + tracingContext, + }) + + try { + const language = input.language ?? 'en' + const country = input.country ?? 'us' + const start = input.start ?? 0 + + const params: Record = { + engine: 'google_local', + q: input.query, + hl: language, + gl: country, + start, + google_domain: 'google.com', + } + + if (typeof input.ludocid === 'string' && input.ludocid.length > 0) { + params.ludocid = input.ludocid + } + + const rawResponse = (await getJson(params)) as GoogleLocalResponse + const totalResults = parseOptionalNumber( + rawResponse.search_information?.total_results + ) + const paginationSource = + rawResponse.pagination ?? rawResponse.serpapi_pagination + + const localResults = (rawResponse.local_results ?? []).map((result) => ({ + position: result.position, + title: result.title, + rating: result.rating, + reviews: parseOptionalNumber(result.reviews), + reviewsOriginal: result.reviews_original, + price: result.price, + description: result.description, + lsig: result.lsig, + thumbnail: result.thumbnail, + googleCid: result.place_id, + placeId: result.place_id, + dataId: result.data_id, + dataCid: result.data_cid, + reviewsLink: result.reviews_link, + photosLink: result.photos_link, + placeIdSearch: result.place_id_search, + gpsCoordinates: + result.gps_coordinates?.latitude !== null && + result.gps_coordinates?.latitude !== undefined && + result.gps_coordinates?.longitude !== null && + result.gps_coordinates?.longitude !== undefined + ? { + latitude: result.gps_coordinates.latitude, + longitude: result.gps_coordinates.longitude, + } + : undefined, + type: normalizeResultType(result.type), + address: result.address, + phone: result.phone, + hours: result.hours, + openState: result.open_state, + website: result.links?.website ?? result.website, + links: result.links + ? { + website: result.links.website, + directions: result.links.directions, + } + : undefined, + serviceOptions: result.service_options + ? { + dineIn: result.service_options.dine_in, + takeout: result.service_options.takeout, + delivery: result.service_options.delivery, + curbsidePickup: result.service_options.curbside_pickup, + } + : undefined, + })) + + const result = { + localResults, + totalResults, + pagination: paginationSource + ? { + next: paginationSource.next, + previous: paginationSource.previous, + } + : undefined, + } + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Google Local search complete: ${localResults.length} places`, + stage: 'google-local', + }, + id: 'google-local', + }) + + localSpan?.update({ + output: result, + metadata: { + 'tool.output.placeCount': localResults.length, + }, + }) + localSpan?.end() + + return result + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error) + localSpan?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + log.error('Google Local search failed', { + query: input.query, + error: errorMessage, + }) + throw new Error(`Google Local search failed: ${errorMessage}`, { + cause: error, + }) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + log.info('Google Local completed', { + toolCallId, + toolName, + placeCount: output.localResults.length, + aborted: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +/** + * Google Maps reviews tool. + */ +export const googleMapsReviewsTool = createTool({ + id: 'google-maps-reviews', + description: + 'Fetch Google Maps reviews for a business or place using either the Maps data ID or place ID. Returns structured place details, review topics, and normalized review entries for downstream analysis.', + inputSchema: googleMapsReviewsInputSchema, + outputSchema: googleMapsReviewsOutputSchema, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + log.info('Google Maps Reviews received input', { + toolCallId, + inputData: { + dataId: input.dataId, + placeId: input.placeId, + language: input.language, + sortBy: input.sortBy, + topicId: input.topicId, + query: input.query, + numResults: input.numResults, + nextPageToken: input.nextPageToken, + }, + messageCount: messages?.length ?? 0, + aborted: abortSignal?.aborted, + hook: 'onInputAvailable', + }) + }, + execute: async (input, context) => { + const writer = context?.writer + const requestContext = context?.requestContext as + | SerpApiLocalContext + | undefined + const tracingContext: TracingContext | undefined = context?.tracingContext + + validateSerpApiKey() + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'in-progress', + message: '📝 Fetching Google Maps reviews...', + stage: 'google-maps-reviews', + }, + id: 'google-maps-reviews', + }) + + const mapsReviewsSpan = getOrCreateSpan({ + type: SpanType.TOOL_CALL, + name: 'google-maps-reviews-tool', + input, + metadata: { + 'tool.id': 'google-maps-reviews', + dataId: input.dataId, + placeId: input.placeId, + sortBy: input.sortBy, + operation: 'google-maps-reviews', + }, + requestContext, + tracingContext, + }) + + try { + const language = input.language ?? 'en' + const sortBy = input.sortBy ?? 'qualityScore' + const numResults = input.numResults ?? 10 + + const params: Record = { + engine: 'google_maps_reviews', + hl: language, + sort_by: sortBy, + google_domain: 'google.com', + } + + if (typeof input.dataId === 'string' && input.dataId.length > 0) { + params.data_id = input.dataId + } else if ( + typeof input.placeId === 'string' && input.placeId.length > 0 + ) { + params.place_id = input.placeId + } + + if (typeof input.topicId === 'string' && input.topicId.length > 0) { + params.topic_id = input.topicId + } + + if (typeof input.query === 'string' && input.query.length > 0) { + params.query = input.query + } + + const canUseNum = Boolean( + input.nextPageToken || input.topicId || input.query + ) + if (canUseNum) { + params.num = numResults + } + + if ( + typeof input.nextPageToken === 'string' && + input.nextPageToken.length > 0 + ) { + params.next_page_token = input.nextPageToken + } + + const rawResponse = (await getJson(params)) as GoogleMapsReviewsResponse + const rawPlaceInfo = rawResponse.place_info ?? rawResponse.place_information + + const placeInfo = rawPlaceInfo + ? { + title: rawPlaceInfo.title, + rating: rawPlaceInfo.rating, + reviews: parseOptionalNumber(rawPlaceInfo.reviews), + price: rawPlaceInfo.price, + type: normalizeResultType(rawPlaceInfo.type), + address: rawPlaceInfo.address, + phone: rawPlaceInfo.phone, + website: rawPlaceInfo.website, + hours: rawPlaceInfo.hours, + openState: rawPlaceInfo.open_state, + description: rawPlaceInfo.description, + thumbnail: rawPlaceInfo.thumbnail, + googleCid: rawPlaceInfo.place_id, + placeId: rawPlaceInfo.place_id, + dataId: rawPlaceInfo.data_id, + dataCid: rawPlaceInfo.data_cid, + gpsCoordinates: + rawPlaceInfo.gps_coordinates?.latitude !== null && + rawPlaceInfo.gps_coordinates?.latitude !== undefined && + rawPlaceInfo.gps_coordinates?.longitude !== null && + rawPlaceInfo.gps_coordinates?.longitude !== undefined + ? { + latitude: rawPlaceInfo.gps_coordinates.latitude, + longitude: rawPlaceInfo.gps_coordinates.longitude, + } + : undefined, + links: rawPlaceInfo.links + ? { + website: rawPlaceInfo.links.website, + directions: rawPlaceInfo.links.directions, + } + : undefined, + serviceOptions: rawPlaceInfo.service_options + ? { + dineIn: rawPlaceInfo.service_options.dine_in, + takeout: rawPlaceInfo.service_options.takeout, + delivery: rawPlaceInfo.service_options.delivery, + curbsidePickup: + rawPlaceInfo.service_options.curbside_pickup, + } + : undefined, + } + : undefined + + const topics = (rawResponse.topics ?? []).map((topic) => ({ + topicId: topic.topic_id, + title: topic.title, + reviews: parseOptionalNumber(topic.reviews), + link: topic.link, + serpapiLink: topic.serpapi_link, + })) + + const reviewsSource = rawResponse.reviews ?? rawResponse.reviews_results ?? [] + const reviews = reviewsSource.map((review) => { + const author = review.author + const authorName = review.author_name ?? author?.name + const authorUrl = review.author_url ?? author?.url + const authorPhotoUrl = review.author_photo_url ?? author?.photo_url + const localGuide = review.local_guide ?? author?.local_guide + const authorReviewCount = parseOptionalNumber( + review.reviews ?? author?.reviews + ) + const authorPhotoCount = parseOptionalNumber( + review.photos ?? author?.photos + ) + + return { + position: review.position, + authorName, + authorUrl, + authorPhotoUrl, + localGuide, + authorReviewCount, + authorPhotoCount, + rating: review.rating, + date: review.date, + snippet: review.snippet ?? review.text, + likes: parseOptionalNumber(review.likes), + images: review.images, + reviewId: review.review_id, + } + }) + + const totalResults = parseOptionalNumber( + rawResponse.search_information?.total_results + ) + + const result = { + placeInfo, + topics, + reviews, + totalResults, + nextPageToken: rawResponse.next_page_token, + } + + await writer?.custom({ + type: 'data-tool-progress', + data: { + status: 'done', + message: `✅ Google Maps reviews ready: ${reviews.length} reviews`, + stage: 'google-maps-reviews', + }, + id: 'google-maps-reviews', + }) + + mapsReviewsSpan?.update({ + output: result, + metadata: { + 'tool.output.reviewCount': reviews.length, + 'tool.output.topicCount': topics.length, + }, + }) + mapsReviewsSpan?.end() + + return result + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error) + mapsReviewsSpan?.error({ + error: error instanceof Error ? error : new Error(errorMessage), + endSpan: true, + }) + log.error('Google Maps Reviews failed', { + dataId: input.dataId, + placeId: input.placeId, + error: errorMessage, + }) + throw new Error(`Google Maps Reviews failed: ${errorMessage}`, { + cause: error, + }) + } + }, + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + log.info('Google Maps Reviews completed', { + toolCallId, + toolName, + reviewCount: output.reviews.length, + topicCount: output.topics?.length ?? 0, + aborted: abortSignal?.aborted, + hook: 'onOutput', + }) + }, +}) + +export type GoogleLocalToolUI = InferUITool +export type GoogleMapsReviewsToolUI = InferUITool +export type GoogleLocalToolOutput = InferToolOutput +export type GoogleLocalToolInput = InferToolInput +export type GoogleMapsReviewsToolOutput = InferToolOutput +export type GoogleMapsReviewsToolInput = InferToolInput diff --git a/src/mastra/tools/serpapi-news-trends.tool.ts b/src/mastra/tools/serpapi-news-trends.tool.ts index e61bc35c..e81d80f9 100644 --- a/src/mastra/tools/serpapi-news-trends.tool.ts +++ b/src/mastra/tools/serpapi-news-trends.tool.ts @@ -8,6 +8,11 @@ */ import { SpanType } from '@mastra/core/observability' import { createTool } from '@mastra/core/tools' +import type { + InferToolInput, + InferToolOutput, + InferUITool, +} from '@mastra/core/tools' import { z } from 'zod' import { getJson } from 'serpapi' import { log } from '../config/logger' @@ -19,32 +24,86 @@ export interface SerpApiNewsContext extends RequestContext { userId?: string } +interface SerpApiNewsSource { + name: string + authors?: string[] +} + interface SerpApiNewsArticle { + position?: number title: string link: string - source: { name: string } + source?: SerpApiNewsSource | string date: string + description?: string snippet?: string thumbnail?: string } +interface SerpApiMenuLink { + title: string + topic_token?: string + serpapi_link?: string +} + interface SerpApiNewsResponse { news_results?: SerpApiNewsArticle[] - search_information?: { total_results?: number } + menu_links?: SerpApiMenuLink[] + search_information?: { total_results?: number | string } +} + +interface SerpApiTrendTimelineValue { + query?: string + value?: string | number + extracted_value?: number +} + +interface SerpApiTrendTimelinePoint { + date?: string + timestamp: string + values?: SerpApiTrendTimelineValue[] + averages?: Array<{ query: string; value: number }> +} + +interface SerpApiTrendAverage { + query: string + value: number +} + +interface SerpApiTrendRelatedQueryItem { + query: string + value: string + extracted_value?: number + link?: string + serpapi_link?: string +} + +interface SerpApiTrendTopic { + value: string + title: string + type: string +} + +interface SerpApiTrendRelatedTopicItem { + topic: SerpApiTrendTopic + value: string + extracted_value?: number + link?: string + serpapi_link?: string } interface SerpApiTrendsResponse { interest_over_time?: { - timeline_data?: Array<{ - timestamp: string - values?: Array<{ value?: number }> - }> + timeline_data?: SerpApiTrendTimelinePoint[] + averages?: SerpApiTrendAverage[] } related_queries?: { - rising?: Array<{ query: string }> + rising?: SerpApiTrendRelatedQueryItem[] + top?: SerpApiTrendRelatedQueryItem[] } related_topics?: { - rising?: Array<{ topic: string }> + rising?: SerpApiTrendRelatedTopicItem[] + top?: SerpApiTrendRelatedTopicItem[] } } @@ -131,9 +190,16 @@ const googleNewsOutputSchema = z.object({ newsArticles: z .array( z.object({ + position: z.number().optional(), title: z.string(), link: z.url(), source: z.string(), + sourceDetails: z + .object({ + name: z.string(), + authors: z.array(z.string()).optional(), + }) + .optional(), date: z.string(), snippet: z.string(), thumbnail: z.url().optional(), @@ -144,6 +210,16 @@ const googleNewsOutputSchema = z.object({ .number() .optional() .describe('Total number of results available'), + menuLinks: z + .array( + z.object({ + title: z.string(), + topicToken: z.string(), + serpapiLink: z.url(), + }) + ) + .optional() + .describe('Topic and section links from the news navigation menu'), }) /** @@ -258,27 +334,78 @@ export const googleNewsTool = createTool({ } const response = await getJson(params) const newsResponse = response as SerpApiNewsResponse + const menuLinks = newsResponse.menu_links + ?.map((menuLink) => { + if ( + !menuLink.title || + !menuLink.topic_token || + !menuLink.serpapi_link + ) { + return null + } + + return { + title: menuLink.title, + topicToken: menuLink.topic_token, + serpapiLink: menuLink.serpapi_link, + } + }) + .filter( + ( + menuLink + ): menuLink is { + title: string + topicToken: string + serpapiLink: string + } => menuLink !== null + ) + const totalResultsRaw = newsResponse.search_information?.total_results + const totalResults = + typeof totalResultsRaw === 'string' + ? Number(totalResultsRaw) + : totalResultsRaw const newsArticles = newsResponse.news_results?.map( (article: { + position?: number title: string link: string - source: { name: string } + source?: + | { name: string; authors?: string[] } + | string date: string snippet?: string thumbnail?: string + description?: string }) => ({ + position: article.position, title: article.title, link: article.link, - source: article.source?.name ?? 'Unknown', + source: + typeof article.source === 'string' + ? article.source + : article.source?.name ?? 'Unknown', + sourceDetails: + typeof article.source === 'object' && + article.source !== null + ? { + name: article.source.name, + authors: article.source.authors, + } + : undefined, date: article.date, - snippet: article.snippet ?? '', + snippet: article.snippet ?? article.description ?? '', thumbnail: article.thumbnail, }) ) ?? [] const result = { newsArticles, - totalResults: newsResponse.search_information?.total_results, + totalResults: + typeof totalResults === 'number' && + Number.isFinite(totalResults) + ? totalResults + : undefined, + menuLinks, } await writer?.custom({ type: 'data-tool-progress', @@ -349,6 +476,7 @@ export const googleNewsLiteTool = createTool({ newsArticles: z .array( z.object({ + position: z.number().optional(), title: z.string(), link: z.url(), source: z.string(), @@ -418,6 +546,7 @@ export const googleNewsLiteTool = createTool({ const newsResultsSchema = z.array( z.object({ + position: z.number().optional(), title: z.string(), link: z.string(), source: z.object({ name: z.string() }).optional(), @@ -431,6 +560,7 @@ export const googleNewsLiteTool = createTool({ const newsArticles = parsed.success && parsed.data.news_results ? parsed.data.news_results.map((article) => ({ + position: article.position, title: article.title, link: article.link, source: article.source?.name ?? 'Unknown', @@ -532,16 +662,90 @@ const googleTrendsOutputSchema = z.object({ interestOverTime: z .array( z.object({ + date: z.string().optional(), timestamp: z.string(), - value: z.number(), + values: z.array( + z.object({ + query: z.string().optional(), + value: z.string(), + extractedValue: z.number(), + }) + ), }) ) .describe('Interest values over time'), + + averages: z + .array( + z.object({ + query: z.string(), + value: z.number(), + }) + ) + .optional() + .describe('Average values for each queried term'), relatedQueries: z - .array(z.string()) + .object({ + rising: z + .array( + z.object({ + query: z.string(), + value: z.string(), + extractedValue: z.number().optional(), + link: z.url().optional(), + serpapiLink: z.url().optional(), + }) + ) + .optional(), + top: z + .array( + z.object({ + query: z.string(), + value: z.string(), + extractedValue: z.number().optional(), + link: z.url().optional(), + serpapiLink: z.url().optional(), + }) + ) + .optional(), + }) .optional() - .describe('Related search queries'), - relatedTopics: z.array(z.string()).optional().describe('Related topics'), + .describe('Related search queries broken down by rising and top'), + relatedTopics: z + .object({ + rising: z + .array( + z.object({ + topic: z.object({ + value: z.string(), + title: z.string(), + type: z.string(), + }), + value: z.string(), + extractedValue: z.number().optional(), + link: z.url().optional(), + serpapiLink: z.url().optional(), + }) + ) + .optional(), + top: z + .array( + z.object({ + topic: z.object({ + value: z.string(), + title: z.string(), + type: z.string(), + }), + value: z.string(), + extractedValue: z.number().optional(), + link: z.url().optional(), + serpapiLink: z.url().optional(), + }) + ) + .optional(), + }) + .optional() + .describe('Related topics broken down by rising and top'), averageInterest: z .number() .optional() @@ -652,9 +856,24 @@ export const googleTrendsTool = createTool({ const response = await getJson(params) const timelinePointSchema = z.object({ + date: z.string().optional(), timestamp: z.string(), values: z - .array(z.object({ value: z.number() })) + .array( + z.object({ + query: z.string().optional(), + value: z.union([z.string(), z.number()]), + extracted_value: z.number().optional(), + }) + ) + .optional(), + averages: z + .array( + z.object({ + query: z.string(), + value: z.number(), + }) + ) .optional(), }) @@ -663,6 +882,14 @@ export const googleTrendsTool = createTool({ interest_over_time: z .object({ timeline_data: z.array(timelinePointSchema).optional(), + averages: z + .array( + z.object({ + query: z.string(), + value: z.number(), + }) + ) + .optional(), }) .optional(), }) @@ -672,32 +899,86 @@ export const googleTrendsTool = createTool({ parsed.success && parsed.data.interest_over_time?.timeline_data ? parsed.data.interest_over_time.timeline_data.map( (point) => ({ + date: point.date, timestamp: point.timestamp, - value: point.values?.[0]?.value ?? 0, + values: (point.values ?? []).map((value) => ({ + query: value.query, + value: String(value.value), + extractedValue: + value.extracted_value ?? + (typeof value.value === 'number' + ? value.value + : Number(value.value) || 0), + })), }) ) : [] const trendsResponse = response as SerpApiTrendsResponse - const relatedQueries = trendsResponse.related_queries?.rising?.map( - (q: { query: string }) => q.query - ) + const relatedQueries = trendsResponse.related_queries + ? { + rising: + trendsResponse.related_queries.rising?.map( + (query) => ({ + query: query.query, + value: query.value, + extractedValue: query.extracted_value, + link: query.link, + serpapiLink: query.serpapi_link, + }) + ) ?? [], + top: + trendsResponse.related_queries.top?.map((query) => ({ + query: query.query, + value: query.value, + extractedValue: query.extracted_value, + link: query.link, + serpapiLink: query.serpapi_link, + })) ?? [], + } + : undefined - const relatedTopics = trendsResponse.related_topics?.rising?.map( - (t: { topic: string }) => t.topic - ) + const relatedTopics = trendsResponse.related_topics + ? { + rising: + trendsResponse.related_topics.rising?.map((topic) => ({ + topic: topic.topic, + value: topic.value, + extractedValue: topic.extracted_value, + link: topic.link, + serpapiLink: topic.serpapi_link, + })) ?? [], + top: + trendsResponse.related_topics.top?.map((topic) => ({ + topic: topic.topic, + value: topic.value, + extractedValue: topic.extracted_value, + link: topic.link, + serpapiLink: topic.serpapi_link, + })) ?? [], + } + : undefined + + const averages = + parsed.success && parsed.data.interest_over_time?.averages + ? parsed.data.interest_over_time.averages.map((average) => ({ + query: average.query, + value: average.value, + })) + : undefined const averageInterest = interestOverTime.length > 0 ? interestOverTime.reduce( - (sum: number, item: { value: number }) => - sum + item.value, + (sum: number, item: { values: Array<{ extractedValue: number }> }) => + sum + (item.values[0]?.extractedValue ?? 0), 0 ) / interestOverTime.length : undefined const result = { interestOverTime, + averages, relatedQueries, relatedTopics, averageInterest, @@ -740,12 +1021,19 @@ export const googleTrendsTool = createTool({ } }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + const relatedQueryCount = + (output.relatedQueries?.rising?.length ?? 0) + + (output.relatedQueries?.top?.length ?? 0) + const relatedTopicCount = + (output.relatedTopics?.rising?.length ?? 0) + + (output.relatedTopics?.top?.length ?? 0) + log.info('Google Trends analysis completed', { toolCallId, toolName, dataPoints: output?.interestOverTime?.length ?? 0, - relatedQueries: output?.relatedQueries?.length ?? 0, - relatedTopics: output?.relatedTopics?.length ?? 0, + relatedQueries: relatedQueryCount, + relatedTopics: relatedTopicCount, aborted: resolveAbortSignal(abortSignal).aborted, hook: 'onOutput', }) @@ -898,3 +1186,16 @@ export const googleAutocompleteTool = createTool({ } }, }) + +export type GoogleNewsToolUI = InferUITool +export type GoogleNewsLiteToolUI = InferUITool +export type GoogleTrendsToolUI = InferUITool +export type GoogleAutocompleteToolUI = InferUITool +export type GoogleTrendsToolOutput = InferToolOutput +export type GoogleTrendsToolInput = InferToolInput +export type GoogleNewsToolOutput = InferToolOutput +export type GoogleNewsToolInput = InferToolInput +export type GoogleNewsLiteToolOutput = InferToolOutput +export type GoogleNewsLiteToolInput = InferToolInput +export type GoogleAutocompleteToolOutput = InferToolOutput +export type GoogleAutocompleteToolInput = InferToolInput \ No newline at end of file diff --git a/src/mastra/tools/serpapi-search.tool.ts b/src/mastra/tools/serpapi-search.tool.ts index 600a85fe..5931f5d3 100644 --- a/src/mastra/tools/serpapi-search.tool.ts +++ b/src/mastra/tools/serpapi-search.tool.ts @@ -9,6 +9,7 @@ import type { RequestContext } from '@mastra/core/request-context' import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' +import type { InferToolInput, InferToolOutput, InferUITool } from '@mastra/core/tools'; import { createTool } from '@mastra/core/tools' import { getJson } from 'serpapi' import { z } from 'zod' @@ -596,3 +597,12 @@ export const googleAiOverviewTool = createTool({ }) }, }) + +export type GoogleSearchToolUI = InferUITool +export type GoogleAiOverviewToolUI = InferUITool + + +export type GoogleSearchToolOutput = InferToolOutput +export type GoogleSearchToolInput = InferToolInput +export type GoogleAiOverviewToolOutput = InferToolOutput +export type GoogleAiOverviewToolInput = InferToolInput \ No newline at end of file diff --git a/src/mastra/tools/serpapi-shopping.tool.ts b/src/mastra/tools/serpapi-shopping.tool.ts index 618485e0..ce23e7bf 100644 --- a/src/mastra/tools/serpapi-shopping.tool.ts +++ b/src/mastra/tools/serpapi-shopping.tool.ts @@ -5,12 +5,14 @@ * * @module serpapi-shopping-tool */ +import type { InferToolInput, InferToolOutput, InferUITool } from '@mastra/core/tools'; import { createTool } from '@mastra/core/tools' import { SpanType } from '@mastra/core/observability' import { getJson } from 'serpapi' import { z } from 'zod' import { log } from '../config/logger' import { validateSerpApiKey } from './serpapi-config' +import { resolveAbortSignal } from './abort-signal.utils'; // Typed interfaces for SerpAPI shopping responses interface AmazonSearchResultItem { @@ -31,6 +33,7 @@ interface AmazonSearchApiResponse { interface WalmartSearchResultItem { title: string product_id: string + product_page_url?: string link: string primary_offer?: { offer_price?: number } rating?: number @@ -43,7 +46,8 @@ interface WalmartSearchApiResponse { interface EbaySearchResultItem { title: string - item_id: string + product_id?: string + item_id?: string link: string price?: { value: number } condition?: string @@ -52,6 +56,17 @@ interface EbaySearchResultItem { thumbnail?: string } +interface EbaySearchNormalizedProduct { + title: string + itemId: string + link: string + price?: number + condition?: string + bids?: number + timeLeft?: string + thumbnail?: string +} + interface EbaySearchApiResponse { organic_results?: EbaySearchResultItem[] } @@ -117,7 +132,7 @@ export const amazonSearchTool = createTool({ log.info('amazonSearchTool tool input streaming started', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, hook: 'onInputStart', }) }, @@ -125,7 +140,7 @@ export const amazonSearchTool = createTool({ log.info('amazonSearchTool received input', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, inputData: { query: input.query, sortBy: input.sortBy, @@ -144,7 +159,7 @@ export const amazonSearchTool = createTool({ outputData: { products: output.products, }, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, hook: 'onOutput', }) }, @@ -192,7 +207,7 @@ export const amazonSearchTool = createTool({ try { const params: Record = { engine: 'amazon', - query: inputData.query, + k: inputData.query, num: numResults, } @@ -204,7 +219,7 @@ export const amazonSearchTool = createTool({ } const sortValue = sortMap[sortBy] if (sortValue) { - params.sort_by = sortValue + params.s = sortValue } } @@ -323,7 +338,7 @@ export const walmartSearchTool = createTool({ log.info('walmartSearchTool tool input streaming started', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, hook: 'onInputStart', }) }, @@ -331,7 +346,7 @@ export const walmartSearchTool = createTool({ log.info('walmartSearchTool received input', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, inputData: { query: input.query, sortBy: input.sortBy, @@ -346,7 +361,7 @@ export const walmartSearchTool = createTool({ log.info('walmartSearchTool completed', { toolCallId, toolName, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, outputData: { products: output.products, }, @@ -419,7 +434,7 @@ export const walmartSearchTool = createTool({ (product: WalmartSearchResultItem) => ({ title: product.title, productId: product.product_id, - link: product.link, + link: product.product_page_url ?? product.link, price: product.primary_offer?.offer_price, rating: product.rating, thumbnail: product.thumbnail, @@ -522,7 +537,7 @@ export const ebaySearchTool = createTool({ log.info('ebaySearchTool tool input streaming started', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, hook: 'onInputStart', }) }, @@ -530,7 +545,7 @@ export const ebaySearchTool = createTool({ log.info('ebaySearchTool received input', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, inputData: { query: input.query, condition: input.condition, @@ -545,7 +560,7 @@ export const ebaySearchTool = createTool({ log.info('ebaySearchTool completed', { toolCallId, toolName, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, outputData: { products: output.products, }, @@ -623,19 +638,38 @@ export const ebaySearchTool = createTool({ const response = await getJson(params) const ebayResponse = response as EbaySearchApiResponse - const products = - ebayResponse.organic_results?.map( - (product: EbaySearchResultItem) => ({ + const products: EbaySearchNormalizedProduct[] = ( + ebayResponse.organic_results ?? [] + ).flatMap((product: EbaySearchResultItem) => { + const itemId = product.product_id ?? product.item_id + + if (!itemId) { + return [] + } + + return [ + { title: product.title, - itemId: product.item_id, + itemId, link: product.link, - price: product.price?.value, - condition: product.condition, - bids: product.bids, - timeLeft: product.time_left, - thumbnail: product.thumbnail, - }) - ) ?? [] + ...(product.price?.value !== undefined + ? { price: product.price.value } + : {}), + ...(product.condition !== undefined + ? { condition: product.condition } + : {}), + ...(product.bids !== undefined + ? { bids: product.bids } + : {}), + ...(product.time_left !== undefined + ? { timeLeft: product.time_left } + : {}), + ...(product.thumbnail !== undefined + ? { thumbnail: product.thumbnail } + : {}), + }, + ] + }) const result = { products } await writer?.custom({ type: 'data-tool-progress', @@ -725,7 +759,7 @@ export const homeDepotSearchTool = createTool({ log.info('homeDepotSearchTool tool input streaming started', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, hook: 'onInputStart', }) }, @@ -733,7 +767,7 @@ export const homeDepotSearchTool = createTool({ log.info('homeDepotSearchTool received input', { toolCallId, messageCount: messages.length, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, inputData: { query: input.query, sortBy: input.sortBy, @@ -747,7 +781,7 @@ export const homeDepotSearchTool = createTool({ log.info('homeDepotSearchTool completed', { toolCallId, toolName, - abortSignal: abortSignal?.aborted, + abortSignal: resolveAbortSignal(abortSignal).aborted, outputData: { products: output.products, }, @@ -867,3 +901,19 @@ export const homeDepotSearchTool = createTool({ } }, }) + +export type AmazonSearchToolUI = InferUITool +export type WalmartSearchToolUI = InferUITool +export type EbaySearchToolUI = InferUITool +export type HomeDepotSearchToolUI = InferUITool + +export type AmazonSearchToolOutput = InferToolOutput +export type AmazonSearchToolInput = InferToolInput +export type WalmartSearchToolOutput = InferToolOutput +export type WalmartSearchToolInput = InferToolInput +export type EbaySearchToolOutput = InferToolOutput +export type EbaySearchToolInput = InferToolInput +export type HomeDepotSearchToolOutput = + InferToolOutput +export type HomeDepotSearchToolInput = + InferToolInput \ No newline at end of file diff --git a/src/mastra/tools/stooq-stock-market-data.tool.ts b/src/mastra/tools/stooq-stock-market-data.tool.ts index 11a7e0bc..3161a881 100644 --- a/src/mastra/tools/stooq-stock-market-data.tool.ts +++ b/src/mastra/tools/stooq-stock-market-data.tool.ts @@ -1,6 +1,7 @@ import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' import type { RequestContext } from '@mastra/core/request-context' +import type { InferToolInput, InferToolOutput} from '@mastra/core/tools'; import { createTool, type InferUITool } from '@mastra/core/tools' import { z } from 'zod' import { log } from '../config/logger' @@ -284,5 +285,7 @@ export const stooqStockQuotesTool = createTool({ }, }) -export type StooqStockQuotesInput = StooqInput + export type StooqStockQuotesUITool = InferUITool +export type StooqStockQuotesOutput = InferToolOutput +export type StooqStockQuotesInput = InferToolInput diff --git a/src/mastra/tools/technical-analysis.tool.ts b/src/mastra/tools/technical-analysis.tool.ts index e732e110..81a07266 100644 --- a/src/mastra/tools/technical-analysis.tool.ts +++ b/src/mastra/tools/technical-analysis.tool.ts @@ -131,6 +131,185 @@ interface HeikinAshiOutput { close: number } +type HookSummary = Record + +interface IchimokuCloudResult { + success: boolean + results: IchimokuCloudOutput[] + message?: string +} + +interface TrendAnalysisResult { + success: boolean + sma?: number[] + ema?: number[] + wma?: number[] + macd?: MACDOutput[] + adx?: number[] + trix?: number[] + kst?: KSTOutput[] + message?: string +} + +interface MomentumAnalysisResult { + success: boolean + rsi?: number[] + stochastic?: Array<{ k: number; d: number }> + williamsR?: number[] + roc?: number[] + forceIndex?: number[] + message?: string +} + +interface VolatilityAnalysisResult { + success: boolean + bollinger?: BollingerBandsOutput[] + atr?: number[] + message?: string +} + +interface VolumeAnalysisResult { + success: boolean + obv?: number[] + adl?: number[] + mfi?: number[] + vwap?: number[] + message?: string +} + +interface StatisticalSummary { + mean: number + median: number + mode: number + standardDeviation: number + variance: number + min: number + max: number + skewness: number + kurtosis: number +} + +type TechnicalAnalysisStats = Pick< + StatisticalSummary, + | 'mean' + | 'median' + | 'mode' + | 'standardDeviation' + | 'variance' + | 'min' + | 'max' +> + +interface StatisticalAnalysisResult { + success: boolean + stats?: StatisticalSummary + regression?: { + m: number + b: number + } + correlation?: number + message?: string +} + +type MarketSentiment = + | 'Strong Buy' + | 'Buy' + | 'Neutral' + | 'Sell' + | 'Strong Sell' + +interface MarketSummaryResult { + success: boolean + sentiment: MarketSentiment + score: number + indicators: Record + message?: string +} + +type TechnicalAnalysisSeriesMap = Partial<{ + sma: number[] + ema: number[] + rsi: number[] + wma: number[] + macd: MACDOutput[] + bollinger: BollingerBandsOutput[] + atr: number[] + stochastic: Array<{ k: number; d: number }> +}> + +interface TechnicalAnalysisResult { + success: boolean + results: TechnicalAnalysisSeriesMap + stats?: TechnicalAnalysisStats + message?: string +} + +function getHookMessageCount(messages: ReadonlyArray | undefined): number { + return messages?.length ?? 0 +} + +function logToolHookStart( + label: string, + toolCallId: string | undefined, + messages: ReadonlyArray | undefined, + abortSignal: AbortSignal | undefined +): void { + log.info(`${label} input streaming started`, { + toolCallId, + messageCount: getHookMessageCount(messages), + abortSignal: abortSignal?.aborted ?? false, + hook: 'onInputStart', + }) +} + +function logToolHookDelta( + label: string, + toolCallId: string | undefined, + inputTextDelta: string, + messages: ReadonlyArray | undefined, + abortSignal: AbortSignal | undefined +): void { + log.info(`${label} received input chunk`, { + toolCallId, + inputTextDelta, + messageCount: getHookMessageCount(messages), + abortSignal: abortSignal?.aborted ?? false, + hook: 'onInputDelta', + }) +} + +function logToolHookAvailable( + label: string, + toolCallId: string | undefined, + messages: ReadonlyArray | undefined, + abortSignal: AbortSignal | undefined, + inputData: HookSummary +): void { + log.info(`${label} received input`, { + toolCallId, + messageCount: getHookMessageCount(messages), + inputData, + abortSignal: abortSignal?.aborted ?? false, + hook: 'onInputAvailable', + }) +} + +function logToolHookOutput( + label: string, + toolCallId: string | undefined, + toolName: string | undefined, + abortSignal: AbortSignal | undefined, + outputData: HookSummary +): void { + log.info(`${label} completed`, { + toolCallId, + toolName, + outputData, + abortSignal: abortSignal?.aborted ?? false, + hook: 'onOutput', + }) +} + export const ichimokuCloudTool = createTool({ id: 'ichimoku-cloud', description: 'Calculate Ichimoku Cloud (Kinko Hyo) components.', @@ -158,7 +337,7 @@ export const ichimokuCloudTool = createTool({ .optional(), message: z.string().optional(), }), - execute: async (inputData, context) => { + execute: async (inputData, context): Promise => { const writer = context?.writer const abortSignal = context?.abortSignal const tracingContext: TracingContext | undefined = context?.tracingContext @@ -204,7 +383,7 @@ export const ichimokuCloudTool = createTool({ }) try { - const results = IchimokuCloud.calculate({ + const rawResults = IchimokuCloud.calculate({ high: inputData.high, low: inputData.low, conversionPeriod, @@ -213,9 +392,26 @@ export const ichimokuCloudTool = createTool({ displacement, }) - const finalResult = { + const startIndex = Math.max( + 0, + inputData.close.length - rawResults.length + ) + const results: IchimokuCloudResult['results'] = rawResults.map( + (result, index) => ({ + tenkanSen: result.conversion, + kijunSen: result.base, + senkouSpanA: result.spanA, + senkouSpanB: result.spanB, + chikouSpan: + inputData.close[startIndex + index] ?? + inputData.close[inputData.close.length - 1] ?? + result.base, + }) + ) + + const finalResult: IchimokuCloudResult = { success: true, - results: results as unknown as IchimokuCloudOutput[], + results, } const duration = Date.now() - startTime @@ -269,30 +465,32 @@ export const ichimokuCloudTool = createTool({ return { success: false, message: err.message } } }, - onInputStart: ({ toolCallId }) => { - log.info('Ichimoku tool input streaming started', { - toolCallId, - hook: 'onInputStart', - }) + onInputStart: ({ toolCallId, messages, abortSignal }) => { + logToolHookStart('Ichimoku tool', toolCallId, messages, abortSignal) }, - onInputDelta: ({ toolCallId }) => { - log.info('Ichimoku tool received input chunk', { + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta( + 'Ichimoku tool', toolCallId, - hook: 'onInputDelta', - }) + inputTextDelta, + messages, + abortSignal + ) }, - onInputAvailable: ({ input, toolCallId }) => { - log.info('Ichimoku tool received input', { - toolCallId, - inputData: input, - hook: 'onInputAvailable', + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + logToolHookAvailable('Ichimoku tool', toolCallId, messages, abortSignal, { + highLength: input.high.length, + lowLength: input.low.length, + closeLength: input.close.length, + conversionPeriod: input.conversionPeriod, + basePeriod: input.basePeriod, + spanPeriod: input.spanPeriod, + displacement: input.displacement, }) }, - onOutput: ({ toolCallId, toolName }) => { - log.info('Ichimoku tool completed', { - toolCallId, - toolName, - hook: 'onOutput', + onOutput: ({ output, toolCallId, toolName, abortSignal }) => { + logToolHookOutput('Ichimoku tool', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -413,30 +611,28 @@ export const fibonacciTool = createTool({ return { success: false, message: errorMessage } } }, - onInputStart: ({ toolCallId }) => { - log.info('Fibonacci tool input streaming started', { - toolCallId, - hook: 'onInputStart', - }) + onInputStart: ({ toolCallId, messages, abortSignal }) => { + logToolHookStart('Fibonacci tool', toolCallId, messages, abortSignal) }, - onInputDelta: ({ toolCallId }) => { - log.info('Fibonacci tool received input chunk', { + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta( + 'Fibonacci tool', toolCallId, - hook: 'onInputDelta', - }) + inputTextDelta, + messages, + abortSignal + ) }, - onInputAvailable: ({ input, toolCallId }) => { - log.info('Fibonacci tool received input', { - toolCallId, - inputData: input, - hook: 'onInputAvailable', + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + logToolHookAvailable('Fibonacci tool', toolCallId, messages, abortSignal, { + high: input.high, + low: input.low, + trend: input.trend, }) }, - onOutput: ({ toolCallId, toolName }) => { - log.info('Fibonacci tool completed', { - toolCallId, - toolName, - hook: 'onOutput', + onOutput: ({ toolCallId, toolName, abortSignal }) => { + logToolHookOutput('Fibonacci tool', toolCallId, toolName, abortSignal, { + success: true, }) }, }) @@ -627,36 +823,27 @@ export const pivotPointsTool = createTool({ } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Pivot points tool input streaming started', { - toolCallId, - messages: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) + logToolHookStart('Pivot points tool', toolCallId, messages, abortSignal) }, - onInputDelta: ({ toolCallId, messages, abortSignal }) => { - log.info('Pivot points tool received input chunk', { + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta( + 'Pivot points tool', toolCallId, - hook: 'onInputDelta', - messages: messages.length, - abortSignal: abortSignal?.aborted, - }) + inputTextDelta, + messages, + abortSignal + ) }, onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Pivot points tool received input', { - toolCallId, - inputData: input, - messages: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', + logToolHookAvailable('Pivot points tool', toolCallId, messages, abortSignal, { + high: input.high, + low: input.low, + close: input.close, }) }, onOutput: ({ toolCallId, toolName, abortSignal }) => { - log.info('Pivot points tool completed', { - toolCallId, - toolName, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Pivot points tool', toolCallId, toolName, abortSignal, { + success: true, }) }, }) @@ -749,7 +936,7 @@ export const trendAnalysisTool = createTool({ const fastPeriod = rawFastPeriod ?? 12 const slowPeriod = rawSlowPeriod ?? 26 const signalPeriod = rawSignalPeriod ?? 9 - const results: Record = { + const results: TrendAnalysisResult = { success: true, sma: SMA.calculate({ values: data, period }), ema: EMA.calculate({ values: data, period }), @@ -767,18 +954,29 @@ export const trendAnalysisTool = createTool({ SMAROCPer4: 15, signalPeriod: 9, }), - macd: MACD.calculate({ - values: data, - fastPeriod, - slowPeriod, - signalPeriod, - SimpleMAOscillator: false, - SimpleMASignal: false, - }), + macd: (() => { + const macdValues: MACDOutput[] = MACD.calculate({ + values: data, + fastPeriod, + slowPeriod, + signalPeriod, + SimpleMAOscillator: false, + SimpleMASignal: false, + }).map((entry): MACDOutput => ({ + MACD: entry.MACD ?? 0, + signal: entry.signal ?? 0, + histogram: entry.histogram ?? 0, + })) + + return macdValues + })(), } if (high && low && close) { - results.adx = ADX.calculate({ high, low, close, period }) + const adxValues: number[] = ADX.calculate({ high, low, close, period }).map( + (entry): number => entry.adx + ) + results.adx = adxValues } toolSpan?.update({ @@ -797,17 +995,7 @@ export const trendAnalysisTool = createTool({ id: 'trend-analysis', }) - return results as unknown as { - success: boolean - sma?: number[] - ema?: number[] - wma?: number[] - macd?: MACDOutput[] - adx?: number[] - trix?: number[] - kst?: KSTOutput[] - message?: string - } + return results } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -828,38 +1016,26 @@ export const trendAnalysisTool = createTool({ } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Trend analysis input streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) + logToolHookStart('Trend analysis', toolCallId, messages, abortSignal) }, onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Trend analysis received input chunk', { + logToolHookDelta( + 'Trend analysis', toolCallId, inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) + messages, + abortSignal + ) }, onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Trend analysis received input', { - toolCallId, - messageCount: messages.length, - inputData: { period: input.period }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', + logToolHookAvailable('Trend analysis', toolCallId, messages, abortSignal, { + period: input.period, + dataLength: input.data.length, }) }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Trend analysis completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Trend analysis', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -890,7 +1066,7 @@ export const momentumAnalysisTool = createTool({ forceIndex: z.array(z.number()).optional(), message: z.string().optional(), }), - execute: async (inputData, context) => { + execute: async (inputData, context): Promise => { const writer = context?.writer const abortSignal = context?.abortSignal const tracingContext: TracingContext | undefined = context?.tracingContext @@ -928,7 +1104,7 @@ export const momentumAnalysisTool = createTool({ inputData const period = rawPeriod ?? 14 const signalPeriod = rawSignalPeriod ?? 3 - const results: Record = { + const results: MomentumAnalysisResult = { success: true, rsi: RSI.calculate({ values: data, period }), roc: ROC.calculate({ values: data, period }), @@ -974,15 +1150,7 @@ export const momentumAnalysisTool = createTool({ id: 'momentum-analysis', }) - return results as unknown as { - success: boolean - rsi?: number[] - stochastic?: Array<{ k: number; d: number }> - williamsR?: number[] - roc?: number[] - forceIndex?: number[] - message?: string - } + return results } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -1003,38 +1171,27 @@ export const momentumAnalysisTool = createTool({ } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Momentum analysis input streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) + logToolHookStart('Momentum analysis', toolCallId, messages, abortSignal) }, onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Momentum analysis received input chunk', { + logToolHookDelta( + 'Momentum analysis', toolCallId, inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) + messages, + abortSignal + ) }, onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Momentum analysis received input', { - toolCallId, - messageCount: messages.length, - inputData: { period: input.period }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', + logToolHookAvailable('Momentum analysis', toolCallId, messages, abortSignal, { + period: input.period, + signalPeriod: input.signalPeriod, + dataLength: input.data.length, }) }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Momentum analysis completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Momentum analysis', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -1102,7 +1259,7 @@ export const volatilityAnalysisTool = createTool({ const { data, period: rawPeriod, stdDev: rawStdDev, high, low, close } = inputData const period = rawPeriod ?? 20 const stdDev = rawStdDev ?? 2 - const results: Record = { + const results: VolatilityAnalysisResult = { success: true, bollinger: BollingerBands.calculate({ values: data, @@ -1131,12 +1288,7 @@ export const volatilityAnalysisTool = createTool({ id: 'volatility-analysis', }) - return results as unknown as { - success: boolean - bollinger?: BollingerBandsOutput[] - atr?: number[] - message?: string - } + return results } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -1157,38 +1309,27 @@ export const volatilityAnalysisTool = createTool({ } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Volatility analysis input streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) + logToolHookStart('Volatility analysis', toolCallId, messages, abortSignal) }, onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Volatility analysis received input chunk', { + logToolHookDelta( + 'Volatility analysis', toolCallId, inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) + messages, + abortSignal + ) }, onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Volatility analysis received input', { - toolCallId, - messageCount: messages.length, - inputData: { period: input.period }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', + logToolHookAvailable('Volatility analysis', toolCallId, messages, abortSignal, { + period: input.period, + stdDev: input.stdDev, + dataLength: input.data.length, }) }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Volatility analysis completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Volatility analysis', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -1247,7 +1388,7 @@ export const volumeAnalysisTool = createTool({ try { const { high, low, close, volume, period: rawPeriod } = inputData const period = rawPeriod ?? 14 - const results: Record = { + const results: VolumeAnalysisResult = { success: true, obv: OBV.calculate({ close, volume }), adl: ADL.calculate({ high, low, close, volume }), @@ -1271,14 +1412,7 @@ export const volumeAnalysisTool = createTool({ id: 'volume-analysis', }) - return results as unknown as { - success: boolean - obv?: number[] - adl?: number[] - mfi?: number[] - vwap?: number[] - message?: string - } + return results } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -1299,38 +1433,29 @@ export const volumeAnalysisTool = createTool({ } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Volume analysis input streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) + logToolHookStart('Volume analysis', toolCallId, messages, abortSignal) }, onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Volume analysis received input chunk', { + logToolHookDelta( + 'Volume analysis', toolCallId, inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) + messages, + abortSignal + ) }, onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Volume analysis received input', { - toolCallId, - messageCount: messages.length, - inputData: { period: input.period }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', + logToolHookAvailable('Volume analysis', toolCallId, messages, abortSignal, { + period: input.period, + highLength: input.high.length, + lowLength: input.low.length, + closeLength: input.close.length, + volumeLength: input.volume.length, }) }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Volume analysis completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Volume analysis', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -1403,7 +1528,7 @@ export const statisticalAnalysisTool = createTool({ try { const { data, dataX, dataY } = inputData - const results: Record = { success: true } + const results: StatisticalAnalysisResult = { success: true } if (data && data.length > 0) { results.stats = { @@ -1445,26 +1570,7 @@ export const statisticalAnalysisTool = createTool({ id: 'statistical-analysis', }) - return results as unknown as { - success: boolean - stats?: { - mean: number - median: number - mode: number - standardDeviation: number - variance: number - min: number - max: number - skewness: number - kurtosis: number - } - regression?: { - m: number - b: number - } - correlation?: number - message?: string - } + return results } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -1485,38 +1591,27 @@ export const statisticalAnalysisTool = createTool({ } }, onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Statistical analysis input streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) + logToolHookStart('Statistical analysis', toolCallId, messages, abortSignal) }, onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Statistical analysis received input chunk', { + logToolHookDelta( + 'Statistical analysis', toolCallId, inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) + messages, + abortSignal + ) }, onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Statistical analysis received input', { - toolCallId, - messageCount: messages.length, - inputData: { hasData: !!input.data }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', + logToolHookAvailable('Statistical analysis', toolCallId, messages, abortSignal, { + dataLength: input.data?.length ?? 0, + dataXLength: input.dataX?.length ?? 0, + dataYLength: input.dataY?.length ?? 0, }) }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Statistical analysis completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Statistical analysis', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -1545,6 +1640,17 @@ export const heikinAshiTool = createTool({ .optional(), message: z.string().optional(), }), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + logToolHookStart('Heikin Ashi', toolCallId, messages, abortSignal) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta('Heikin Ashi', toolCallId, inputTextDelta, messages, abortSignal) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + logToolHookAvailable('Heikin Ashi', toolCallId, messages, abortSignal, { + dataLength: input.close.length, + }) + }, execute: async (inputData, context) => { const writer = context?.writer const abortSignal = context?.abortSignal @@ -1627,39 +1733,9 @@ export const heikinAshiTool = createTool({ return { success: false, message: errorMessage } } }, - onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Heikin Ashi streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) - }, - onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Heikin Ashi received input chunk', { - toolCallId, - inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) - }, - onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Heikin Ashi received input', { - toolCallId, - messageCount: messages.length, - inputData: { dataLength: input.close.length }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', - }) - }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Heikin Ashi completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Heikin Ashi', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -1688,6 +1764,27 @@ export const marketSummaryTool = createTool({ indicators: z.record(z.string(), z.string()), message: z.string().optional(), }), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + logToolHookStart('Market summary', toolCallId, messages, abortSignal) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta( + 'Market summary', + toolCallId, + inputTextDelta, + messages, + abortSignal + ) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + logToolHookAvailable('Market summary', toolCallId, messages, abortSignal, { + openLength: input.open.length, + highLength: input.high.length, + lowLength: input.low.length, + closeLength: input.close.length, + volumeLength: input.volume.length, + }) + }, execute: async (inputData, context) => { const writer = context?.writer const abortSignal = context?.abortSignal @@ -1794,7 +1891,12 @@ export const marketSummaryTool = createTool({ sentiment = 'Sell' } - const finalResult = { success: true, sentiment, score, indicators } + const finalResult: MarketSummaryResult = { + success: true, + sentiment, + score, + indicators, + } toolSpan?.update({ output: finalResult, @@ -1832,62 +1934,20 @@ export const marketSummaryTool = createTool({ }, id: 'market-summary', }) - return { + const errorResult: MarketSummaryResult = { success: false, sentiment: 'Neutral', score: 0, indicators: {}, message: errorMessage, - } as unknown as { - success: boolean - sentiment: - | 'Strong Buy' - | 'Buy' - | 'Neutral' - | 'Sell' - | 'Strong Sell' - score: number - indicators: Record - message?: string } + return errorResult } }, - onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Market summary streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) - }, - onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Market summary received input chunk', { - toolCallId, - inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) - }, - onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Market summary received input', { - toolCallId, - messageCount: messages.length, - inputData: { dataLength: input.close.length }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', - }) - }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Market summary completed', { - toolCallId, - toolName, - outputData: { - success: output.success, - sentiment: output.sentiment, - }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Market summary', toolCallId, toolName, abortSignal, { + success: output.success, + sentiment: output.sentiment, }) }, }) @@ -1906,6 +1966,23 @@ export const candlestickPatternTool = createTool({ patterns: z.record(z.string(), z.boolean()).optional(), message: z.string().optional(), }), + onInputStart: ({ toolCallId, messages, abortSignal }) => { + logToolHookStart('Pattern detection', toolCallId, messages, abortSignal) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta( + 'Pattern detection', + toolCallId, + inputTextDelta, + messages, + abortSignal + ) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + logToolHookAvailable('Pattern detection', toolCallId, messages, abortSignal, { + dataLength: input.close.length, + }) + }, execute: async (inputData, context) => { const writer = context?.writer const abortSignal = context?.abortSignal @@ -2005,39 +2082,9 @@ export const candlestickPatternTool = createTool({ return { success: false, message: errorMessage } } }, - onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Pattern detection streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) - }, - onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Pattern detection received input chunk', { - toolCallId, - inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) - }, - onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Pattern detection received input', { - toolCallId, - messageCount: messages.length, - inputData: { dataLength: input.close.length }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', - }) - }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Pattern detection completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Pattern detection', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) @@ -2128,7 +2175,30 @@ export const technicalAnalysisTool = createTool({ .optional(), message: z.string().optional(), }), - + onInputStart: ({ toolCallId, messages, abortSignal }) => { + logToolHookStart('Technical analysis tool', toolCallId, messages, abortSignal) + }, + onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { + logToolHookDelta( + 'Technical analysis tool', + toolCallId, + inputTextDelta, + messages, + abortSignal + ) + }, + onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { + logToolHookAvailable( + 'Technical analysis tool', + toolCallId, + messages, + abortSignal, + { + operation: input.operation, + dataLength: input.data.length, + } + ) + }, execute: async (inputData, context) => { const writer = context?.writer const abortSignal = context?.abortSignal @@ -2186,8 +2256,8 @@ export const technicalAnalysisTool = createTool({ volume: undefined as number[] | undefined, } const params = { ...defaultParams, ...(inputParams ?? {}) } - const results: Record = {} - let stats = undefined + const results: TechnicalAnalysisSeriesMap = {} + let stats: TechnicalAnalysisStats | undefined = undefined if (data.length === 0) { throw new Error('Data series cannot be empty') @@ -2226,14 +2296,22 @@ export const technicalAnalysisTool = createTool({ } if (operation === 'macd' || operation === 'all') { - results.macd = MACD.calculate({ - values: data, - fastPeriod: params.fastPeriod ?? 12, - slowPeriod: params.slowPeriod ?? 26, - signalPeriod: params.signalPeriod ?? 9, - SimpleMAOscillator: false, - SimpleMASignal: false, - }) + results.macd = (() => { + const macdValues: MACDOutput[] = MACD.calculate({ + values: data, + fastPeriod: params.fastPeriod ?? 12, + slowPeriod: params.slowPeriod ?? 26, + signalPeriod: params.signalPeriod ?? 9, + SimpleMAOscillator: false, + SimpleMASignal: false, + }).map((entry): MACDOutput => ({ + MACD: entry.MACD ?? 0, + signal: entry.signal ?? 0, + histogram: entry.histogram ?? 0, + })) + + return macdValues + })() } if (operation === 'bollinger' || operation === 'all') { @@ -2266,7 +2344,7 @@ export const technicalAnalysisTool = createTool({ } } - const finalResult = { + const finalResult: TechnicalAnalysisResult = { success: true, results, stats, @@ -2319,42 +2397,9 @@ export const technicalAnalysisTool = createTool({ } } }, - onInputStart: ({ toolCallId, messages, abortSignal }) => { - log.info('Technical analysis tool input streaming started', { - toolCallId, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputStart', - }) - }, - onInputDelta: ({ inputTextDelta, toolCallId, messages, abortSignal }) => { - log.info('Technical analysis tool received input chunk', { - toolCallId, - inputTextDelta, - messageCount: messages.length, - abortSignal: abortSignal?.aborted, - hook: 'onInputDelta', - }) - }, - onInputAvailable: ({ input, toolCallId, messages, abortSignal }) => { - log.info('Technical analysis tool received input', { - toolCallId, - messageCount: messages.length, - inputData: { - operation: input.operation, - dataLength: input.data.length, - }, - abortSignal: abortSignal?.aborted, - hook: 'onInputAvailable', - }) - }, onOutput: ({ output, toolCallId, toolName, abortSignal }) => { - log.info('Technical analysis tool completed', { - toolCallId, - toolName, - outputData: { success: output.success }, - abortSignal: abortSignal?.aborted, - hook: 'onOutput', + logToolHookOutput('Technical analysis tool', toolCallId, toolName, abortSignal, { + success: output.success, }) }, }) diff --git a/src/mastra/tools/yahoo-finance-stock.tool.ts b/src/mastra/tools/yahoo-finance-stock.tool.ts index 940025c7..2d6bd22a 100644 --- a/src/mastra/tools/yahoo-finance-stock.tool.ts +++ b/src/mastra/tools/yahoo-finance-stock.tool.ts @@ -1,7 +1,7 @@ import { SpanType, getOrCreateSpan } from '@mastra/core/observability' import type { TracingContext } from '@mastra/core/observability' import type { RequestContext } from '@mastra/core/request-context' -import { createTool, type InferUITool } from '@mastra/core/tools' +import { createTool, InferToolInput, InferToolOutput, type InferUITool } from '@mastra/core/tools' import { z } from 'zod' import { log } from '../config/logger' import { httpFetch } from '../lib/http-client' @@ -359,5 +359,7 @@ export const yahooFinanceStockQuotesTool = createTool({ }, }) -export type YahooFinanceStockQuotesInput = YahooInput + export type YahooFinanceStockQuotesUITool = InferUITool +export type YahooFinanceStockQuotesOutput = InferToolOutput +export type YahooFinanceStockQuotesInput = InferToolInput diff --git a/src/mastra/workspaces.ts b/src/mastra/workspaces.ts index 2aafbb51..96d2d55e 100644 --- a/src/mastra/workspaces.ts +++ b/src/mastra/workspaces.ts @@ -1,17 +1,69 @@ import type { LSPConfig, LSPDiagnostic, - LSPServerDef, + //LSPServerDef, Lifecycle, LocalFilesystemOptions, LocalSandboxOptions, SandboxLifecycle, SkillSearchOptions, - SandboxLifecycleHook, - FilesystemLifecycle, + FilesystemLifecycleHook, MastraFilesystemOptions, ProcessHandle, SandboxProcessManager, + SandboxDetectionResult, + SandboxError, + SandboxLifecycleHook, + SandboxExecutionError, + SandboxFeatureNotSupportedError, + SandboxInfo, + SandboxNotAvailableError, + SandboxNotReadyError, + SandboxOperation, + SandboxTimeoutError, + SearchNotAvailableError, + SkillSearchResult, + Skill, + SkillFormat, + SkillMetadata, + SkillPublishResult, + SkillSource, + StaleFileError, + SkillsContext, + SkillsResolver, + SkillSourceStat, + SkillSourceEntry, + SpawnProcessOptions, + ProcessInfo, + ListOptions, + FilesystemLifecycle, + FileContent, + FilesystemError, + FilesystemMountConfig, + FileEntry, + FileStat, + FileExistsError, + FileNotFoundError, + FileReadRequiredError, + FilesystemIcon, + FilesystemInfo, + FilesystemNotAvailableError, + FilesystemNotMountableError, + FilesystemNotReadyError, + WorkspaceNotAvailableError, + WorkspaceInfo, + WorkspaceSandbox, + WorkspaceFilesystem, + WorkspaceConfig, + WorkspaceError, + WorkspaceNotReadyError, + WorkspaceSkills, + WorkspaceStatus, + WriteOptions, + WorkspaceReadOnlyError, + MastraSandbox, + MastraFilesystem, + MastraSandboxOptions } from '@mastra/core/workspace' import { Workspace, @@ -19,7 +71,7 @@ import { LocalSandbox, WORKSPACE_TOOLS, } from '@mastra/core/workspace' -import type { ProcessInfo, SpawnProcessOptions } from '@mastra/core/workspace' +//import { createFilesystem } from '@mastra/core/workspace/filesystem' import { DaytonaSandbox } from '@mastra/daytona' import { AgentFSFilesystem } from '@mastra/agentfs' import { AgentFS } from 'agentfs-sdk' @@ -31,13 +83,26 @@ import { } from 'vscode-jsonrpc/node' import { libsqlvector } from './config/libsql' import { embed } from 'ai' -import { ModelRouterEmbeddingModel } from '@mastra/core/llm' -import { VersionedSkillSource } from '@mastra/core/workspace' + +//import { VersionedSkillSource } from '@mastra/core/workspace' +//import { SandboxContent } from '../components/ai-elements/sandbox'; +import { fastembed } from '@mastra/fastembed' +import { S3Filesystem } from '@mastra/s3' + + +export const s3filesystem = new S3Filesystem({ + bucket: 'my-bucket', + region: 'us-east-1', + endpoint: 'http://localhost:9000', + accessKeyId: 'minioadmin', + secretAccessKey: 'minioadmin', +}) + export const localWorkspacePath = process.env.WORKSPACE_PATH ?? './workspace' export const daytonaWorkspacePath = process.env.DAYTONA_WORKSPACE_PATH ?? './daytona-workspace' -export const workspaceSkillPaths = ['.agents/skills', '/skills', './**/skills'] +export const workspaceSkillPaths = [ 'skills', './**/skills'] export const agentFsAgentId = process.env.AGENTFS_AGENT_ID ?? 'agentFs-db' export const sandboxPathEnv = process.env.PATH ?? '' @@ -52,6 +117,9 @@ export const filesystemLifecycleState: FilesystemLifecycle = { status: 'pending', } +export const filesystemLifecycleStateHook: FilesystemLifecycle = { + status: 'pending', +} export const sandboxLifecycleState: SandboxLifecycle = { status: 'pending', } @@ -94,10 +162,6 @@ export const sandboxStopHook: SandboxLifecycleHook = async ({ sandbox }) => { } export const workspaceLspConfig: LSPConfig = { - binaryOverrides: { - typescript: 'typescript-language-server --stdio', - eslint: 'vscode-eslint-language-server --stdio', - }, searchPaths: [process.cwd()], root: localWorkspacePath, diagnosticTimeout: 5000, @@ -107,10 +171,6 @@ export const workspaceLspConfig: LSPConfig = { } export const daytonaLspConfig: LSPConfig = { - binaryOverrides: { - typescript: 'typescript-language-server --stdio', - eslint: 'vscode-eslint-language-server --stdio', - }, root: daytonaWorkspacePath, diagnosticTimeout: 5_000, initTimeout: 15_000, @@ -119,63 +179,10 @@ export const daytonaLspConfig: LSPConfig = { packageRunner: 'npx --yes', } -export const workspaceLspServers: LSPServerDef[] = [ - { - id: 'typescript', - name: 'TypeScript Language Server', - languageIds: [ - 'typescript', - 'typescriptreact', - 'javascript', - 'javascriptreact', - ], - markers: ['package.json', 'tsconfig.json', 'jsconfig.json'], - command: () => 'typescript-language-server --stdio', - initialization: () => ({ - preferences: { - includeCompletionsForModuleExports: true, - }, - }), - }, - { - id: 'eslint', - name: 'ESLint Language Server', - languageIds: [ - 'typescript', - 'typescriptreact', - 'javascript', - 'javascriptreact', - ], - markers: ['eslint.config.js', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json'], - command: () => 'vscode-eslint-language-server --stdio', - initialization: () => ({ - validate: 'on', - }), - }, -] export const workspaceLspDiagnostics: LSPDiagnostic[] = [] export const agentFsSdk = AgentFS -export const localTypescriptOnlyLspConfig: LSPConfig = { - ...workspaceLspConfig, - disableServers: ['eslint'], -} - -export const localEslintOnlyLspConfig: LSPConfig = { - ...workspaceLspConfig, - disableServers: ['typescript'], -} - -export const daytonaTypescriptOnlyLspConfig: LSPConfig = { - ...daytonaLspConfig, - disableServers: ['eslint'], -} - -export const daytonaEslintOnlyLspConfig: LSPConfig = { - ...daytonaLspConfig, - disableServers: ['typescript'], -} export const workspaceSpawnOptions: SpawnProcessOptions = { cwd: localWorkspacePath, @@ -302,9 +309,7 @@ export const mainWorkspace = new Workspace({ vectorStore: libsqlvector, embedder: async (text: string) => { const { embedding } = await embed({ - model: new ModelRouterEmbeddingModel( - 'google/gemini-embedding-2-preview' - ), + model: fastembed.base, value: text, }) return embedding @@ -337,100 +342,32 @@ export const mainWorkspace = new Workspace({ }, }, }, + + }, skills: ['/skills'], bm25: { k1: 1.5, b: 0.75, }, + autoIndexPaths: ['/docs', '/support/faq', '/skills', '/**/*.md', '/**/*.txt'], //skillSource: new VersionedSkillSource(versionTree, blobStore, versionCreatedAt), }) -export const localFilesystemOnlyWorkspace = new Workspace({ - id: 'local-filesystem-only-workspace', - name: 'LocalFilesystemOnlyWorkspace', - filesystem: new LocalFilesystem(mainFilesystemOptions), - skills: workspaceSkillPaths, - bm25: true, -}) - -export const localSandboxOnlyWorkspace = new Workspace({ - id: 'local-sandbox-only-workspace', - name: 'LocalSandboxOnlyWorkspace', - sandbox: new LocalSandbox(mainSandboxOptions), - lsp: workspaceLspConfig, -}) - -export const localReadOnlyWorkspace = new Workspace({ - id: 'local-readonly-workspace', - name: 'LocalReadonlyWorkspace', - filesystem: new LocalFilesystem({ - ...mainFilesystemOptions, - id: 'filesystem-readonly', - readOnly: true, - }), - sandbox: new LocalSandbox(mainSandboxOptions), - lsp: workspaceLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) - -export const localApprovalWorkspace = new Workspace({ - id: 'local-approval-workspace', - name: 'LocalApprovalWorkspace', - filesystem: new LocalFilesystem(mainFilesystemOptions), - sandbox: new LocalSandbox(mainSandboxOptions), - lsp: workspaceLspConfig, - tools: { - [WORKSPACE_TOOLS.FILESYSTEM.WRITE_FILE]: { - requireReadBeforeWrite: true, - }, - [WORKSPACE_TOOLS.FILESYSTEM.EDIT_FILE]: { - requireReadBeforeWrite: true, - }, - [WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: { - requireApproval: true, - }, - [WORKSPACE_TOOLS.FILESYSTEM.AST_EDIT]: { - requireReadBeforeWrite: true, - }, - [WORKSPACE_TOOLS.FILESYSTEM.DELETE]: { - requireApproval: true, - }, +const handle = await mainSandbox.processes.spawn('typescript-language-server --stdio', { + cwd: '/', + env: { + ...process.env, + NODE_ENV: 'development', }, - skills: workspaceSkillPaths, - bm25: true, + timeout: 60_000, }) -export const localLspWorkspace = new Workspace({ - id: 'local-lsp-workspace', - name: 'LocalLspWorkspace', - filesystem: new LocalFilesystem(mainFilesystemOptions), - sandbox: new LocalSandbox(mainSandboxOptions), - lsp: workspaceLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) - -export const localTypescriptLspWorkspace = new Workspace({ - id: 'local-typescript-lsp-workspace', - name: 'LocalTypescriptLspWorkspace', - filesystem: new LocalFilesystem(mainFilesystemOptions), - sandbox: new LocalSandbox(mainSandboxOptions), - lsp: localTypescriptOnlyLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) - -export const localEslintLspWorkspace = new Workspace({ - id: 'local-eslint-lsp-workspace', - name: 'LocalEslintLspWorkspace', - filesystem: new LocalFilesystem(mainFilesystemOptions), - sandbox: new LocalSandbox(mainSandboxOptions), - lsp: localEslintOnlyLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) +const connection = createMessageConnection( + new StreamMessageReader(handle.reader), + new StreamMessageWriter(handle.writer), +) +connection.listen() export const agentFsWorkspace = new Workspace({ id: 'agentfs-workspace', @@ -456,9 +393,7 @@ export const agentFsWorkspace = new Workspace({ vectorStore: libsqlvector, embedder: async (text: string) => { const { embedding } = await embed({ - model: new ModelRouterEmbeddingModel( - 'google/gemini-embedding-2-preview' - ), + model: fastembed.base, value: text, }) return embedding @@ -467,25 +402,6 @@ export const agentFsWorkspace = new Workspace({ bm25: true, }) -export const agentFsReadOnlyWorkspace = new Workspace({ - id: 'agentfs-readonly-workspace', - name: 'AgentFSReadonlyWorkspace', - filesystem: new AgentFSFilesystem( - typeof agentFsDbPath === 'string' && agentFsDbPath.length > 0 - ? { - id: 'agentfs-filesystem-readonly', - path: agentFsDbPath, - readOnly: true, - } - : { - id: 'agentfs-filesystem-readonly', - agentId: agentFsAgentId, - readOnly: true, - } - ), - skills: workspaceSkillPaths, - bm25: true, -}) export const daytonaSandbox = new DaytonaSandbox({ id: 'daytona-sandbox', @@ -507,76 +423,12 @@ export const daytonaWorkspace = new Workspace({ bm25: true, }) -export const daytonaLspWorkspace = new Workspace({ - id: 'daytona-lsp-workspace', - name: 'DaytonaLspWorkspace', - filesystem: new LocalFilesystem(daytonaFilesystemOptions), - sandbox: new DaytonaSandbox({ - id: 'daytona-lsp-sandbox', - name: 'Daytona LSP Sandbox', - image: 'node:20-slim', - resources: { cpu: 2, memory: 4, disk: 6 }, - language: 'typescript', - timeout: 120_000, - apiKey: process.env.DAYTONA_API_KEY, - apiUrl: process.env.DAYTONA_API_URL, - }), - lsp: daytonaLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) - -export const daytonaTypescriptLspWorkspace = new Workspace({ - id: 'daytona-typescript-lsp-workspace', - name: 'DaytonaTypescriptLspWorkspace', - filesystem: new LocalFilesystem(daytonaFilesystemOptions), - sandbox: new DaytonaSandbox({ - id: 'daytona-typescript-lsp-sandbox', - name: 'Daytona TypeScript LSP Sandbox', - image: 'node:20-slim', - resources: { cpu: 2, memory: 4, disk: 6 }, - language: 'typescript', - timeout: 120_000, - apiKey: process.env.DAYTONA_API_KEY, - apiUrl: process.env.DAYTONA_API_URL, - }), - lsp: daytonaTypescriptOnlyLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) -export const daytonaEslintLspWorkspace = new Workspace({ - id: 'daytona-eslint-lsp-workspace', - name: 'DaytonaEslintLspWorkspace', - filesystem: new LocalFilesystem(daytonaFilesystemOptions), - sandbox: new DaytonaSandbox({ - id: 'daytona-eslint-lsp-sandbox', - name: 'Daytona ESLint LSP Sandbox', - image: 'node:20-slim', - resources: { cpu: 2, memory: 4, disk: 6 }, - language: 'typescript', - timeout: 120_000, - apiKey: process.env.DAYTONA_API_KEY, - apiUrl: process.env.DAYTONA_API_URL, - }), - lsp: daytonaEslintOnlyLspConfig, - skills: workspaceSkillPaths, - bm25: true, -}) export const workspaceVariants = { mainWorkspace, - localFilesystemOnlyWorkspace, - localSandboxOnlyWorkspace, - localReadOnlyWorkspace, - localApprovalWorkspace, - localLspWorkspace, - localTypescriptLspWorkspace, - localEslintLspWorkspace, agentFsWorkspace, - agentFsReadOnlyWorkspace, daytonaWorkspace, - daytonaLspWorkspace, - daytonaTypescriptLspWorkspace, - daytonaEslintLspWorkspace, + // Add more workspace variants here + // variantName: new Workspace({ ... }) } as const diff --git a/src/scripts/seed-agent.ts b/src/scripts/seed-agent.ts new file mode 100644 index 00000000..daea8693 --- /dev/null +++ b/src/scripts/seed-agent.ts @@ -0,0 +1,20 @@ +import { google } from '@ai-sdk/google' +import { mastra } from '../mastra' + +const editor = mastra.getEditor() +if (!editor) throw new Error('Editor is not registered on Mastra') + +// Create a stored agent override for an existing code-defined agent +await editor.agent.create({ + id: 'support-agent', + name: 'Support Agent', + instructions: 'You are a friendly support agent for Acme Inc.', + model: { + provider: 'google', + model: 'gemini-3.0-flash-preview', + name: 'google/gemini-3.0-flash-preview', + }, + tools: { + search_kb: { description: 'Search the Acme knowledge base' }, + }, +}) diff --git a/src/scripts/seed-prompts.ts b/src/scripts/seed-prompts.ts new file mode 100644 index 00000000..94ae031c --- /dev/null +++ b/src/scripts/seed-prompts.ts @@ -0,0 +1,11 @@ +import { mastra } from '../mastra' + +const editor = mastra.getEditor()! + +await editor.prompt.create({ + id: 'brand-voice', + name: 'Brand voice', + description: 'Acme Inc. tone and style guidelines', + content: + 'You write in a friendly, concise tone. Always address the user as {{userName || "there"}}.', +}) \ No newline at end of file diff --git a/src/scripts/update-agent.ts b/src/scripts/update-agent.ts new file mode 100644 index 00000000..8fab3043 --- /dev/null +++ b/src/scripts/update-agent.ts @@ -0,0 +1,9 @@ +import { mastra } from '../mastra' + +const editor = mastra.getEditor()! + +await editor.agent.update({ + id: 'support-agent', + instructions: + "You are a friendly support agent for Acme Inc. Always respond in the user's language.", +}) diff --git a/src/scripts/update-prompts.ts b/src/scripts/update-prompts.ts new file mode 100644 index 00000000..23faf987 --- /dev/null +++ b/src/scripts/update-prompts.ts @@ -0,0 +1,8 @@ +import { mastra } from '../mastra' + +const editor = mastra.getEditor()! + +await editor.prompt.update({ + id: 'brand-voice', + content: 'You write in a friendly, concise tone. Always greet the user by name when available.', +})