@@ -4,8 +4,9 @@ import { Elysia, t } from "elysia";
44import { createReflectionAgent , createTriageAgent } from "../ai/agents" ;
55import { buildAppContext } from "../ai/config/context" ;
66import { record , setAttributes } from "../lib/tracing" ;
7- import { validateWebsite } from "../lib/website-utils" ;
8- import { QueryBuilders } from "../query/builders" ;
7+ import { getWebsiteDomain , validateWebsite } from "../lib/website-utils" ;
8+ import { executeQuery , QueryBuilders } from "../query/builders" ;
9+ import type { QueryRequest } from "../query/types" ;
910
1011const AgentRequestSchema = t . Object ( {
1112 websiteId : t . String ( ) ,
@@ -30,6 +31,58 @@ const AgentRequestSchema = t.Object({
3031 ) ,
3132} ) ;
3233
34+ /**
35+ * Schema for manual tool invocation requests.
36+ * Allows users to directly invoke analytics queries without going through the AI agent.
37+ */
38+ const InvokeRequestSchema = t . Object ( {
39+ websiteId : t . String ( ) ,
40+ tool : t . String ( {
41+ description : "The tool/query type to invoke" ,
42+ } ) ,
43+ params : t . Object ( {
44+ from : t . String ( { description : "Start date in ISO format (e.g., 2024-01-01)" } ) ,
45+ to : t . String ( { description : "End date in ISO format (e.g., 2024-01-31)" } ) ,
46+ timeUnit : t . Optional (
47+ t . Union ( [
48+ t . Literal ( "minute" ) ,
49+ t . Literal ( "hour" ) ,
50+ t . Literal ( "day" ) ,
51+ t . Literal ( "week" ) ,
52+ t . Literal ( "month" ) ,
53+ ] )
54+ ) ,
55+ filters : t . Optional (
56+ t . Array (
57+ t . Object ( {
58+ field : t . String ( ) ,
59+ op : t . Union ( [
60+ t . Literal ( "eq" ) ,
61+ t . Literal ( "ne" ) ,
62+ t . Literal ( "contains" ) ,
63+ t . Literal ( "not_contains" ) ,
64+ t . Literal ( "starts_with" ) ,
65+ t . Literal ( "in" ) ,
66+ t . Literal ( "not_in" ) ,
67+ ] ) ,
68+ value : t . Union ( [
69+ t . String ( ) ,
70+ t . Number ( ) ,
71+ t . Array ( t . Union ( [ t . String ( ) , t . Number ( ) ] ) ) ,
72+ ] ) ,
73+ target : t . Optional ( t . String ( ) ) ,
74+ having : t . Optional ( t . Boolean ( ) ) ,
75+ } )
76+ )
77+ ) ,
78+ groupBy : t . Optional ( t . Array ( t . String ( ) ) ) ,
79+ orderBy : t . Optional ( t . String ( ) ) ,
80+ limit : t . Optional ( t . Number ( { minimum : 1 , maximum : 1000 } ) ) ,
81+ offset : t . Optional ( t . Number ( { minimum : 0 } ) ) ,
82+ } ) ,
83+ timezone : t . Optional ( t . String ( ) ) ,
84+ } ) ;
85+
3386type UIMessage = {
3487 id : string ;
3588 role : "user" | "assistant" ;
@@ -205,4 +258,122 @@ export const agent = new Elysia({ prefix: "/v1/agent" })
205258 } ) ;
206259 } ,
207260 { body : AgentRequestSchema , idleTimeout : 60_000 }
208- ) ;
261+ )
262+ . post (
263+ "/invoke" ,
264+ async function agentInvoke ( { body, user, request } ) {
265+ return record ( "agentInvoke" , async ( ) => {
266+ setAttributes ( {
267+ "agent.website_id" : body . websiteId ,
268+ "agent.user_id" : user ?. id ?? "unknown" ,
269+ "agent.tool" : body . tool ,
270+ } ) ;
271+
272+ try {
273+ const websiteValidation = await validateWebsite ( body . websiteId ) ;
274+ if ( ! ( websiteValidation . success && websiteValidation . website ) ) {
275+ return {
276+ success : false ,
277+ error : websiteValidation . error ?? "Website not found" ,
278+ } ;
279+ }
280+
281+ const { website } = websiteValidation ;
282+
283+ let authorized = website . isPublic ;
284+ if ( ! authorized ) {
285+ if ( website . organizationId ) {
286+ const { success } = await websitesApi . hasPermission ( {
287+ headers : request . headers ,
288+ body : { permissions : { website : [ "read" ] } } ,
289+ } ) ;
290+ authorized = success ;
291+ } else {
292+ authorized = website . userId === user ?. id ;
293+ }
294+ }
295+
296+ if ( ! authorized ) {
297+ return {
298+ success : false ,
299+ error : "Unauthorized" ,
300+ } ;
301+ }
302+
303+ // Validate the tool exists
304+ const availableTools = Object . keys ( QueryBuilders ) ;
305+ if ( ! availableTools . includes ( body . tool ) ) {
306+ return {
307+ success : false ,
308+ error : `Unknown tool: ${ body . tool } . Available tools: ${ availableTools . join ( ", " ) } ` ,
309+ availableTools,
310+ } ;
311+ }
312+
313+ // Get website domain for the query
314+ const websiteDomain = await getWebsiteDomain ( body . websiteId ) ;
315+
316+ // Build and execute the query
317+ const queryRequest : QueryRequest = {
318+ projectId : body . websiteId ,
319+ type : body . tool ,
320+ from : body . params . from ,
321+ to : body . params . to ,
322+ timeUnit : body . params . timeUnit ,
323+ filters : body . params . filters ,
324+ groupBy : body . params . groupBy ,
325+ orderBy : body . params . orderBy ,
326+ limit : body . params . limit ,
327+ offset : body . params . offset ,
328+ timezone : body . timezone ?? "UTC" ,
329+ } ;
330+
331+ const startTime = Date . now ( ) ;
332+ const data = await executeQuery (
333+ queryRequest ,
334+ websiteDomain ,
335+ queryRequest . timezone
336+ ) ;
337+ const executionTime = Date . now ( ) - startTime ;
338+
339+ return {
340+ success : true ,
341+ tool : body . tool ,
342+ data,
343+ meta : {
344+ rowCount : data . length ,
345+ executionTime,
346+ timezone : queryRequest . timezone ,
347+ from : body . params . from ,
348+ to : body . params . to ,
349+ } ,
350+ } ;
351+ } catch ( error ) {
352+ console . error ( "Agent invoke error:" , error ) ;
353+ return {
354+ success : false ,
355+ error : error instanceof Error ? error . message : "Unknown error" ,
356+ } ;
357+ }
358+ } ) ;
359+ } ,
360+ { body : InvokeRequestSchema }
361+ )
362+ . get ( "/tools" , async function listTools ( { user } ) {
363+ if ( ! user ?. id ) {
364+ return {
365+ success : false ,
366+ error : "Authentication required" ,
367+ } ;
368+ }
369+
370+ const tools = Object . keys ( QueryBuilders ) . map ( ( key ) => ( {
371+ name : key ,
372+ description : `Execute ${ key } analytics query` ,
373+ } ) ) ;
374+
375+ return {
376+ success : true ,
377+ tools,
378+ } ;
379+ } ) ;
0 commit comments