diff --git a/examples/README.md b/examples/README.md index 5b3b9d42e..fd97d2732 100644 --- a/examples/README.md +++ b/examples/README.md @@ -108,6 +108,7 @@ Create a multi-agent research workflow where different AI agents collaborate to - [Hugging Face (MCP)](./with-hugging-face-mcp) — Access HF tools and models through MCP from agents. - [JWT Auth](./with-jwt-auth) — Protect agent endpoints with JWT verification and helpers. - [Langfuse](./with-langfuse) — Send traces and metrics to Langfuse for observability. +- [Feedback Templates](./with-feedback) — Configure per-agent feedback templates for thumbs, numeric, and categorical feedback. - [Live Evals](./with-live-evals) — Run online evaluations against prompts/agents during development. - [MCP Basics](./with-mcp) — Connect to MCP servers and call tools from an agent. - [MCP Elicitation](./with-mcp-elicitation) — Handle `elicitation/create` requests from MCP tools with per-request handlers. diff --git a/examples/with-feedback/.env.example b/examples/with-feedback/.env.example new file mode 100644 index 000000000..202d9d913 --- /dev/null +++ b/examples/with-feedback/.env.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY=your_openai_api_key_here +VOLTAGENT_PUBLIC_KEY=pk_your_public_key_here +VOLTAGENT_SECRET_KEY=sk_your_secret_key_here diff --git a/examples/with-feedback/.gitignore b/examples/with-feedback/.gitignore new file mode 100644 index 000000000..122750dbf --- /dev/null +++ b/examples/with-feedback/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.DS_Store +.env +.voltagent/ diff --git a/examples/with-feedback/README.md b/examples/with-feedback/README.md new file mode 100644 index 000000000..7124eb9b3 --- /dev/null +++ b/examples/with-feedback/README.md @@ -0,0 +1,57 @@ +# VoltAgent Feedback Templates + +This example shows how to configure different feedback templates per agent so the Console can render thumbs, numeric sliders, and categorical options. + +## Agents + +- `thumbs` - boolean satisfaction signal (thumbs up/down) +- `rating` - continuous 1-5 rating slider +- `issues` - categorical issue type selection + +## Setup + +1. Install dependencies: + +```bash +pnpm install +``` + +2. Copy the env file and set your keys: + +```bash +cp .env.example .env +``` + +Update `OPENAI_API_KEY`, `VOLTAGENT_PUBLIC_KEY`, and `VOLTAGENT_SECRET_KEY` in `.env`. + +3. Start the server: + +```bash +pnpm dev +``` + +The server runs on `http://localhost:3141` by default. + +## Usage + +Send a message to any agent: + +```bash +curl -X POST http://localhost:3141/agents/thumbs/chat \ + -H "Content-Type: application/json" \ + -d '{"input":"Give me a quick summary of VoltAgent."}' +``` + +```bash +curl -X POST http://localhost:3141/agents/rating/chat \ + -H "Content-Type: application/json" \ + -d '{"input":"Explain vector search in two sentences."}' +``` + +```bash +curl -X POST http://localhost:3141/agents/issues/chat \ + -H "Content-Type: application/json" \ + -d '{"input":"Draft a short welcome email."}' +``` + +Open the Console Observability Playground to test the feedback UI on each agent response. diff --git a/examples/with-feedback/package.json b/examples/with-feedback/package.json new file mode 100644 index 000000000..a9075f3dd --- /dev/null +++ b/examples/with-feedback/package.json @@ -0,0 +1,36 @@ +{ + "name": "voltagent-example-with-feedback", + "author": "", + "dependencies": { + "@ai-sdk/openai": "^3.0.0", + "@voltagent/cli": "^0.1.21", + "@voltagent/core": "^2.0.10", + "@voltagent/logger": "^2.0.2", + "@voltagent/server-hono": "^2.0.3", + "ai": "^6.0.0" + }, + "devDependencies": { + "@types/node": "^24.2.1", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + }, + "keywords": [ + "agent", + "ai", + "voltagent" + ], + "license": "MIT", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/VoltAgent/voltagent.git", + "directory": "examples/with-feedback" + }, + "scripts": { + "build": "tsc", + "dev": "tsx watch --env-file=.env ./src", + "start": "node dist/index.js", + "volt": "volt" + }, + "type": "module" +} diff --git a/examples/with-feedback/src/index.ts b/examples/with-feedback/src/index.ts new file mode 100644 index 000000000..7deca61b5 --- /dev/null +++ b/examples/with-feedback/src/index.ts @@ -0,0 +1,67 @@ +import { openai } from "@ai-sdk/openai"; +import { Agent, VoltAgent } from "@voltagent/core"; +import { createPinoLogger } from "@voltagent/logger"; +import { honoServer } from "@voltagent/server-hono"; + +const logger = createPinoLogger({ + name: "with-feedback", + level: "info", +}); + +const thumbsAgent = new Agent({ + name: "Thumbs Feedback Agent", + instructions: "You are a helpful assistant. Keep replies short and clear.", + model: openai("gpt-4o-mini"), + feedback: { + key: "satisfaction", + feedbackConfig: { + type: "categorical", + categories: [ + { value: 1, label: "Helpful" }, + { value: 0, label: "Not helpful" }, + ], + }, + }, +}); + +const ratingAgent = new Agent({ + name: "Rating Feedback Agent", + instructions: "Respond in concise bullet points.", + model: openai("gpt-4o-mini"), + feedback: { + key: "relevance_score", + feedbackConfig: { + type: "continuous", + min: 1, + max: 5, + }, + }, +}); + +const issuesAgent = new Agent({ + name: "Issue Tagging Agent", + instructions: "Answer clearly and include a short summary.", + model: openai("gpt-4o-mini"), + feedback: { + key: "issue_type", + feedbackConfig: { + type: "categorical", + categories: [ + { value: 1, label: "Incorrect" }, + { value: 2, label: "Incomplete" }, + { value: 3, label: "Unsafe" }, + { value: 4, label: "Other" }, + ], + }, + }, +}); + +new VoltAgent({ + agents: { + thumbs: thumbsAgent, + rating: ratingAgent, + issues: issuesAgent, + }, + server: honoServer(), + logger, +}); diff --git a/examples/with-feedback/tsconfig.json b/examples/with-feedback/tsconfig.json new file mode 100644 index 000000000..cee90c6f3 --- /dev/null +++ b/examples/with-feedback/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/src/agent/agent.ts b/packages/core/src/agent/agent.ts index 7f681bf04..91026e1e9 100644 --- a/packages/core/src/agent/agent.ts +++ b/packages/core/src/agent/agent.ts @@ -117,6 +117,8 @@ import type { VoltAgentTextStreamPart } from "./subagent/types"; import type { AgentEvalConfig, AgentEvalOperationType, + AgentFeedbackMetadata, + AgentFeedbackOptions, AgentFullState, AgentGuardrailState, AgentOptions, @@ -133,6 +135,7 @@ import type { const BUFFER_CONTEXT_KEY = Symbol("conversationBuffer"); const QUEUE_CONTEXT_KEY = Symbol("memoryPersistQueue"); const STEP_PERSIST_COUNT_KEY = Symbol("persistedStepCount"); +const DEFAULT_FEEDBACK_KEY = "satisfaction"; // ============================================================================ // Types @@ -183,6 +186,8 @@ export type StreamTextResultWithContext< toTextStreamResponse: AIStreamTextResult["toTextStreamResponse"]; // Additional context field context: Map; + // Feedback metadata for the trace, if enabled + feedback?: AgentFeedbackMetadata | null; } & Record; /** @@ -223,6 +228,8 @@ export interface GenerateTextResultWithContext< // Typed structured output override if provided by callers experimental_output: OutputValue; output: OutputValue; + // Feedback metadata for the trace, if enabled + feedback?: AgentFeedbackMetadata | null; } type LLMOperation = "streamText" | "generateText" | "streamObject" | "generateObject"; @@ -243,7 +250,7 @@ function cloneGenerateTextResultWithContext< overrides: Partial< Pick< GenerateTextResultWithContext, - "text" | "context" | "toolCalls" | "toolResults" + "text" | "context" | "toolCalls" | "toolResults" | "feedback" > >, ): GenerateTextResultWithContext { @@ -300,6 +307,19 @@ function applyForcedToolChoice( }; } +type Deferred = { + promise: Promise; + resolve: (value: T) => void; +}; + +function createDeferred(): Deferred { + let resolve!: (value: T) => void; + const promise = new Promise((resolver) => { + resolve = resolver; + }); + return { promise, resolve }; +} + /** * Base options for all generation methods * Extends AI SDK's CallSettings for full compatibility @@ -330,6 +350,7 @@ export interface BaseGenerationOptions extends Partial { // Steps control maxSteps?: number; + feedback?: boolean | AgentFeedbackOptions; /** * Custom stop condition for ai-sdk step execution. * When provided, this overrides VoltAgent's default `stepCountIs(maxSteps)`. @@ -421,6 +442,7 @@ export class Agent { private readonly voltOpsClient?: VoltOpsClient; private readonly prompts?: PromptHelper; private readonly evalConfig?: AgentEvalConfig; + private readonly feedbackOptions?: AgentFeedbackOptions | boolean; private readonly inputGuardrails: NormalizedInputGuardrail[]; private readonly outputGuardrails: NormalizedOutputGuardrail[]; private readonly observabilityAuthWarningState: ObservabilityFlushState = { @@ -446,6 +468,7 @@ export class Agent { this.context = toContextMap(options.context); this.voltOpsClient = options.voltOpsClient; this.evalConfig = options.eval; + this.feedbackOptions = options.feedback; this.inputGuardrails = normalizeInputGuardrailList(options.inputGuardrails || []); this.outputGuardrails = normalizeOutputGuardrailList(options.outputGuardrails || []); @@ -509,6 +532,10 @@ export class Agent { const startTime = Date.now(); const oc = this.createOperationContext(input, options); const methodLogger = oc.logger; + const feedbackOptions = this.resolveFeedbackOptions(options); + const feedbackClient = feedbackOptions ? this.getFeedbackClient() : undefined; + const shouldDeferPersist = Boolean(feedbackOptions && feedbackClient); + let feedbackMetadata: AgentFeedbackMetadata | null = null; // Wrap entire execution in root span for trace context const rootSpan = oc.traceContext.getRootSpan(); @@ -516,6 +543,8 @@ export class Agent { const guardrailSet = this.resolveGuardrailSets(options); const buffer = this.getConversationBuffer(oc); const persistQueue = this.getMemoryPersistQueue(oc); + const feedbackPromise = + feedbackOptions && feedbackClient ? this.createFeedbackMetadata(oc, options) : null; let effectiveInput: typeof input = input; try { effectiveInput = await executeInputGuardrails( @@ -603,6 +632,7 @@ export class Agent { parentAgentId, parentOperationContext, hooks, + feedback: _feedback, maxSteps: userMaxSteps, tools: userTools, output, @@ -672,7 +702,9 @@ export class Agent { this.recordStepResults(result.steps, oc); - await persistQueue.flush(buffer, oc); + if (!shouldDeferPersist) { + await persistQueue.flush(buffer, oc); + } const usageInfo = convertUsage(result.usage); const finalText = await executeOutputGuardrails({ @@ -749,11 +781,24 @@ export class Agent { // Close span after scheduling scorers oc.traceContext.end("completed"); + if (feedbackPromise) { + feedbackMetadata = await feedbackPromise; + } + + if (feedbackMetadata) { + buffer.addMetadataToLastAssistantMessage({ feedback: feedbackMetadata }); + } + + if (shouldDeferPersist) { + await persistQueue.flush(buffer, oc); + } + return cloneGenerateTextResultWithContext(result, { text: finalText, context: oc.context, toolCalls: aggregatedToolCalls, toolResults: aggregatedToolResults, + feedback: feedbackMetadata, }); } catch (error) { // Check if this is a BailError (subagent early termination via abort) @@ -845,6 +890,23 @@ export class Agent { ): Promise { const startTime = Date.now(); const oc = this.createOperationContext(input, options); + const feedbackOptions = this.resolveFeedbackOptions(options); + const feedbackClient = feedbackOptions ? this.getFeedbackClient() : undefined; + const shouldDeferPersist = Boolean(feedbackOptions && feedbackClient); + const feedbackDeferred = feedbackOptions + ? createDeferred() + : null; + let feedbackValue: AgentFeedbackMetadata | null = null; + let feedbackResolved = false; + let feedbackFinalizeRequested = false; + let feedbackApplied = false; + const resolveFeedbackDeferred = (value: AgentFeedbackMetadata | null) => { + if (!feedbackDeferred || feedbackResolved) { + return; + } + feedbackResolved = true; + feedbackDeferred.resolve(value); + }; // Wrap entire execution in root span to ensure all logs have trace context const rootSpan = oc.traceContext.getRootSpan(); @@ -853,6 +915,33 @@ export class Agent { const guardrailSet = this.resolveGuardrailSets(options); const buffer = this.getConversationBuffer(oc); const persistQueue = this.getMemoryPersistQueue(oc); + const scheduleFeedbackPersist = (metadata: AgentFeedbackMetadata | null) => { + if (!metadata || feedbackApplied) { + return; + } + feedbackApplied = true; + buffer.addMetadataToLastAssistantMessage({ feedback: metadata }); + if (shouldDeferPersist) { + void persistQueue.flush(buffer, oc).catch((error) => { + oc.logger?.debug?.("Failed to persist feedback metadata", { error }); + }); + } + }; + const feedbackPromise = + feedbackOptions && feedbackClient ? this.createFeedbackMetadata(oc, options) : null; + if (feedbackPromise) { + feedbackPromise + .then((metadata) => { + feedbackValue = metadata; + resolveFeedbackDeferred(metadata); + if (feedbackFinalizeRequested) { + scheduleFeedbackPersist(metadata); + } + }) + .catch(() => resolveFeedbackDeferred(null)); + } else if (feedbackDeferred) { + resolveFeedbackDeferred(null); + } let effectiveInput: typeof input = input; try { effectiveInput = await executeInputGuardrails( @@ -936,6 +1025,7 @@ export class Agent { parentAgentId, parentOperationContext, hooks, + feedback: _feedback, maxSteps: userMaxSteps, tools: userTools, onFinish: userOnFinish, @@ -1006,6 +1096,8 @@ export class Agent { return; } + resolveFeedbackDeferred(null); + // Log the error methodLogger.error("Stream error occurred", { error: actualError, @@ -1049,7 +1141,9 @@ export class Agent { finishReason: finalResult.finishReason, }); - await persistQueue.flush(buffer, oc); + if (!shouldDeferPersist) { + await persistQueue.flush(buffer, oc); + } // History update removed - using OpenTelemetry only @@ -1190,9 +1284,22 @@ export class Agent { oc.traceContext.end("completed"); - // Ensure all spans are exported on finish - // Uses waitUntil if available to avoid blocking - await flushObservability( + feedbackFinalizeRequested = true; + + if (!feedbackResolved && feedbackDeferred) { + await feedbackDeferred.promise; + } + + if (feedbackResolved && feedbackValue) { + scheduleFeedbackPersist(feedbackValue); + } else if (shouldDeferPersist) { + void persistQueue.flush(buffer, oc).catch((error) => { + oc.logger?.debug?.("Failed to persist deferred messages", { error }); + }); + } + + // Schedule span flush without blocking the response + void flushObservability( this.getObservability(), oc.logger ?? this.logger, this.observabilityAuthWarningState, @@ -1417,22 +1524,57 @@ export class Agent { }); }; + const attachFeedbackMetadata = ( + baseStream: ToUIMessageStreamReturn, + ): ToUIMessageStreamReturn => { + if (!feedbackDeferred) { + return baseStream; + } + + return createAsyncIterableReadable(async (controller) => { + const reader = (baseStream as ReadableStream).getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value !== undefined) { + controller.enqueue(value); + } + } + if (feedbackDeferred) { + await feedbackDeferred.promise; + } + if (feedbackResolved && feedbackValue) { + controller.enqueue({ + type: "message-metadata", + messageMetadata: { + feedback: feedbackValue, + }, + } as UIStreamChunk); + } + controller.close(); + } catch (error) { + controller.error(error); + } finally { + reader.releaseLock(); + } + }); + }; + const toUIMessageStreamSanitized = ( streamOptions?: ToUIMessageStreamOptions, ): ToUIMessageStreamReturn => { - if (agent.subAgentManager.hasSubAgents()) { - return createMergedUIStream(streamOptions); - } - return getGuardrailAwareUIStream(streamOptions); + const baseStream = agent.subAgentManager.hasSubAgents() + ? createMergedUIStream(streamOptions) + : getGuardrailAwareUIStream(streamOptions); + return attachFeedbackMetadata(baseStream); }; const toUIMessageStreamResponseSanitized = ( options?: ToUIMessageStreamResponseOptions, ): ReturnType => { const streamOptions = options as ToUIMessageStreamOptions | undefined; - const stream = agent.subAgentManager.hasSubAgents() - ? createMergedUIStream(streamOptions) - : getGuardrailAwareUIStream(streamOptions); + const stream = toUIMessageStreamSanitized(streamOptions); const responseInit = options ? { ...options } : {}; return createUIMessageStreamResponse({ stream, @@ -1445,9 +1587,7 @@ export class Agent { init?: Parameters[1], ): void => { const streamOptions = init as ToUIMessageStreamOptions | undefined; - const stream = agent.subAgentManager.hasSubAgents() - ? createMergedUIStream(streamOptions) - : getGuardrailAwareUIStream(streamOptions); + const stream = toUIMessageStreamSanitized(streamOptions); const initOptions = init ? { ...init } : {}; pipeUIMessageStreamToResponse({ response, @@ -1489,6 +1629,9 @@ export class Agent { }); }, context: oc.context, + get feedback() { + return feedbackValue; + }, }; return resultWithContext; @@ -1596,6 +1739,7 @@ export class Agent { parentAgentId, parentOperationContext, hooks, + feedback: _feedback, maxSteps: userMaxSteps, tools: userTools, output: _output, @@ -1830,6 +1974,7 @@ export class Agent { parentAgentId, parentOperationContext, hooks, + feedback: _feedback, maxSteps: userMaxSteps, tools: userTools, onFinish: userOnFinish, @@ -2554,6 +2699,87 @@ export class Agent { return this.defaultObservability; } + private resolveFeedbackOptions( + options?: BaseGenerationOptions, + ): AgentFeedbackOptions | undefined { + const raw = options?.feedback ?? this.feedbackOptions; + if (!raw) { + return undefined; + } + if (raw === true) { + return {}; + } + return raw; + } + + private getFeedbackTraceId(oc: OperationContext): string | undefined { + try { + return oc.traceContext.getRootSpan().spanContext().traceId; + } catch { + return undefined; + } + } + + private getFeedbackClient(): VoltOpsClient | undefined { + const voltOpsClient = + this.voltOpsClient || AgentRegistry.getInstance().getGlobalVoltOpsClient(); + if (!voltOpsClient || typeof voltOpsClient.hasValidKeys !== "function") { + return undefined; + } + if (!voltOpsClient.hasValidKeys()) { + return undefined; + } + return voltOpsClient; + } + + private async createFeedbackMetadata( + oc: OperationContext, + options?: BaseGenerationOptions, + ): Promise { + const feedbackOptions = this.resolveFeedbackOptions(options); + if (!feedbackOptions) { + return null; + } + + const voltOpsClient = this.getFeedbackClient(); + if (!voltOpsClient) { + return null; + } + + const traceId = this.getFeedbackTraceId(oc); + if (!traceId) { + return null; + } + + const key = feedbackOptions.key?.trim() || DEFAULT_FEEDBACK_KEY; + + try { + const token = await voltOpsClient.createFeedbackToken({ + traceId, + key, + feedbackConfig: feedbackOptions.feedbackConfig ?? null, + expiresAt: feedbackOptions.expiresAt, + expiresIn: feedbackOptions.expiresIn, + }); + + return { + traceId, + key, + url: token.url, + tokenId: token.id, + expiresAt: token.expiresAt, + feedbackConfig: token.feedbackConfig ?? feedbackOptions.feedbackConfig ?? null, + }; + } catch (error) { + oc.logger.debug("Failed to create feedback token", { + traceId, + key, + error, + }); + return null; + } + } + /** * Check if semantic search is supported */ diff --git a/packages/core/src/agent/conversation-buffer.ts b/packages/core/src/agent/conversation-buffer.ts index 027bedebd..365b56ae7 100644 --- a/packages/core/src/agent/conversation-buffer.ts +++ b/packages/core/src/agent/conversation-buffer.ts @@ -106,6 +106,26 @@ export class ConversationBuffer { return this.messages.map((message) => this.cloneMessage(message)); } + addMetadataToLastAssistantMessage(metadata: Record): void { + if (!metadata || Object.keys(metadata).length === 0) { + return; + } + + const lastAssistantIndex = this.findLastAssistantIndex(); + if (lastAssistantIndex === -1) { + return; + } + + const target = this.messages[lastAssistantIndex]; + const existing = + typeof target.metadata === "object" && target.metadata !== null ? target.metadata : {}; + target.metadata = { + ...(existing as Record), + ...metadata, + } as UIMessage["metadata"]; + this.pendingMessageIds.add(target.id); + } + private appendExistingMessage( message: UIMessage, options: { markAsSaved?: boolean } = { markAsSaved: true }, diff --git a/packages/core/src/agent/types.ts b/packages/core/src/agent/types.ts index dcbb4c1d2..10bfbf44c 100644 --- a/packages/core/src/agent/types.ts +++ b/packages/core/src/agent/types.ts @@ -34,6 +34,8 @@ import type { DynamicValueOptions, PromptContent, PromptHelper, + VoltOpsFeedbackConfig, + VoltOpsFeedbackExpiresIn, } from "../voltops/types"; import type { ContextInput } from "./agent"; import type { AgentHooks } from "./hooks"; @@ -51,6 +53,22 @@ export interface ApiToolInfo { parameters?: any; } +export type AgentFeedbackOptions = { + key?: string; + feedbackConfig?: VoltOpsFeedbackConfig | null; + expiresAt?: Date | string; + expiresIn?: VoltOpsFeedbackExpiresIn; +}; + +export type AgentFeedbackMetadata = { + traceId: string; + key: string; + url: string; + tokenId?: string; + expiresAt?: string; + feedbackConfig?: VoltOpsFeedbackConfig | null; +}; + /** * Tool with node_id for agent state */ @@ -466,6 +484,7 @@ export type AgentOptions = { temperature?: number; maxOutputTokens?: number; maxSteps?: number; + feedback?: AgentFeedbackOptions | boolean; /** * Default stop condition for step execution (ai-sdk `stopWhen`). * Per-call `stopWhen` in method options overrides this. @@ -667,6 +686,9 @@ export interface CommonGenerateOptions { // Maximum number of steps for this specific request (overrides agent's maxSteps) maxSteps?: number; + // Feedback configuration for trace satisfaction links + feedback?: AgentFeedbackOptions | boolean; + // AbortController for cancelling the operation and accessing the signal abortController?: AbortController; @@ -982,6 +1004,9 @@ export interface StreamTextFinishResult { /** Token usage information (if available). */ usage?: UsageInfo; + /** Feedback metadata for the trace, if enabled. */ + feedback?: AgentFeedbackMetadata | null; + /** The reason the stream finished (if available, e.g., 'stop', 'length', 'tool-calls'). */ finishReason?: string; @@ -1041,6 +1066,8 @@ export interface StandardizedTextResult { text: string; /** Token usage information (if available). */ usage?: UsageInfo; + /** Feedback metadata for the trace, if enabled. */ + feedback?: AgentFeedbackMetadata | null; /** Original provider response (if needed). */ providerResponse?: unknown; /** Finish reason (if available from provider). */ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1a0c49c94..a8fffcfa0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -147,6 +147,8 @@ export * from "./events/types"; export type { AgentOptions, AgentSummarizationOptions, + AgentFeedbackOptions, + AgentFeedbackMetadata, AgentResponse, AgentFullState, ApiToolInfo, diff --git a/packages/core/src/voltops/client.ts b/packages/core/src/voltops/client.ts index f7254b0bb..6abf89e36 100644 --- a/packages/core/src/voltops/client.ts +++ b/packages/core/src/voltops/client.ts @@ -55,6 +55,9 @@ import type { VoltOpsEvalRunSummary, VoltOpsEvalsApi, VoltOpsFailEvalRunRequest, + VoltOpsFeedbackConfig, + VoltOpsFeedbackToken, + VoltOpsFeedbackTokenCreateInput, VoltOpsPromptManager, VoltOpsScorerSummary, } from "./types"; @@ -237,6 +240,48 @@ export class VoltOpsClient implements IVoltOpsClient { return await this.fetchImpl(url, requestInit); } + public async createFeedbackToken( + input: VoltOpsFeedbackTokenCreateInput, + ): Promise { + const payload: Record = { + trace_id: input.traceId, + feedback_key: input.key, + }; + + if (input.feedbackConfig !== undefined) { + payload.feedback_config = input.feedbackConfig; + } + if (input.expiresAt !== undefined) { + payload.expires_at = input.expiresAt; + } + if (input.expiresIn !== undefined) { + payload.expires_in = input.expiresIn; + } + + const response = await this.request<{ + id?: string; + url?: string; + expires_at?: string; + feedback_config?: VoltOpsFeedbackConfig | null; + }>("POST", "/api/public/feedback/tokens", payload); + + const id = response?.id; + const url = response?.url; + const expiresAt = response?.expires_at; + const feedbackConfig = response?.feedback_config ?? input.feedbackConfig ?? null; + + if (!id || !url || !expiresAt) { + throw new Error("Failed to create feedback token via VoltOps"); + } + + return { + id, + url, + expiresAt, + feedbackConfig, + }; + } + // getObservabilityExporter removed - observability now handled by VoltAgentObservability /** diff --git a/packages/core/src/voltops/index.ts b/packages/core/src/voltops/index.ts index 87ad07433..0a630ac96 100644 --- a/packages/core/src/voltops/index.ts +++ b/packages/core/src/voltops/index.ts @@ -35,6 +35,10 @@ export type { VoltOpsCompleteEvalRunRequest, VoltOpsCreateScorerRequest, VoltOpsScorerSummary, + VoltOpsFeedbackConfig, + VoltOpsFeedbackExpiresIn, + VoltOpsFeedbackToken, + VoltOpsFeedbackTokenCreateInput, VoltOpsActionExecutionResult, VoltOpsAirtableCreateRecordParams, VoltOpsAirtableUpdateRecordParams, diff --git a/packages/core/src/voltops/types.ts b/packages/core/src/voltops/types.ts index 8d38b9c19..50cb8b35c 100644 --- a/packages/core/src/voltops/types.ts +++ b/packages/core/src/voltops/types.ts @@ -120,6 +120,39 @@ export type VoltOpsClientOptions = { }; }; +export type VoltOpsFeedbackConfig = { + type: "continuous" | "categorical" | "freeform"; + min?: number; + max?: number; + categories?: Array<{ + value: string | number; + label?: string; + description?: string; + }>; + [key: string]: any; +}; + +export type VoltOpsFeedbackExpiresIn = { + days?: number; + hours?: number; + minutes?: number; +}; + +export type VoltOpsFeedbackToken = { + id: string; + url: string; + expiresAt: string; + feedbackConfig?: VoltOpsFeedbackConfig | null; +}; + +export type VoltOpsFeedbackTokenCreateInput = { + traceId: string; + key: string; + feedbackConfig?: VoltOpsFeedbackConfig | null; + expiresAt?: Date | string; + expiresIn?: VoltOpsFeedbackExpiresIn; +}; + /** * Cached prompt data for performance optimization */ @@ -906,6 +939,9 @@ export interface VoltOpsClient { /** Evaluations API surface */ evals: VoltOpsEvalsApi; + /** Create a feedback token for the given trace */ + createFeedbackToken(input: VoltOpsFeedbackTokenCreateInput): Promise; + /** Create a prompt helper for agent instructions */ createPromptHelper(agentId: string, historyEntryId?: string): PromptHelper; diff --git a/packages/server-core/src/handlers/agent.handlers.ts b/packages/server-core/src/handlers/agent.handlers.ts index add7e75ec..247957730 100644 --- a/packages/server-core/src/handlers/agent.handlers.ts +++ b/packages/server-core/src/handlers/agent.handlers.ts @@ -92,6 +92,7 @@ export async function handleGenerateText( finishReason: result.finishReason, toolCalls: result.toolCalls, toolResults: result.toolResults, + feedback: result.feedback ?? null, // Try to access output safely - getter throws if not defined ...(() => { try { diff --git a/packages/server-core/src/schemas/agent.schemas.ts b/packages/server-core/src/schemas/agent.schemas.ts index 5de934d99..5f3c8a8d6 100644 --- a/packages/server-core/src/schemas/agent.schemas.ts +++ b/packages/server-core/src/schemas/agent.schemas.ts @@ -72,6 +72,43 @@ export const BasicJsonSchema = z .passthrough() .describe("The Zod schema for the desired object output (passed as JSON)"); +const FeedbackConfigSchema = z + .object({ + type: z.enum(["continuous", "categorical", "freeform"]), + min: z.number().optional(), + max: z.number().optional(), + categories: z + .array( + z.object({ + value: z.union([z.string(), z.number()]), + label: z.string().optional(), + description: z.string().optional(), + }), + ) + .optional(), + }) + .passthrough() + .describe("Feedback configuration for the trace"); + +const FeedbackExpiresInSchema = z + .object({ + days: z.number().int().optional(), + hours: z.number().int().optional(), + minutes: z.number().int().optional(), + }) + .passthrough() + .describe("Relative expiration for feedback tokens"); + +const FeedbackOptionsSchema = z + .object({ + key: z.string().optional().describe("Feedback key for the trace"), + feedbackConfig: FeedbackConfigSchema.nullish().optional(), + expiresAt: z.string().optional().describe("Absolute expiration timestamp (ISO 8601)"), + expiresIn: FeedbackExpiresInSchema.optional(), + }) + .passthrough() + .describe("Feedback options for the generated trace"); + // Generation options schema export const GenerateOptionsSchema = z .object({ @@ -155,6 +192,10 @@ export const GenerateOptionsSchema = z }) .optional() .describe("Structured output configuration for schema-guided generation"), + feedback: z + .union([z.boolean(), FeedbackOptionsSchema]) + .optional() + .describe("Enable or configure feedback tokens for the trace"), }) .passthrough(); @@ -189,6 +230,18 @@ export const TextResponseSchema = z.object({ toolCalls: z.array(z.any()).optional(), toolResults: z.array(z.any()).optional(), output: z.any().optional().describe("Structured output when output is used"), + feedback: z + .object({ + traceId: z.string(), + key: z.string(), + url: z.string(), + tokenId: z.string().optional(), + expiresAt: z.string().optional(), + feedbackConfig: FeedbackConfigSchema.nullish().optional(), + }) + .nullable() + .optional() + .describe("Feedback metadata for the trace"), }) .describe("AI SDK formatted response"), ]), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4562a5bf1..05407478f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1177,6 +1177,37 @@ importers: specifier: ^5.8.2 version: 5.9.2 + examples/with-feedback: + dependencies: + '@ai-sdk/openai': + specifier: ^3.0.0 + version: 3.0.1(zod@4.2.1) + '@voltagent/cli': + specifier: ^0.1.21 + version: link:../../packages/cli + '@voltagent/core': + specifier: ^2.0.10 + version: link:../../packages/core + '@voltagent/logger': + specifier: ^2.0.2 + version: link:../../packages/logger + '@voltagent/server-hono': + specifier: ^2.0.3 + version: link:../../packages/server-hono + ai: + specifier: ^6.0.0 + version: 6.0.3(zod@4.2.1) + devDependencies: + '@types/node': + specifier: ^24.2.1 + version: 24.6.2 + tsx: + specifier: ^4.19.3 + version: 4.20.4 + typescript: + specifier: ^5.8.2 + version: 5.9.3 + examples/with-google-ai: dependencies: '@ai-sdk/google': diff --git a/website/docs/agents/message-types.md b/website/docs/agents/message-types.md index 24276dfd0..67c3520dc 100644 --- a/website/docs/agents/message-types.md +++ b/website/docs/agents/message-types.md @@ -151,6 +151,22 @@ conversationHistory.forEach((msg) => { }); ``` +#### Feedback metadata + +When feedback is enabled, VoltAgent attaches feedback metadata to assistant UI messages under `message.metadata.feedback`. This is how UIs can show thumbs up/down and submit feedback later. + +```ts +const feedback = message.metadata?.feedback as + | { traceId?: string; key?: string; url?: string } + | undefined; + +if (feedback?.url) { + console.log("Submit feedback to:", feedback.url); +} +``` + +See [Feedback](/observability-docs/feedback) for the full flow and API examples. + ### 3. VoltAgentTextStreamPart (Streaming Extension) **VoltAgentTextStreamPart** extends AI SDK's `TextStreamPart` with SubAgent metadata, enabling multi-agent coordination during streaming. diff --git a/website/docs/agents/overview.md b/website/docs/agents/overview.md index 2a5fe42af..10289e8de 100644 --- a/website/docs/agents/overview.md +++ b/website/docs/agents/overview.md @@ -109,6 +109,37 @@ const [fullText, usage, finishReason] = await Promise.all([ console.log(`\nTotal: ${fullText.length} chars, ${usage?.totalTokens} tokens`); ``` +### Feedback (optional) + +If you have VoltOps API keys configured, you can enable feedback per agent or per call. VoltAgent creates a short-lived feedback token and attaches it to the assistant message metadata and the result object. + +```ts +const result = await agent.generateText("Summarize this trace", { + feedback: { + key: "satisfaction", + feedbackConfig: { + type: "categorical", + categories: [ + { value: 1, label: "Satisfied" }, + { value: 0, label: "Unsatisfied" }, + ], + }, + }, +}); + +console.log(result.feedback?.url); +``` + +If the feedback key is already registered, you can pass only `key` and let the stored config populate the token. + +```ts +const result = await agent.generateText("Quick rating?", { + feedback: { key: "satisfaction" }, +}); +``` + +For end-to-end examples (SDK, API, and useChat), see [Feedback](/observability-docs/feedback). + ### Structured Data Generation Use `output` with `generateText`/`streamText` to get structured data while still using tools and all agent capabilities. diff --git a/website/docs/observability/feedback.md b/website/docs/observability/feedback.md new file mode 100644 index 000000000..2c4a695da --- /dev/null +++ b/website/docs/observability/feedback.md @@ -0,0 +1,9 @@ +--- +title: Feedback +slug: /observability/feedback +sidebar_label: Feedback +--- + +Feedback documentation now lives in the Observability docs. + +See [Feedback in Observability docs](/observability-docs/feedback). diff --git a/website/observability/feedback.md b/website/observability/feedback.md new file mode 100644 index 000000000..84c09cf39 --- /dev/null +++ b/website/observability/feedback.md @@ -0,0 +1,253 @@ +--- +title: Feedback +--- + +# Feedback + +Feedback lets you capture user ratings and comments tied to a trace. VoltAgent can create signed feedback tokens for each trace and attach them to assistant message metadata so you can submit feedback later from the UI. + +## How it works + +- Feedback is always linked to a trace_id. +- When feedback is enabled, VoltAgent requests a short-lived token from the VoltOps API and returns metadata. +- The metadata is added to the last assistant message so you can show a UI control and submit later. + +Feedback metadata shape: + +```json +{ + "traceId": "...", + "key": "satisfaction", + "url": "https://api.voltagent.dev/api/public/feedback/ingest/...", + "tokenId": "...", + "expiresAt": "2026-01-06T18:25:26.005Z", + "feedbackConfig": { + "type": "categorical", + "categories": [ + { "value": 1, "label": "Satisfied" }, + { "value": 0, "label": "Unsatisfied" } + ] + } +} +``` + +## Feedback keys (registry) + +Feedback keys let you register a reusable schema for a signal (numeric, boolean, or categorical). The system stores keys per project and uses them to resolve `feedbackConfig` when you only pass `key`. + +- If a key exists with `feedback_config`, it is reused when `feedbackConfig` is omitted. +- If a key does not exist and you pass `feedbackConfig`, the key is created automatically. +- If a key exists, the stored config wins. Update the key if you need to change the schema. + +Manage feedback keys in the Console UI under Settings. + +## Enable feedback in the SDK + +You can enable feedback at the agent level or per request. A VoltOps client must be configured (environment or explicit) so tokens can be created. + +### Agent-level default + +```ts +import { Agent } from "@voltagent/core"; +import { openai } from "@ai-sdk/openai"; + +const agent = new Agent({ + name: "support-agent", + instructions: "Help users solve issues", + model: openai("gpt-4o-mini"), + feedback: true, +}); +``` + +### Per-call feedback options + +```ts +const result = await agent.generateText("Help me reset my password", { + feedback: { + key: "satisfaction", + feedbackConfig: { + type: "categorical", + categories: [ + { value: 1, label: "Satisfied" }, + { value: 0, label: "Unsatisfied" }, + ], + }, + expiresIn: { hours: 6 }, + }, +}); + +const feedback = result.feedback; +``` + +### Use a registered key + +If the key is already registered, you can omit `feedbackConfig` and the stored config is used. + +```ts +const result = await agent.generateText("How was the answer?", { + feedback: { key: "satisfaction" }, +}); +``` + +### Streaming feedback metadata + +For streaming, VoltAgent attaches feedback metadata to the stream wrapper returned by `agent.streamText`. The `onFinish` callback receives the underlying AI SDK `StreamTextResult`, which does not include VoltAgent feedback metadata. Read feedback from the returned stream wrapper after the stream completes. + +```ts +const stream = await agent.streamText("Explain this trace", { + feedback: true, + onFinish: async (result) => { + // result is the AI SDK StreamTextResult (no VoltAgent feedback here) + console.log(await result.text); + }, +}); + +for await (const _chunk of stream.textStream) { + // consume stream output +} + +console.log(stream.feedback); +``` + +## useChat integration + +When you use the `/agents/:id/chat` endpoint (AI SDK useChat compatible), the assistant message includes feedback metadata under `message.metadata.feedback`. You can render a thumbs up/down UI and submit feedback to `feedback.url`. + +```ts +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; + +const transport = new DefaultChatTransport({ + api: `${apiUrl}/agents/${agentId}/chat`, + prepareSendMessagesRequest({ messages }) { + const lastMessage = messages[messages.length - 1]; + return { + body: { + input: [lastMessage], + options: { + feedback: { + key: "satisfaction", + feedbackConfig: { + type: "categorical", + categories: [ + { value: 1, label: "Satisfied" }, + { value: 0, label: "Unsatisfied" }, + ], + }, + }, + }, + }, + }; + }, +}); + +const { messages } = useChat({ transport }); + +async function submitFeedback(message: any, score: number) { + const feedback = message?.metadata?.feedback; + if (!feedback?.url) return; + + await fetch(feedback.url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + score, + comment: "Helpful response", + feedback_source_type: "app", + }), + }); +} +``` + +## API usage + +Use the API directly when you are not calling the SDK or when you want a custom feedback flow. + +### Create a feedback token + +```bash +curl -X POST "https://api.voltagent.dev/api/public/feedback/tokens" \ + -H "X-Public-Key: $VOLTAGENT_PUBLIC_KEY" \ + -H "X-Secret-Key: $VOLTAGENT_SECRET_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "trace_id": "trace-id", + "feedback_key": "satisfaction", + "expires_in": { "hours": 6 }, + "feedback_config": { + "type": "categorical", + "categories": [ + { "value": 1, "label": "Satisfied" }, + { "value": 0, "label": "Unsatisfied" } + ] + } + }' +``` + +Response: + +```json +{ + "id": "token-id", + "url": "https://api.voltagent.dev/api/public/feedback/ingest/token-id", + "expires_at": "2026-01-06T18:25:26.005Z" +} +``` + +If the key is already registered, you can omit `feedback_config` and the stored config is used. + +### Submit feedback with the token + +```bash +curl -X POST "https://api.voltagent.dev/api/public/feedback/ingest/token-id" \ + -H "Content-Type: application/json" \ + -d '{ + "score": 1, + "comment": "Resolved my issue", + "feedback_source_type": "app" + }' +``` + +### Direct feedback create + +If you want to submit feedback directly (without a token), call the feedback endpoint with project keys: + +```bash +curl -X POST "https://api.voltagent.dev/api/public/feedback" \ + -H "X-Public-Key: $VOLTAGENT_PUBLIC_KEY" \ + -H "X-Secret-Key: $VOLTAGENT_SECRET_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "trace_id": "trace-id", + "key": "satisfaction", + "score": 0, + "comment": "Did not help" + }' +``` + +## Custom feedback + +Use different keys and configs to collect multiple signals. + +```ts +const result = await agent.generateText("Review this answer", { + feedback: { + key: "accuracy", + feedbackConfig: { + type: "continuous", + min: 0, + max: 5, + }, + }, +}); + +const accuracyFeedback = result.feedback; +``` + +You can also use `type: "freeform"` when you want only text feedback (no score). + +## Notes + +- If VoltOps keys are not configured, `feedback` will be null. +- Tokens expire. Use `expiresAt` or `expiresIn` to control TTL. +- Store feedback metadata with your message history so users can rate later. diff --git a/website/sidebarsObservability.ts b/website/sidebarsObservability.ts index a6d876255..455b9202a 100644 --- a/website/sidebarsObservability.ts +++ b/website/sidebarsObservability.ts @@ -20,6 +20,11 @@ const sidebars: SidebarsConfig = { id: "llm-usage-and-costs", label: "LLM Usage & Costs", }, + { + type: "doc", + id: "feedback", + label: "Feedback", + }, { type: "category", label: "Tracing", diff --git a/website/src/data/tweets.json b/website/src/data/tweets.json index 32337fee7..c08a8d2ce 100644 --- a/website/src/data/tweets.json +++ b/website/src/data/tweets.json @@ -5,7 +5,10 @@ "text": "VoltAgent = IKEA instructions for AI agents…", "created_at": "2025-04-28T20:41:15.000Z", "lang": "ca", - "display_text_range": [0, 44], + "display_text_range": [ + 0, + 44 + ], "entities": { "hashtags": [], "urls": [], @@ -27,7 +30,10 @@ "text": "🚨🚨🚨 VoltAgent is now live. 🚨🚨🚨\n\nAn open-source AI Agent Framework. \n\n🔍 Built-in observability\n🧠 Multi-agent architecture\n⚡ Supervisor-based coordination\n🪄 Native tool support & memory\n🧩 First-class TypeScript DX\n\nForget no-code lock-ins.\nEverything you need is in one https://t.co/5wcCP91jGA", "created_at": "2025-04-21T07:14:10.000Z", "lang": "en", - "display_text_range": [0, 281], + "display_text_range": [ + 0, + 281 + ], "entities": { "hashtags": [], "urls": [], @@ -35,7 +41,10 @@ "symbols": [], "media": [ { - "indices": [272, 295], + "indices": [ + 272, + 295 + ], "url": "https://t.co/5wcCP91jGA", "display_url": "pic.x.com/5wcCP91jGA", "expanded_url": "https://x.com/voltagent_dev/status/1914216071734894795/photo/1" @@ -68,13 +77,19 @@ "text": "Such a beautiful architecture diagram, Claude, ty 🥹\n\nCurrently assembling some AI agents\n\nIt's agent volt⚡️ time! \n\nRemember, @voltagent_dev = IKEA instructions for AI agents… https://t.co/rpBP28d4ei", "created_at": "2025-06-03T01:08:10.000Z", "lang": "en", - "display_text_range": [0, 177], + "display_text_range": [ + 0, + 177 + ], "entities": { "hashtags": [], "urls": [], "user_mentions": [ { - "indices": [126, 140], + "indices": [ + 126, + 140 + ], "id_str": "1912724805406863360", "name": "voltagent", "screen_name": "voltagent_dev" @@ -83,7 +98,10 @@ "symbols": [], "media": [ { - "indices": [177, 200], + "indices": [ + 177, + 200 + ], "url": "https://t.co/rpBP28d4ei", "display_url": "pic.x.com/rpBP28d4ei", "expanded_url": "https://x.com/siimh/status/1929706642851193172/photo/1" @@ -119,12 +137,18 @@ "text": "🤔 What happens when real-time data meets AI?\n\n💪 The team at @voltagent_dev integrated @peakacom into their chatbot, enabling it to access live data and making it smarter as a result.\n\n✍️ Check out the blog post for the details👇\n🔗 https://t.co/RoNXr7Y9ZP", "created_at": "2025-04-29T17:05:47.000Z", "lang": "en", - "display_text_range": [0, 258], + "display_text_range": [ + 0, + 258 + ], "entities": { "hashtags": [], "urls": [ { - "indices": [231, 254], + "indices": [ + 231, + 254 + ], "url": "https://t.co/RoNXr7Y9ZP", "display_url": "voltagent.dev/blog/data-awar…", "expanded_url": "https://voltagent.dev/blog/data-aware-chatbot-voltagent-peaka" @@ -132,13 +156,19 @@ ], "user_mentions": [ { - "indices": [60, 74], + "indices": [ + 60, + 74 + ], "id_str": "1912724805406863360", "name": "voltagent", "screen_name": "voltagent_dev" }, { - "indices": [86, 95], + "indices": [ + 86, + 95 + ], "id_str": "1301806903308189696", "name": "Peaka", "screen_name": "peakacom" @@ -163,12 +193,18 @@ "text": "@tomsiwik if you're building in typescript, you need to see voltagent. \n\ntheir state and memory features are solid. \n\njust built a sales pipeline for a client with it, and it runs smoothly. huge win for complex systems.\n\nhttps://t.co/ZuCR9qgqcj", "created_at": "2025-07-30T12:37:04.000Z", "lang": "en", - "display_text_range": [10, 246], + "display_text_range": [ + 10, + 246 + ], "entities": { "hashtags": [], "urls": [ { - "indices": [223, 246], + "indices": [ + 223, + 246 + ], "url": "https://t.co/ZuCR9qgqcj", "display_url": "voltagent.dev/docs/agents/me…", "expanded_url": "https://voltagent.dev/docs/agents/memory/overview/" @@ -196,13 +232,19 @@ "text": "Gonna try use @voltagent_dev in SupaPM - Nuxt project. I had planned rolling my own agents for now but voltagent looks promising. I’ll let you know how I get on https://t.co/4ltNgHGEhn", "created_at": "2025-05-26T18:42:43.000Z", "lang": "en", - "display_text_range": [0, 161], + "display_text_range": [ + 0, + 161 + ], "entities": { "hashtags": [], "urls": [], "user_mentions": [ { - "indices": [14, 28], + "indices": [ + 14, + 28 + ], "id_str": "1912724805406863360", "name": "voltagent", "screen_name": "voltagent_dev" @@ -211,7 +253,10 @@ "symbols": [], "media": [ { - "indices": [162, 185], + "indices": [ + 162, + 185 + ], "url": "https://t.co/4ltNgHGEhn", "display_url": "pic.x.com/4ltNgHGEhn", "expanded_url": "https://x.com/cderm/status/1927072927213596780/video/1" @@ -255,25 +300,37 @@ "text": "It's great to see more agentic tools looking at @tursodatabase for their database needs. Today we have a guest post from @voltagent_dev CEO @omerfarukaplak.\n\nWhat makes Turso Cloud great for their use case? You can easily create individual databases per project that are then", "created_at": "2025-05-26T17:30:30.000Z", "lang": "en", - "display_text_range": [0, 275], + "display_text_range": [ + 0, + 275 + ], "entities": { "hashtags": [], "urls": [], "user_mentions": [ { - "indices": [48, 62], + "indices": [ + 48, + 62 + ], "id_str": "1432739296008671245", "name": "Turso", "screen_name": "tursodatabase" }, { - "indices": [121, 135], + "indices": [ + 121, + 135 + ], "id_str": "1912724805406863360", "name": "voltagent", "screen_name": "voltagent_dev" }, { - "indices": [140, 155], + "indices": [ + 140, + 155 + ], "id_str": "1151088688148955136", "name": "Omer Aplak", "screen_name": "omerfarukaplak" @@ -298,21 +355,33 @@ "text": "#VoltAgent を試した手順・内容を記事に書いた\n( I wrote an article detailing the steps I took to try out VoltAgent )\n\n●VoltAgent(TypeScript の AIエージェントフレームワーク)を軽く試してみる - #Qiita\nhttps://t.co/Udxy7OyUFi", "created_at": "2025-05-19T15:24:17.000Z", "lang": "ja", - "display_text_range": [0, 181], + "display_text_range": [ + 0, + 181 + ], "entities": { "hashtags": [ { - "indices": [0, 10], + "indices": [ + 0, + 10 + ], "text": "VoltAgent" }, { - "indices": [151, 157], + "indices": [ + 151, + 157 + ], "text": "Qiita" } ], "urls": [ { - "indices": [158, 181], + "indices": [ + 158, + 181 + ], "url": "https://t.co/Udxy7OyUFi", "display_url": "qiita.com/youtoy/items/6…", "expanded_url": "https://qiita.com/youtoy/items/6990e175e92c54265580" @@ -338,12 +407,18 @@ "text": "voltagent、早速解説動画出しました!⚡️\n\n本当は言いたくない解説もしたので、絶対にみないでください!!!(見てください)\n\nhttps://t.co/eyIvYupNtV", "created_at": "2025-05-19T03:16:50.000Z", "lang": "ja", - "display_text_range": [0, 90], + "display_text_range": [ + 0, + 90 + ], "entities": { "hashtags": [], "urls": [ { - "indices": [67, 90], + "indices": [ + 67, + 90 + ], "url": "https://t.co/eyIvYupNtV", "display_url": "youtu.be/5WARn6C9ITM?si…", "expanded_url": "https://youtu.be/5WARn6C9ITM?si=rBDflUMGFNeyrorH" @@ -369,19 +444,28 @@ "text": "@Arindam_1729 @voltagent_dev It’s super easy to use! I imported the repo and used the docs for context with Windsurf and I made 3 agents yesterday.", "created_at": "2025-05-16T12:18:10.000Z", "lang": "en", - "display_text_range": [29, 147], + "display_text_range": [ + 29, + 147 + ], "entities": { "hashtags": [], "urls": [], "user_mentions": [ { - "indices": [0, 13], + "indices": [ + 0, + 13 + ], "id_str": "1533666605279891457", "name": "Arindam Majumder 𝕏", "screen_name": "Arindam_1729" }, { - "indices": [14, 28], + "indices": [ + 14, + 28 + ], "id_str": "1912724805406863360", "name": "voltagent", "screen_name": "voltagent_dev" @@ -408,7 +492,10 @@ "text": "Build powerful AI agents without the boilerplate. Just TypeScript, logic, and control. https://t.co/SHO0Jbui4m", "created_at": "2025-05-08T15:33:57.000Z", "lang": "en", - "display_text_range": [0, 86], + "display_text_range": [ + 0, + 86 + ], "entities": { "hashtags": [], "urls": [], @@ -416,7 +503,10 @@ "symbols": [], "media": [ { - "indices": [87, 110], + "indices": [ + 87, + 110 + ], "url": "https://t.co/SHO0Jbui4m", "display_url": "pic.x.com/SHO0Jbui4m", "expanded_url": "https://x.com/GithubProjects/status/1920502438215250259/photo/1" @@ -452,12 +542,18 @@ "text": "明日はこれを触るか……。\n\nAIエージェント系、どんどん発展してますね。\n\n最終的にはOpenAI、Google、あるいは中国系AIが総取りなんだろうけど。\n\nVoltAgent - Open Source TypeScript AI Agent Framework https://t.co/CiddI3z4yw", "created_at": "2025-05-18T11:04:46.000Z", "lang": "ja", - "display_text_range": [0, 157], + "display_text_range": [ + 0, + 157 + ], "entities": { "hashtags": [], "urls": [ { - "indices": [134, 157], + "indices": [ + 134, + 157 + ], "url": "https://t.co/CiddI3z4yw", "display_url": "voltagent.dev", "expanded_url": "https://voltagent.dev/" @@ -483,12 +579,18 @@ "text": "JavaScript devs working in AI, this is worth a look...\n\nJust came across @voltagent_dev. They’re bridging the gap → code-first flexibility meets visual intuition, built specifically for JavaScript.\n\n👉 https://t.co/LtTAdsk9qW https://t.co/6Y5ymA9SxO", "created_at": "2025-04-28T07:32:45.000Z", "lang": "en", - "display_text_range": [0, 225], + "display_text_range": [ + 0, + 225 + ], "entities": { "hashtags": [], "urls": [ { - "indices": [201, 224], + "indices": [ + 201, + 224 + ], "url": "https://t.co/LtTAdsk9qW", "display_url": "voltagent.dev", "expanded_url": "https://voltagent.dev/" @@ -496,7 +598,10 @@ ], "user_mentions": [ { - "indices": [73, 87], + "indices": [ + 73, + 87 + ], "id_str": "1912724805406863360", "name": "voltagent", "screen_name": "voltagent_dev" @@ -505,7 +610,10 @@ "symbols": [], "media": [ { - "indices": [225, 248], + "indices": [ + 225, + 248 + ], "url": "https://t.co/6Y5ymA9SxO", "display_url": "pic.x.com/6Y5ymA9SxO", "expanded_url": "https://x.com/jsmasterypro/status/1916757463426302247/video/1" @@ -553,12 +661,18 @@ "text": "像拼乐高一样组合 AI 功能\nVoltAgent 一个开源的 TypeScript 框架,专为快速构建AI智能体应用设计。它通过提供现成模块和工具包,让开发者无需从零写代码就能创建由大语言模型驱动的智能系统。\n🧩https://t.co/MaiM9xMKxU https://t.co/yzFhuynSo6", "created_at": "2025-04-24T00:25:55.000Z", "lang": "zh", - "display_text_range": [0, 131], + "display_text_range": [ + 0, + 131 + ], "entities": { "hashtags": [], "urls": [ { - "indices": [107, 130], + "indices": [ + 107, + 130 + ], "url": "https://t.co/MaiM9xMKxU", "display_url": "github.com/VoltAgent/volt…", "expanded_url": "https://github.com/VoltAgent/voltagent" @@ -568,7 +682,10 @@ "symbols": [], "media": [ { - "indices": [131, 154], + "indices": [ + 131, + 154 + ], "url": "https://t.co/yzFhuynSo6", "display_url": "pic.x.com/yzFhuynSo6", "expanded_url": "https://x.com/geekbb/status/1915200495461028321/photo/1" @@ -604,7 +721,10 @@ "text": "朝活でVoltAgentをちゃんと使ってみている\n直感的で良い\n\nこれは流行るかもしれない", "created_at": "2025-05-19T00:33:58.000Z", "lang": "ja", - "display_text_range": [0, 45], + "display_text_range": [ + 0, + 45 + ], "entities": { "hashtags": [], "urls": [], @@ -622,4 +742,4 @@ "is_blue_verified": true } } -} +} \ No newline at end of file