Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 31 additions & 20 deletions apps/tradinggoose/app/api/copilot/chat/review-session-post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ describe('Copilot Chat POST Generic Sessions', () => {
{
id: 'tool-call-1',
name: 'lookup_context',
arguments: JSON.stringify({ query: 'price' }),
success: true,
result: { ok: true },
},
Expand Down Expand Up @@ -410,13 +411,18 @@ describe('Copilot Chat POST Generic Sessions', () => {
reviewSessionId: 'review-session-1',
assistantMessage: expect.objectContaining({
content: '',
toolCalls: [
{
id: 'tool-call-1',
name: 'lookup_context',
success: true,
result: { ok: true },
},
contentBlocks: [
expect.objectContaining({
type: 'tool_call',
toolCall: {
id: 'tool-call-1',
name: 'lookup_context',
arguments: { query: 'price' },
params: { query: 'price' },
success: true,
result: { ok: true },
},
}),
],
}),
})
Expand Down Expand Up @@ -550,13 +556,16 @@ describe('Copilot Chat POST Generic Sessions', () => {
reviewSessionId: 'review-session-1',
assistantMessage: expect.objectContaining({
content: 'Saved response',
toolCalls: [
{
id: 'tool-call-1',
name: 'lookup_context',
success: true,
result: { ok: true },
},
contentBlocks: [
expect.objectContaining({
type: 'tool_call',
toolCall: {
id: 'tool-call-1',
name: 'lookup_context',
success: true,
result: { ok: true },
},
}),
],
}),
})
Expand Down Expand Up @@ -661,7 +670,6 @@ describe('Copilot Chat POST Generic Sessions', () => {
role: 'assistant',
content: 'Saved response',
timestamp: expect.any(String),
toolCalls: undefined,
},
],
'completed'
Expand Down Expand Up @@ -1093,7 +1101,7 @@ describe('Copilot Chat POST Generic Sessions', () => {
type: 'function_call',
call_id: 'tool-call-stringified',
name: 'read_workflow',
arguments: JSON.stringify({ workflowId: 'wf-stringified' }),
arguments: JSON.stringify({ entityId: 'wf-stringified' }),
},
},
{ type: 'response.completed', response: { id: 'response-stringified-tool-args' } },
Expand All @@ -1117,11 +1125,14 @@ describe('Copilot Chat POST Generic Sessions', () => {
expect.objectContaining({
reviewSessionId: 'review-session-stringified-tool-args',
assistantMessage: expect.objectContaining({
toolCalls: [
contentBlocks: [
expect.objectContaining({
id: 'tool-call-stringified',
name: 'read_workflow',
arguments: { workflowId: 'wf-stringified' },
type: 'tool_call',
toolCall: expect.objectContaining({
id: 'tool-call-stringified',
name: 'read_workflow',
arguments: { entityId: 'wf-stringified' },
}),
}),
],
}),
Expand Down
22 changes: 15 additions & 7 deletions apps/tradinggoose/app/api/copilot/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ type ReviewSessionRow = Parameters<typeof mapSessionToApiResponse>[0]
interface PersistMessageAttachments {
fileAttachments?: ReviewMessageInput['fileAttachments']
contexts?: ReviewMessageInput['contexts']
toolCalls?: ReviewMessageInput['toolCalls']
contentBlocks?: ReviewMessageInput['contentBlocks']
}

Expand Down Expand Up @@ -135,7 +134,6 @@ async function persistChatMessages(
): Promise<PersistChatMessagesResult> {
const hasAssistantMessage =
(typeof params.assistantContent === 'string' && params.assistantContent.trim().length > 0) ||
(Array.isArray(params.toolCalls) && params.toolCalls.length > 0) ||
(Array.isArray(params.contentBlocks) && params.contentBlocks.length > 0)

await db.transaction(async (tx) => {
Expand Down Expand Up @@ -165,7 +163,6 @@ async function persistChatMessages(
role: MESSAGE_ROLES.ASSISTANT,
content: params.assistantContent ?? '',
timestamp: params.timestamp,
toolCalls: params.toolCalls,
contentBlocks: params.contentBlocks,
}
: null
Expand Down Expand Up @@ -1205,7 +1202,6 @@ export async function POST(req: NextRequest) {
fileAttachments:
fileAttachments && fileAttachments.length > 0 ? fileAttachments : undefined,
contexts: Array.isArray(contexts) && contexts.length > 0 ? contexts : undefined,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
contentBlocks: contentBlocks.length > 0 ? contentBlocks : undefined,
latestTurnStatus,
})
Expand Down Expand Up @@ -1295,9 +1291,21 @@ export async function POST(req: NextRequest) {
})
}

const toolCalls = Array.isArray(responseData.toolCalls) ? responseData.toolCalls : undefined
const contentBlocks = Array.isArray(responseData.toolCalls)
? responseData.toolCalls.map((toolCall: any) => {
const args = normalizeFunctionCallArguments(toolCall.arguments)
return {
type: 'tool_call',
timestamp: Date.now(),
toolCall: {
...toolCall,
...(args ? { arguments: args, params: args } : {}),
},
}
})
: undefined

if (currentSession && (responseData.content || toolCalls?.length)) {
if (currentSession && (responseData.content || contentBlocks?.length)) {
await persistChatMessages({
reviewSessionId: actualReviewSessionId!,
userMessageId: userMessageIdToUse,
Expand All @@ -1307,7 +1315,7 @@ export async function POST(req: NextRequest) {
fileAttachments:
fileAttachments && fileAttachments.length > 0 ? fileAttachments : undefined,
contexts: Array.isArray(contexts) && contexts.length > 0 ? contexts : undefined,
toolCalls,
contentBlocks,
latestTurnStatus: 'completed',
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ describe('Copilot Chat Update Messages', () => {
messageRole: message.role,
content: message.content,
timestamp: message.timestamp,
...(Array.isArray(message.toolCalls) ? { toolCalls: message.toolCalls } : {}),
...(Array.isArray(message.contentBlocks)
? { contentBlocks: message.contentBlocks }
: {}),
Expand All @@ -169,7 +168,6 @@ describe('Copilot Chat Update Messages', () => {
role: row.messageRole,
content: row.content,
timestamp: row.timestamp,
...(Array.isArray(row.toolCalls) ? { toolCalls: row.toolCalls } : {}),
...(Array.isArray(row.contentBlocks) ? { contentBlocks: row.contentBlocks } : {}),
...(Array.isArray(row.contexts) ? { contexts: row.contexts } : {}),
...(Array.isArray(row.citations) ? { citations: row.citations } : {}),
Expand Down Expand Up @@ -398,13 +396,6 @@ describe('Copilot Chat Update Messages', () => {
messageRole: 'assistant',
content: '',
timestamp: '2026-03-30T12:00:01.000Z',
toolCalls: [
{
id: 'tool-1',
name: 'edit_workflow',
state: 'success',
},
],
contentBlocks: [
{
type: 'tool_call',
Expand Down Expand Up @@ -473,16 +464,6 @@ describe('Copilot Chat Update Messages', () => {
messageRole: 'assistant',
content: '',
timestamp: '2026-03-30T12:00:01.000Z',
toolCalls: [
{
id: 'tool-entity-1',
name: 'edit_skill',
state: 'success',
params: {
entityDocument: '{}',
},
},
],
contentBlocks: [
{
type: 'tool_call',
Expand Down Expand Up @@ -545,13 +526,6 @@ describe('Copilot Chat Update Messages', () => {
messageRole: 'assistant',
content: '',
timestamp: '2026-03-30T12:00:01.000Z',
toolCalls: [
{
id: 'tool-1',
name: 'edit_workflow',
state: 'pending',
},
],
contentBlocks: [
{
type: 'tool_call',
Expand All @@ -574,13 +548,6 @@ describe('Copilot Chat Update Messages', () => {
role: 'assistant',
content: '',
timestamp: '2026-03-30T12:00:01.000Z',
toolCalls: [
{
id: 'tool-1',
name: 'edit_workflow',
state: 'rejected',
},
],
contentBlocks: [
{
type: 'tool_call',
Expand Down Expand Up @@ -610,12 +577,6 @@ describe('Copilot Chat Update Messages', () => {
expect(insertValues.mock.calls[1]?.[0]).toEqual([
expect.objectContaining({
itemId: 'message-1',
toolCalls: [
expect.objectContaining({
id: 'tool-1',
state: 'rejected',
}),
],
contentBlocks: [
expect.objectContaining({
type: 'tool_call',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const UpdateMessagesSchema = z.object({
role: z.enum(['user', 'assistant']),
content: z.string(),
timestamp: z.string(),
toolCalls: z.array(z.any()).optional(),
contentBlocks: z.array(z.any()).optional(),
contexts: z.array(z.any()).optional(),
citations: z.array(z.any()).optional(),
Expand All @@ -65,7 +64,6 @@ function normalizeReviewMessageForPersistence(message: ReviewMessageApi | Incomi
role: message.role,
content: message.content ?? '',
timestamp: message.timestamp ?? '',
toolCalls: Array.isArray(message.toolCalls) ? message.toolCalls : [],
contentBlocks: Array.isArray(message.contentBlocks) ? message.contentBlocks : [],
contexts: Array.isArray(message.contexts) ? message.contexts : [],
citations: Array.isArray(message.citations) ? message.citations : [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/auth'
import { REVIEW_ENTITY_KINDS } from '@/lib/copilot/review-sessions/types'
import { buildCopilotServerToolErrorResponse } from '@/lib/copilot/server-tool-errors'
import { createLogger } from '@/lib/logs/console/logger'
import { checkWorkspaceAccess } from '@/lib/permissions/utils'

const logger = createLogger('ExecuteCopilotServerToolAPI')

Expand All @@ -16,7 +18,9 @@ const ExecuteSchema = z.object({
payload: z.unknown().optional(),
context: z
.object({
contextWorkflowId: z.string().optional(),
contextEntityKind: z.enum(REVIEW_ENTITY_KINDS).optional(),
contextEntityId: z.string().optional(),
workspaceId: z.string().optional(),
})
.optional(),
})
Expand Down Expand Up @@ -61,6 +65,16 @@ export async function POST(req: NextRequest) {
}

logger.info(`[${tracker.requestId}] Executing server tool`, { toolName })
if (context?.workspaceId) {
const workspaceAccess = await checkWorkspaceAccess(context.workspaceId, userId)
if (!workspaceAccess.exists || !workspaceAccess.hasAccess) {
return NextResponse.json(
{ error: 'Access denied to this workspace', code: 'WORKSPACE_ACCESS_DENIED' },
{ status: 403 }
)
}
}

const result = await routeExecution(toolName, payload, {
userId,
...context,
Expand Down
28 changes: 2 additions & 26 deletions apps/tradinggoose/app/api/tools/custom/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,14 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { listCustomTools, upsertCustomTools } from '@/lib/custom-tools/operations'
import { CustomToolUpsertRequestSchema } from '@/lib/custom-tools/schema'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { deleteYjsSessionInSocketServer } from '@/lib/yjs/server/snapshot-bridge'

const logger = createLogger('CustomToolsAPI')

const CustomToolSchema = z.object({
workspaceId: z
.string({ required_error: 'workspaceId is required' })
.min(1, 'workspaceId is required'),
tools: z.array(
z.object({
id: z.string().optional(),
title: z.string().min(1, 'Tool title is required'),
schema: z.object({
type: z.literal('function'),
function: z.object({
name: z.string().min(1, 'Function name is required'),
description: z.string().optional(),
parameters: z.object({
type: z.string(),
properties: z.record(z.any()),
required: z.array(z.string()).optional(),
}),
}),
}),
code: z.string(),
})
),
})

// GET - Fetch all custom tools for a workspace
export async function GET(request: NextRequest) {
const requestId = generateRequestId()
Expand Down Expand Up @@ -109,7 +85,7 @@ export async function POST(req: NextRequest) {

try {
// Validate the request body
const { tools, workspaceId } = CustomToolSchema.parse(body)
const { tools, workspaceId } = CustomToolUpsertRequestSchema.parse(body)

const permission = await getUserEntityPermissions(authResult.userId, 'workspace', workspaceId)
if (!permission) {
Expand Down
10 changes: 5 additions & 5 deletions apps/tradinggoose/blocks/blocks/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
name: 'Function',
description: 'Run custom logic',
longDescription:
'This is a core workflow block. Execute custom TypeScript code within your workflow. Code transpiles to JavaScript at runtime and executes on E2B when enabled, otherwise local VM. Built-in indicators are available through indicator.<ID>(marketSeries) with full Historical Data block output.',
'This is a core workflow block. Execute custom TypeScript code within your workflow. Code transpiles to JavaScript at runtime and executes on E2B when enabled, otherwise local VM. Available indicators are executed through indicator.<ID>(marketSeries) with full Historical Data block output.',
bestPractices: `
- Write TypeScript statements only (no function wrapper).
- If you need external imports, enable E2B at the environment level.
- Do not define Pine indicators directly in this block (no indicator(...), PineTS, or pinets imports).
- To execute built-in indicators, call indicator.<ID>(marketSeries) with the full Historical Data output, not <historical_data.close>. Example: await indicator.RSI(<historical_data>).
- To execute available indicators, call indicator.<ID>(marketSeries) with the full Historical Data output, not <historical_data.close>. Example: await indicator.RSI(<historical_data>).
- Indicator params must be passed as an object. Use saved input titles as keys, for example: await indicator.RSI(<historical_data>, { Length: 7 }) or await indicator.RSI(<historical_data>, { inputs: { Length: 7 } }).
- Use indicator.list() if you need to inspect supported built-in indicator IDs before calling one.
- Reference upstream outputs by copying exact tags like <agent.content>, workflow variables like <variable.riskLimit>, and environment variables with {{ENV_VAR_NAME}}. Avoid XML/HTML tags.
- Use indicator.list() if you need to inspect supported available indicator IDs before calling one.
- Reference upstream outputs by copying exact TradingGoose tags like <agent.content>, workflow variables like <variable.riskLimit>, and environment variables with {{ENV_VAR_NAME}}. These references are valid Function code and resolve before execution; avoid arbitrary XML/HTML tags.
`,
docsLink: 'https://docs.tradinggoose.ai/blocks/function',
category: 'blocks',
Expand Down Expand Up @@ -47,7 +47,7 @@ IMPORTANT FORMATTING RULES:
6. Output: Ensure the code returns a value if the function is expected to produce output. Use 'return'.
7. Clarity: Write clean, readable code.
8. No Explanations: Do NOT include markdown formatting, comments explaining the rules, or any text other than the raw TypeScript code for the function body.
9. Built-in indicators only: Do NOT define indicators directly with indicator(...) or pinets imports. Use indicator.<ID>(marketSeries) with the full Historical Data output, not <historical_data.close>. The optional second argument must be an object, e.g. await indicator.RSI(<historical_data>, { Length: 7 }). Use indicator.list() if the built-in ID is unknown.
9. Available indicators only: Do NOT define indicators directly with indicator(...) or pinets imports. Use indicator.<ID>(marketSeries) with the full Historical Data output, not <historical_data.close>. The optional second argument must be an object, e.g. await indicator.RSI(<historical_data>, { Length: 7 }). Use indicator.list() if the available ID is unknown.

Example Scenario:
User Prompt: "Fetch user data from an API. Use the User ID passed in as 'userId' and an API Key stored as the 'SERVICE_API_KEY' environment variable."
Expand Down
Loading
Loading