mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-07 21:25:38 -05:00
Compare commits
58 Commits
feat/the-c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479cd347ad | ||
|
|
0cb6714496 | ||
|
|
7b36f9257e | ||
|
|
99ae5435e3 | ||
|
|
925f06add7 | ||
|
|
193b95cfec | ||
|
|
0ca25bbab6 | ||
|
|
1edaf197b2 | ||
|
|
474b1af145 | ||
|
|
a3a99eda19 | ||
|
|
1a66d48add | ||
|
|
46822e91f3 | ||
|
|
2bb68335ee | ||
|
|
8528fbe2d2 | ||
|
|
31fdd2be13 | ||
|
|
028bc652c2 | ||
|
|
c6bf5cd58c | ||
|
|
11dc18a80d | ||
|
|
ab4e9dc72f | ||
|
|
1c58c35bd8 | ||
|
|
d63a5cb504 | ||
|
|
8bd5d41723 | ||
|
|
c12931bc50 | ||
|
|
e9c4251c1c | ||
|
|
cc2be33d6b | ||
|
|
45371e521e | ||
|
|
0ce0f98aa5 | ||
|
|
dff1c9d083 | ||
|
|
b09f683072 | ||
|
|
a8bb0db660 | ||
|
|
af82820a28 | ||
|
|
4372841797 | ||
|
|
5e8c843241 | ||
|
|
7bf3d73ee6 | ||
|
|
7ffc11a738 | ||
|
|
be578e2ed7 | ||
|
|
f415e5edc4 | ||
|
|
13a6e6c3fa | ||
|
|
f5ab7f21ae | ||
|
|
bfb6fffe38 | ||
|
|
4fbec0a43f | ||
|
|
585f5e365b | ||
|
|
3792bdd252 | ||
|
|
eb5d1f3e5b | ||
|
|
54ab82c8dd | ||
|
|
f895bf469b | ||
|
|
dd3209af06 | ||
|
|
b6ba3b50a7 | ||
|
|
b304233062 | ||
|
|
57e4b49bd6 | ||
|
|
e12dd204ed | ||
|
|
3d9d9cbc54 | ||
|
|
0f4ec962ad | ||
|
|
4827866f9a | ||
|
|
3e697d9ed9 | ||
|
|
4431a1a484 | ||
|
|
4d1a9a3f22 | ||
|
|
eb07a080fb |
@@ -5462,3 +5462,24 @@ export function EnrichSoIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AgentSkillsIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width='16'
|
||||||
|
height='16'
|
||||||
|
viewBox='0 0 16 16'
|
||||||
|
fill='none'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d='M8 1L14.0622 4.5V11.5L8 15L1.93782 11.5V4.5L8 1Z'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth='1.5'
|
||||||
|
fill='none'
|
||||||
|
/>
|
||||||
|
<path d='M8 4.5L11 6.25V9.75L8 11.5L5 9.75V6.25L8 4.5Z' fill='currentColor' />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Switch between modes using the mode selector at the bottom of the input area.
|
|||||||
Select your preferred AI model using the model selector at the bottom right of the input area.
|
Select your preferred AI model using the model selector at the bottom right of the input area.
|
||||||
|
|
||||||
**Available Models:**
|
**Available Models:**
|
||||||
- Claude 4.6 Opus (default), 4.5 Opus, Sonnet, Haiku
|
- Claude 4.5 Opus, Sonnet (default), Haiku
|
||||||
- GPT 5.2 Codex, Pro
|
- GPT 5.2 Codex, Pro
|
||||||
- Gemini 3 Pro
|
- Gemini 3 Pro
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ This means you can attach many skills to an agent without bloating its context w
|
|||||||
|
|
||||||
## Creating Skills
|
## Creating Skills
|
||||||
|
|
||||||
Go to **Settings** (gear icon) and select **Skills** under the Tools section.
|
Go to **Settings** and select **Skills** under the Tools section.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Click **Add** to create a new skill with three fields:
|
Click **Add** to create a new skill with three fields:
|
||||||
|
|
||||||
@@ -52,11 +54,22 @@ Use when the user asks you to write, optimize, or debug SQL queries.
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Recommended structure:**
|
||||||
|
- **When to use** — Specific triggers and scenarios
|
||||||
|
- **Instructions** — Step-by-step guidance with numbered lists
|
||||||
|
- **Examples** — Input/output samples showing expected behavior
|
||||||
|
- **Common Patterns** — Reusable approaches for frequent tasks
|
||||||
|
- **Edge Cases** — Gotchas and special considerations
|
||||||
|
|
||||||
|
Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills.
|
||||||
|
|
||||||
## Adding Skills to an Agent
|
## Adding Skills to an Agent
|
||||||
|
|
||||||
Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to.
|
Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to.
|
||||||
|
|
||||||
Selected skills appear as chips that you can click to edit or remove.
|

|
||||||
|
|
||||||
|
Selected skills appear as cards that you can click to edit or remove.
|
||||||
|
|
||||||
### What Happens at Runtime
|
### What Happens at Runtime
|
||||||
|
|
||||||
@@ -69,12 +82,50 @@ When the workflow runs:
|
|||||||
|
|
||||||
This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed.
|
This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed.
|
||||||
|
|
||||||
## Tips
|
## Common Use Cases
|
||||||
|
|
||||||
- **Keep descriptions actionable** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis"
|
Skills are most valuable when agents need specialized knowledge or multi-step workflows:
|
||||||
|
|
||||||
|
**Domain Expertise**
|
||||||
|
- `api-integration-expert` — Best practices for calling specific APIs (authentication, rate limiting, error handling)
|
||||||
|
- `data-transformation` — ETL patterns, data cleaning, and validation rules
|
||||||
|
- `code-reviewer` — Code review guidelines specific to your team's standards
|
||||||
|
|
||||||
|
**Workflow Templates**
|
||||||
|
- `bug-investigation` — Step-by-step debugging methodology (reproduce → isolate → test → fix)
|
||||||
|
- `feature-implementation` — Development workflow from requirements to deployment
|
||||||
|
- `document-generator` — Templates and formatting rules for technical documentation
|
||||||
|
|
||||||
|
**Company-Specific Knowledge**
|
||||||
|
- `our-architecture` — System architecture diagrams, service dependencies, and deployment processes
|
||||||
|
- `style-guide` — Brand guidelines, writing tone, UI/UX patterns
|
||||||
|
- `customer-onboarding` — Standard procedures and common customer questions
|
||||||
|
|
||||||
|
**When to use skills vs. agent instructions:**
|
||||||
|
- Use **skills** for knowledge that applies across multiple workflows or changes frequently
|
||||||
|
- Use **agent instructions** for task-specific context that's unique to a single agent
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
**Writing Effective Descriptions**
|
||||||
|
- **Be specific and keyword-rich** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis"
|
||||||
|
- **Include activation triggers** — Mention specific words or phrases that should prompt the skill (e.g., "Use when the user mentions PDFs, forms, or document extraction")
|
||||||
|
- **Keep it under 200 words** — Agents scan descriptions quickly; make every word count
|
||||||
|
|
||||||
|
**Skill Scope and Organization**
|
||||||
- **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill
|
- **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill
|
||||||
- **Use markdown structure** — Headers, lists, and code blocks help the agent parse and follow instructions
|
- **Limit to 5-10 skills per agent** — More skills = more decision overhead; start small and add as needed
|
||||||
- **Test iteratively** — Run your workflow and check if the agent activates the skill when expected
|
- **Split large skills** — If a skill exceeds 500 lines, break it into focused sub-skills
|
||||||
|
|
||||||
|
**Content Structure**
|
||||||
|
- **Use markdown formatting** — Headers, lists, and code blocks help agents parse and follow instructions
|
||||||
|
- **Provide examples** — Show input/output pairs so agents understand expected behavior
|
||||||
|
- **Be explicit about edge cases** — Don't assume agents will infer special handling
|
||||||
|
|
||||||
|
**Testing and Iteration**
|
||||||
|
- **Test activation** — Run your workflow and verify the agent loads the skill when expected
|
||||||
|
- **Check for false positives** — Make sure skills aren't activating when they shouldn't
|
||||||
|
- **Refine descriptions** — If a skill isn't loading when needed, add more keywords to the description
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
color="#6366F1"
|
color="#6366F1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* MANUAL-CONTENT-START:intro */}
|
||||||
|
[Airweave](https://airweave.ai/) is an AI-powered semantic search platform that helps you discover and retrieve knowledge across all your synced data sources. Built for modern teams, Airweave enables fast, relevant search results using neural, hybrid, or keyword-based strategies tailored to your needs.
|
||||||
|
|
||||||
|
With Airweave, you can:
|
||||||
|
|
||||||
|
- **Search smarter**: Use natural language queries to uncover information stored across your connected tools and databases
|
||||||
|
- **Unify your data**: Seamlessly access content from sources like code, docs, chat, emails, cloud files, and more
|
||||||
|
- **Customize retrieval**: Select between hybrid (semantic + keyword), neural, or keyword search strategies for optimal results
|
||||||
|
- **Boost recall**: Expand search queries with AI to find more comprehensive answers
|
||||||
|
- **Rerank results using AI**: Prioritize the most relevant answers with powerful language models
|
||||||
|
- **Get instant answers**: Generate clear, AI-powered responses synthesized from your data
|
||||||
|
|
||||||
|
In Sim, the Airweave integration empowers your agents to search, summarize, and extract insights from all your organization’s data via a single tool. Use Airweave to drive rich, contextual knowledge retrieval within your workflows—whether answering questions, generating summaries, or supporting dynamic decision-making.
|
||||||
|
{/* MANUAL-CONTENT-END */}
|
||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Search across your synced data sources using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.
|
Search across your synced data sources using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.
|
||||||
|
|||||||
BIN
apps/docs/public/static/skills/add-skill.png
Normal file
BIN
apps/docs/public/static/skills/add-skill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/docs/public/static/skills/manage-skills.png
Normal file
BIN
apps/docs/public/static/skills/manage-skills.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpAuthorizationServerMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpAuthorizationServerMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpAuthorizationServerMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpProtectedResourceMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpProtectedResourceMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
||||||
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { getRedisClient } from '@/lib/core/config/redis'
|
import { getRedisClient } from '@/lib/core/config/redis'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!agent.agent.isPublished) {
|
if (!agent.agent.isPublished) {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
|
return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
|
|||||||
const { agentId } = await params
|
const { agentId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
|||||||
const { agentId } = await params
|
const { agentId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
|||||||
const { agentId } = await params
|
const { agentId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
logger.warn('A2A agent publish auth failed:', { error: auth.error, hasUserId: !!auth.userId })
|
logger.warn('A2A agent publish auth failed:', { error: auth.error, hasUserId: !!auth.userId })
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
||||||
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
||||||
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||||
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -27,7 +27,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ export async function GET(request: NextRequest) {
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { jwtDecode } from 'jwt-decode'
|
import { jwtDecode } from 'jwt-decode'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { evaluateScopeCoverage, type OAuthProvider, parseProvider } from '@/lib/oauth'
|
import { evaluateScopeCoverage, type OAuthProvider, parseProvider } from '@/lib/oauth'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -81,7 +81,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const { provider: providerParam, workflowId, credentialId } = parseResult.data
|
const { provider: providerParam, workflowId, credentialId } = parseResult.data
|
||||||
|
|
||||||
// Authenticate requester (supports session, API key, internal JWT)
|
// Authenticate requester (supports session, API key, internal JWT)
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkSessionOrInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthenticated credentials request rejected`)
|
logger.warn(`[${requestId}] Unauthenticated credentials request rejected`)
|
||||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
const mockRefreshTokenIfNeeded = vi.fn()
|
const mockRefreshTokenIfNeeded = vi.fn()
|
||||||
const mockGetOAuthToken = vi.fn()
|
const mockGetOAuthToken = vi.fn()
|
||||||
const mockAuthorizeCredentialUse = vi.fn()
|
const mockAuthorizeCredentialUse = vi.fn()
|
||||||
const mockCheckHybridAuth = vi.fn()
|
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||||
|
|
||||||
const mockLogger = createMockLogger()
|
const mockLogger = createMockLogger()
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: mockCheckHybridAuth,
|
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
|
|
||||||
describe('credentialAccountUserId + providerId path', () => {
|
describe('credentialAccountUserId + providerId path', () => {
|
||||||
it('should reject unauthenticated requests', async () => {
|
it('should reject unauthenticated requests', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Authentication required',
|
error: 'Authentication required',
|
||||||
})
|
})
|
||||||
@@ -255,30 +255,8 @@ describe('OAuth Token API Routes', () => {
|
|||||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject API key authentication', async () => {
|
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
|
||||||
success: true,
|
|
||||||
authType: 'api_key',
|
|
||||||
userId: 'test-user-id',
|
|
||||||
})
|
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
|
||||||
credentialAccountUserId: 'test-user-id',
|
|
||||||
providerId: 'google',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
|
||||||
|
|
||||||
const response = await POST(req)
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
expect(response.status).toBe(401)
|
|
||||||
expect(data).toHaveProperty('error', 'User not authenticated')
|
|
||||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should reject internal JWT authentication', async () => {
|
it('should reject internal JWT authentication', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'internal_jwt',
|
authType: 'internal_jwt',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -300,7 +278,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should reject requests for other users credentials', async () => {
|
it('should reject requests for other users credentials', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'attacker-user-id',
|
userId: 'attacker-user-id',
|
||||||
@@ -322,7 +300,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should allow session-authenticated users to access their own credentials', async () => {
|
it('should allow session-authenticated users to access their own credentials', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -345,7 +323,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return 404 when credential not found for user', async () => {
|
it('should return 404 when credential not found for user', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -373,7 +351,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
*/
|
*/
|
||||||
describe('GET handler', () => {
|
describe('GET handler', () => {
|
||||||
it('should return access token successfully', async () => {
|
it('should return access token successfully', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -402,7 +380,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
expect(response.status).toBe(200)
|
expect(response.status).toBe(200)
|
||||||
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
||||||
|
|
||||||
expect(mockCheckHybridAuth).toHaveBeenCalled()
|
expect(mockCheckSessionOrInternalAuth).toHaveBeenCalled()
|
||||||
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
|
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
|
||||||
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -421,7 +399,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle authentication failure', async () => {
|
it('should handle authentication failure', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Authentication required',
|
error: 'Authentication required',
|
||||||
})
|
})
|
||||||
@@ -440,7 +418,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle credential not found', async () => {
|
it('should handle credential not found', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -461,7 +439,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle missing access token', async () => {
|
it('should handle missing access token', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -487,7 +465,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle token refresh failure', async () => {
|
it('should handle token refresh failure', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export async function POST(request: NextRequest) {
|
|||||||
providerId,
|
providerId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
|
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
|
||||||
success: auth.success,
|
success: auth.success,
|
||||||
@@ -187,7 +187,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const { credentialId } = parseResult.data
|
const { credentialId } = parseResult.data
|
||||||
|
|
||||||
// For GET requests, we only support session-based authentication
|
// For GET requests, we only support session-based authentication
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const UpdateCostSchema = z.object({
|
|||||||
model: z.string().min(1, 'Model is required'),
|
model: z.string().min(1, 'Model is required'),
|
||||||
inputTokens: z.number().min(0).default(0),
|
inputTokens: z.number().min(0).default(0),
|
||||||
outputTokens: z.number().min(0).default(0),
|
outputTokens: z.number().min(0).default(0),
|
||||||
source: z.enum(['copilot', 'mcp_copilot']).default('copilot'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,14 +75,12 @@ export async function POST(req: NextRequest) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId, cost, model, inputTokens, outputTokens, source } = validation.data
|
const { userId, cost, model, inputTokens, outputTokens } = validation.data
|
||||||
const isMcp = source === 'mcp_copilot'
|
|
||||||
|
|
||||||
logger.info(`[${requestId}] Processing cost update`, {
|
logger.info(`[${requestId}] Processing cost update`, {
|
||||||
userId,
|
userId,
|
||||||
cost,
|
cost,
|
||||||
model,
|
model,
|
||||||
source,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if user stats record exists (same as ExecutionLogger)
|
// Check if user stats record exists (same as ExecutionLogger)
|
||||||
@@ -99,7 +96,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
|
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFields: Record<string, unknown> = {
|
const updateFields = {
|
||||||
totalCost: sql`total_cost + ${cost}`,
|
totalCost: sql`total_cost + ${cost}`,
|
||||||
currentPeriodCost: sql`current_period_cost + ${cost}`,
|
currentPeriodCost: sql`current_period_cost + ${cost}`,
|
||||||
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
|
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
|
||||||
@@ -108,24 +105,17 @@ export async function POST(req: NextRequest) {
|
|||||||
lastActive: new Date(),
|
lastActive: new Date(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also increment MCP-specific counters when source is mcp_copilot
|
|
||||||
if (isMcp) {
|
|
||||||
updateFields.totalMcpCopilotCost = sql`total_mcp_copilot_cost + ${cost}`
|
|
||||||
updateFields.currentPeriodMcpCopilotCost = sql`current_period_mcp_copilot_cost + ${cost}`
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))
|
await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))
|
||||||
|
|
||||||
logger.info(`[${requestId}] Updated user stats record`, {
|
logger.info(`[${requestId}] Updated user stats record`, {
|
||||||
userId,
|
userId,
|
||||||
addedCost: cost,
|
addedCost: cost,
|
||||||
source,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Log usage for complete audit trail
|
// Log usage for complete audit trail
|
||||||
await logModelUsage({
|
await logModelUsage({
|
||||||
userId,
|
userId,
|
||||||
source: isMcp ? 'mcp_copilot' : 'copilot',
|
source: 'copilot',
|
||||||
model,
|
model,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
|
|
||||||
const GenerateApiKeySchema = z.object({
|
const GenerateApiKeySchema = z.object({
|
||||||
@@ -17,6 +17,9 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const userId = session.user.id
|
const userId = session.user.id
|
||||||
|
|
||||||
|
// Move environment variable access inside the function
|
||||||
|
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||||
|
|
||||||
const body = await req.json().catch(() => ({}))
|
const body = await req.json().catch(() => ({}))
|
||||||
const validationResult = GenerateApiKeySchema.safeParse(body)
|
const validationResult = GenerateApiKeySchema.safeParse(body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@@ -12,6 +12,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const userId = session.user.id
|
const userId = session.user.id
|
||||||
|
|
||||||
|
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||||
|
|
||||||
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/get-api-keys`, {
|
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/get-api-keys`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -66,6 +68,8 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'id is required' }, { status: 400 })
|
return NextResponse.json({ error: 'id is required' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||||
|
|
||||||
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/delete`, {
|
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/delete`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,130 +0,0 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
getStreamMeta,
|
|
||||||
readStreamEvents,
|
|
||||||
type StreamMeta,
|
|
||||||
} from '@/lib/copilot/orchestrator/stream-buffer'
|
|
||||||
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
|
|
||||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
|
||||||
|
|
||||||
const logger = createLogger('CopilotChatStreamAPI')
|
|
||||||
const POLL_INTERVAL_MS = 250
|
|
||||||
const MAX_STREAM_MS = 10 * 60 * 1000
|
|
||||||
|
|
||||||
function encodeEvent(event: Record<string, any>): Uint8Array {
|
|
||||||
return new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { userId: authenticatedUserId, isAuthenticated } =
|
|
||||||
await authenticateCopilotRequestSessionOnly()
|
|
||||||
|
|
||||||
if (!isAuthenticated || !authenticatedUserId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(request.url)
|
|
||||||
const streamId = url.searchParams.get('streamId') || ''
|
|
||||||
const fromParam = url.searchParams.get('from') || '0'
|
|
||||||
const fromEventId = Number(fromParam || 0)
|
|
||||||
// If batch=true, return buffered events as JSON instead of SSE
|
|
||||||
const batchMode = url.searchParams.get('batch') === 'true'
|
|
||||||
const toParam = url.searchParams.get('to')
|
|
||||||
const toEventId = toParam ? Number(toParam) : undefined
|
|
||||||
|
|
||||||
if (!streamId) {
|
|
||||||
return NextResponse.json({ error: 'streamId is required' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = (await getStreamMeta(streamId)) as StreamMeta | null
|
|
||||||
logger.info('[Resume] Stream lookup', {
|
|
||||||
streamId,
|
|
||||||
fromEventId,
|
|
||||||
toEventId,
|
|
||||||
batchMode,
|
|
||||||
hasMeta: !!meta,
|
|
||||||
metaStatus: meta?.status,
|
|
||||||
})
|
|
||||||
if (!meta) {
|
|
||||||
return NextResponse.json({ error: 'Stream not found' }, { status: 404 })
|
|
||||||
}
|
|
||||||
if (meta.userId && meta.userId !== authenticatedUserId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch mode: return all buffered events as JSON
|
|
||||||
if (batchMode) {
|
|
||||||
const events = await readStreamEvents(streamId, fromEventId)
|
|
||||||
const filteredEvents = toEventId ? events.filter((e) => e.eventId <= toEventId) : events
|
|
||||||
logger.info('[Resume] Batch response', {
|
|
||||||
streamId,
|
|
||||||
fromEventId,
|
|
||||||
toEventId,
|
|
||||||
eventCount: filteredEvents.length,
|
|
||||||
})
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
events: filteredEvents,
|
|
||||||
status: meta.status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
let lastEventId = Number.isFinite(fromEventId) ? fromEventId : 0
|
|
||||||
|
|
||||||
const flushEvents = async () => {
|
|
||||||
const events = await readStreamEvents(streamId, lastEventId)
|
|
||||||
if (events.length > 0) {
|
|
||||||
logger.info('[Resume] Flushing events', {
|
|
||||||
streamId,
|
|
||||||
fromEventId: lastEventId,
|
|
||||||
eventCount: events.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (const entry of events) {
|
|
||||||
lastEventId = entry.eventId
|
|
||||||
const payload = {
|
|
||||||
...entry.event,
|
|
||||||
eventId: entry.eventId,
|
|
||||||
streamId: entry.streamId,
|
|
||||||
}
|
|
||||||
controller.enqueue(encodeEvent(payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await flushEvents()
|
|
||||||
|
|
||||||
while (Date.now() - startTime < MAX_STREAM_MS) {
|
|
||||||
const currentMeta = await getStreamMeta(streamId)
|
|
||||||
if (!currentMeta) break
|
|
||||||
|
|
||||||
await flushEvents()
|
|
||||||
|
|
||||||
if (currentMeta.status === 'complete' || currentMeta.status === 'error') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.signal.aborted) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Stream replay failed', {
|
|
||||||
streamId,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
controller.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Response(stream, { headers: SSE_HEADERS })
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { REDIS_TOOL_CALL_PREFIX, REDIS_TOOL_CALL_TTL_SECONDS } from '@/lib/copilot/constants'
|
|
||||||
import {
|
import {
|
||||||
authenticateCopilotRequestSessionOnly,
|
authenticateCopilotRequestSessionOnly,
|
||||||
createBadRequestResponse,
|
createBadRequestResponse,
|
||||||
@@ -24,8 +23,7 @@ const ConfirmationSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the user's tool decision to Redis. The server-side orchestrator's
|
* Update tool call status in Redis
|
||||||
* waitForToolDecision() polls Redis for this value.
|
|
||||||
*/
|
*/
|
||||||
async function updateToolCallStatus(
|
async function updateToolCallStatus(
|
||||||
toolCallId: string,
|
toolCallId: string,
|
||||||
@@ -34,24 +32,57 @@ async function updateToolCallStatus(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const redis = getRedisClient()
|
const redis = getRedisClient()
|
||||||
if (!redis) {
|
if (!redis) {
|
||||||
logger.warn('Redis client not available for tool confirmation')
|
logger.warn('updateToolCallStatus: Redis client not available')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const key = `${REDIS_TOOL_CALL_PREFIX}${toolCallId}`
|
const key = `tool_call:${toolCallId}`
|
||||||
const payload = {
|
const timeout = 600000 // 10 minutes timeout for user confirmation
|
||||||
|
const pollInterval = 100 // Poll every 100ms
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
logger.info('Polling for tool call in Redis', { toolCallId, key, timeout })
|
||||||
|
|
||||||
|
// Poll until the key exists or timeout
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
const exists = await redis.exists(key)
|
||||||
|
if (exists) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before next poll
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final check if key exists after polling
|
||||||
|
const exists = await redis.exists(key)
|
||||||
|
if (!exists) {
|
||||||
|
logger.warn('Tool call not found in Redis after polling timeout', {
|
||||||
|
toolCallId,
|
||||||
|
key,
|
||||||
|
timeout,
|
||||||
|
pollDuration: Date.now() - startTime,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store both status and message as JSON
|
||||||
|
const toolCallData = {
|
||||||
status,
|
status,
|
||||||
message: message || null,
|
message: message || null,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
await redis.set(key, JSON.stringify(payload), 'EX', REDIS_TOOL_CALL_TTL_SECONDS)
|
|
||||||
|
await redis.set(key, JSON.stringify(toolCallData), 'EX', 86400) // Keep 24 hour expiry
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update tool call status', {
|
logger.error('Failed to update tool call status in Redis', {
|
||||||
toolCallId,
|
toolCallId,
|
||||||
status,
|
status,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
message,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
|
|
||||||
import { routeExecution } from '@/lib/copilot/tools/server/router'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/copilot/credentials
|
|
||||||
* Returns connected OAuth credentials for the authenticated user.
|
|
||||||
* Used by the copilot store for credential masking.
|
|
||||||
*/
|
|
||||||
export async function GET(_req: NextRequest) {
|
|
||||||
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
|
|
||||||
if (!isAuthenticated || !userId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await routeExecution('get_credentials', {}, { userId })
|
|
||||||
return NextResponse.json({ success: true, result })
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to load credentials',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import {
|
||||||
|
authenticateCopilotRequestSessionOnly,
|
||||||
|
createBadRequestResponse,
|
||||||
|
createInternalServerErrorResponse,
|
||||||
|
createRequestTracker,
|
||||||
|
createUnauthorizedResponse,
|
||||||
|
} from '@/lib/copilot/request-helpers'
|
||||||
|
import { routeExecution } from '@/lib/copilot/tools/server/router'
|
||||||
|
|
||||||
|
const logger = createLogger('ExecuteCopilotServerToolAPI')
|
||||||
|
|
||||||
|
const ExecuteSchema = z.object({
|
||||||
|
toolName: z.string(),
|
||||||
|
payload: z.unknown().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const tracker = createRequestTracker()
|
||||||
|
try {
|
||||||
|
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
|
||||||
|
if (!isAuthenticated || !userId) {
|
||||||
|
return createUnauthorizedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json()
|
||||||
|
try {
|
||||||
|
const preview = JSON.stringify(body).slice(0, 300)
|
||||||
|
logger.debug(`[${tracker.requestId}] Incoming request body preview`, { preview })
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const { toolName, payload } = ExecuteSchema.parse(body)
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Executing server tool`, { toolName })
|
||||||
|
const result = await routeExecution(toolName, payload, { userId })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resultPreview = JSON.stringify(result).slice(0, 300)
|
||||||
|
logger.debug(`[${tracker.requestId}] Server tool result preview`, { toolName, resultPreview })
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, result })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
logger.debug(`[${tracker.requestId}] Zod validation error`, { issues: error.issues })
|
||||||
|
return createBadRequestResponse('Invalid request body for execute-copilot-server-tool')
|
||||||
|
}
|
||||||
|
logger.error(`[${tracker.requestId}] Failed to execute server tool:`, error)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to execute server tool'
|
||||||
|
return createInternalServerErrorResponse(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
247
apps/sim/app/api/copilot/execute-tool/route.ts
Normal file
247
apps/sim/app/api/copilot/execute-tool/route.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { db } from '@sim/db'
|
||||||
|
import { account, workflow } from '@sim/db/schema'
|
||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
import {
|
||||||
|
createBadRequestResponse,
|
||||||
|
createInternalServerErrorResponse,
|
||||||
|
createRequestTracker,
|
||||||
|
createUnauthorizedResponse,
|
||||||
|
} from '@/lib/copilot/request-helpers'
|
||||||
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||||
|
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
|
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
||||||
|
import { executeTool } from '@/tools'
|
||||||
|
import { getTool, resolveToolId } from '@/tools/utils'
|
||||||
|
|
||||||
|
const logger = createLogger('CopilotExecuteToolAPI')
|
||||||
|
|
||||||
|
const ExecuteToolSchema = z.object({
|
||||||
|
toolCallId: z.string(),
|
||||||
|
toolName: z.string(),
|
||||||
|
arguments: z.record(z.any()).optional().default({}),
|
||||||
|
workflowId: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const tracker = createRequestTracker()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await getSession()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return createUnauthorizedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
const body = await req.json()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const preview = JSON.stringify(body).slice(0, 300)
|
||||||
|
logger.debug(`[${tracker.requestId}] Incoming execute-tool request`, { preview })
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const { toolCallId, toolName, arguments: toolArgs, workflowId } = ExecuteToolSchema.parse(body)
|
||||||
|
|
||||||
|
const resolvedToolName = resolveToolId(toolName)
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Executing tool`, {
|
||||||
|
toolCallId,
|
||||||
|
toolName,
|
||||||
|
resolvedToolName,
|
||||||
|
workflowId,
|
||||||
|
hasArgs: Object.keys(toolArgs).length > 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolConfig = getTool(resolvedToolName)
|
||||||
|
if (!toolConfig) {
|
||||||
|
// Find similar tool names to help debug
|
||||||
|
const { tools: allTools } = await import('@/tools/registry')
|
||||||
|
const allToolNames = Object.keys(allTools)
|
||||||
|
const prefix = toolName.split('_').slice(0, 2).join('_')
|
||||||
|
const similarTools = allToolNames
|
||||||
|
.filter((name) => name.startsWith(`${prefix.split('_')[0]}_`))
|
||||||
|
.slice(0, 10)
|
||||||
|
|
||||||
|
logger.warn(`[${tracker.requestId}] Tool not found in registry`, {
|
||||||
|
toolName,
|
||||||
|
prefix,
|
||||||
|
similarTools,
|
||||||
|
totalToolsInRegistry: allToolNames.length,
|
||||||
|
})
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Tool not found: ${toolName}. Similar tools: ${similarTools.join(', ')}`,
|
||||||
|
toolCallId,
|
||||||
|
},
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the workspaceId from the workflow (env vars are stored at workspace level)
|
||||||
|
let workspaceId: string | undefined
|
||||||
|
if (workflowId) {
|
||||||
|
const workflowResult = await db
|
||||||
|
.select({ workspaceId: workflow.workspaceId })
|
||||||
|
.from(workflow)
|
||||||
|
.where(eq(workflow.id, workflowId))
|
||||||
|
.limit(1)
|
||||||
|
workspaceId = workflowResult[0]?.workspaceId ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get decrypted environment variables early so we can resolve all {{VAR}} references
|
||||||
|
const decryptedEnvVars = await getEffectiveDecryptedEnv(userId, workspaceId)
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Fetched environment variables`, {
|
||||||
|
workflowId,
|
||||||
|
workspaceId,
|
||||||
|
envVarCount: Object.keys(decryptedEnvVars).length,
|
||||||
|
envVarKeys: Object.keys(decryptedEnvVars),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build execution params starting with LLM-provided arguments
|
||||||
|
// Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects)
|
||||||
|
const executionParams: Record<string, any> = resolveEnvVarReferences(
|
||||||
|
toolArgs,
|
||||||
|
decryptedEnvVars,
|
||||||
|
{ deep: true }
|
||||||
|
) as Record<string, any>
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {
|
||||||
|
toolName,
|
||||||
|
originalArgKeys: Object.keys(toolArgs),
|
||||||
|
resolvedArgKeys: Object.keys(executionParams),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resolve OAuth access token if required
|
||||||
|
if (toolConfig.oauth?.required && toolConfig.oauth.provider) {
|
||||||
|
const provider = toolConfig.oauth.provider
|
||||||
|
logger.info(`[${tracker.requestId}] Resolving OAuth token`, { provider })
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the account for this provider and user
|
||||||
|
const accounts = await db
|
||||||
|
.select()
|
||||||
|
.from(account)
|
||||||
|
.where(and(eq(account.providerId, provider), eq(account.userId, userId)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
const acc = accounts[0]
|
||||||
|
const requestId = generateRequestId()
|
||||||
|
const { accessToken } = await refreshTokenIfNeeded(requestId, acc as any, acc.id)
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
executionParams.accessToken = accessToken
|
||||||
|
logger.info(`[${tracker.requestId}] OAuth token resolved`, { provider })
|
||||||
|
} else {
|
||||||
|
logger.warn(`[${tracker.requestId}] No access token available`, { provider })
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `OAuth token not available for ${provider}. Please reconnect your account.`,
|
||||||
|
toolCallId,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[${tracker.requestId}] No account found for provider`, { provider })
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `No ${provider} account connected. Please connect your account first.`,
|
||||||
|
toolCallId,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${tracker.requestId}] Failed to resolve OAuth token`, {
|
||||||
|
provider,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get OAuth token for ${provider}`,
|
||||||
|
toolCallId,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tool requires an API key that wasn't resolved via {{ENV_VAR}} reference
|
||||||
|
const needsApiKey = toolConfig.params?.apiKey?.required
|
||||||
|
|
||||||
|
if (needsApiKey && !executionParams.apiKey) {
|
||||||
|
logger.warn(`[${tracker.requestId}] No API key found for tool`, { toolName })
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `API key not provided for ${toolName}. Use {{YOUR_API_KEY_ENV_VAR}} to reference your environment variable.`,
|
||||||
|
toolCallId,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add execution context
|
||||||
|
executionParams._context = {
|
||||||
|
workflowId,
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for function_execute - inject environment variables
|
||||||
|
if (toolName === 'function_execute') {
|
||||||
|
executionParams.envVars = decryptedEnvVars
|
||||||
|
executionParams.workflowVariables = {} // No workflow variables in copilot context
|
||||||
|
executionParams.blockData = {} // No block data in copilot context
|
||||||
|
executionParams.blockNameMapping = {} // No block mapping in copilot context
|
||||||
|
executionParams.language = executionParams.language || 'javascript'
|
||||||
|
executionParams.timeout = executionParams.timeout || 30000
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Injected env vars for function_execute`, {
|
||||||
|
envVarCount: Object.keys(decryptedEnvVars).length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the tool
|
||||||
|
logger.info(`[${tracker.requestId}] Executing tool with resolved credentials`, {
|
||||||
|
toolName,
|
||||||
|
hasAccessToken: !!executionParams.accessToken,
|
||||||
|
hasApiKey: !!executionParams.apiKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await executeTool(resolvedToolName, executionParams)
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Tool execution complete`, {
|
||||||
|
toolName,
|
||||||
|
success: result.success,
|
||||||
|
hasOutput: !!result.output,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
toolCallId,
|
||||||
|
result: {
|
||||||
|
success: result.success,
|
||||||
|
output: result.output,
|
||||||
|
error: result.error,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
logger.debug(`[${tracker.requestId}] Zod validation error`, { issues: error.issues })
|
||||||
|
return createBadRequestResponse('Invalid request body for execute-tool')
|
||||||
|
}
|
||||||
|
logger.error(`[${tracker.requestId}] Failed to execute tool:`, error)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to execute tool'
|
||||||
|
return createInternalServerErrorResponse(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
|
||||||
import {
|
import {
|
||||||
authenticateCopilotRequestSessionOnly,
|
authenticateCopilotRequestSessionOnly,
|
||||||
createBadRequestResponse,
|
createBadRequestResponse,
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
} from '@/lib/copilot/request-helpers'
|
} from '@/lib/copilot/request-helpers'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
|
|
||||||
|
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||||
|
|
||||||
const BodySchema = z.object({
|
const BodySchema = z.object({
|
||||||
messageId: z.string(),
|
messageId: z.string(),
|
||||||
diffCreated: z.boolean(),
|
diffCreated: z.boolean(),
|
||||||
|
|||||||
123
apps/sim/app/api/copilot/tools/mark-complete/route.ts
Normal file
123
apps/sim/app/api/copilot/tools/mark-complete/route.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { createLogger } from '@sim/logger'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
|
||||||
|
import {
|
||||||
|
authenticateCopilotRequestSessionOnly,
|
||||||
|
createBadRequestResponse,
|
||||||
|
createInternalServerErrorResponse,
|
||||||
|
createRequestTracker,
|
||||||
|
createUnauthorizedResponse,
|
||||||
|
} from '@/lib/copilot/request-helpers'
|
||||||
|
import { env } from '@/lib/core/config/env'
|
||||||
|
|
||||||
|
const logger = createLogger('CopilotMarkToolCompleteAPI')
|
||||||
|
|
||||||
|
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||||
|
|
||||||
|
const MarkCompleteSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
status: z.number().int(),
|
||||||
|
message: z.any().optional(),
|
||||||
|
data: z.any().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/copilot/tools/mark-complete
|
||||||
|
* Proxy to Sim Agent: POST /api/tools/mark-complete
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const tracker = createRequestTracker()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
|
||||||
|
if (!isAuthenticated || !userId) {
|
||||||
|
return createUnauthorizedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json()
|
||||||
|
|
||||||
|
// Log raw body shape for diagnostics (avoid dumping huge payloads)
|
||||||
|
try {
|
||||||
|
const bodyPreview = JSON.stringify(body).slice(0, 300)
|
||||||
|
logger.debug(`[${tracker.requestId}] Incoming mark-complete raw body preview`, {
|
||||||
|
preview: `${bodyPreview}${bodyPreview.length === 300 ? '...' : ''}`,
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const parsed = MarkCompleteSchema.parse(body)
|
||||||
|
|
||||||
|
const messagePreview = (() => {
|
||||||
|
try {
|
||||||
|
const s =
|
||||||
|
typeof parsed.message === 'string' ? parsed.message : JSON.stringify(parsed.message)
|
||||||
|
return s ? `${s.slice(0, 200)}${s.length > 200 ? '...' : ''}` : undefined
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Forwarding tool mark-complete`, {
|
||||||
|
userId,
|
||||||
|
toolCallId: parsed.id,
|
||||||
|
toolName: parsed.name,
|
||||||
|
status: parsed.status,
|
||||||
|
hasMessage: parsed.message !== undefined,
|
||||||
|
hasData: parsed.data !== undefined,
|
||||||
|
messagePreview,
|
||||||
|
agentUrl: `${SIM_AGENT_API_URL}/api/tools/mark-complete`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const agentRes = await fetch(`${SIM_AGENT_API_URL}/api/tools/mark-complete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(parsed),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Attempt to parse agent response JSON
|
||||||
|
let agentJson: any = null
|
||||||
|
let agentText: string | null = null
|
||||||
|
try {
|
||||||
|
agentJson = await agentRes.json()
|
||||||
|
} catch (_) {
|
||||||
|
try {
|
||||||
|
agentText = await agentRes.text()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[${tracker.requestId}] Agent responded to mark-complete`, {
|
||||||
|
status: agentRes.status,
|
||||||
|
ok: agentRes.ok,
|
||||||
|
responseJsonPreview: agentJson ? JSON.stringify(agentJson).slice(0, 300) : undefined,
|
||||||
|
responseTextPreview: agentText ? agentText.slice(0, 300) : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (agentRes.ok) {
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
agentJson?.error || agentText || `Agent responded with status ${agentRes.status}`
|
||||||
|
const status = agentRes.status >= 500 ? 500 : 400
|
||||||
|
|
||||||
|
logger.warn(`[${tracker.requestId}] Mark-complete failed`, {
|
||||||
|
status,
|
||||||
|
error: errorMessage,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ success: false, error: errorMessage }, { status })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
logger.warn(`[${tracker.requestId}] Invalid mark-complete request body`, {
|
||||||
|
issues: error.issues,
|
||||||
|
})
|
||||||
|
return createBadRequestResponse('Invalid request body for mark-complete')
|
||||||
|
}
|
||||||
|
logger.error(`[${tracker.requestId}] Failed to proxy mark-complete:`, error)
|
||||||
|
return createInternalServerErrorResponse('Failed to mark tool as complete')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ const DEFAULT_ENABLED_MODELS: Record<CopilotModelId, boolean> = {
|
|||||||
'claude-4-sonnet': false,
|
'claude-4-sonnet': false,
|
||||||
'claude-4.5-haiku': true,
|
'claude-4.5-haiku': true,
|
||||||
'claude-4.5-sonnet': true,
|
'claude-4.5-sonnet': true,
|
||||||
'claude-4.6-opus': true,
|
|
||||||
'claude-4.5-opus': true,
|
'claude-4.5-opus': true,
|
||||||
'claude-4.1-opus': false,
|
'claude-4.1-opus': false,
|
||||||
'gemini-3-pro': true,
|
'gemini-3-pro': true,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function setupFileApiMocks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: authenticated,
|
success: authenticated,
|
||||||
userId: authenticated ? 'test-user-id' : undefined,
|
userId: authenticated ? 'test-user-id' : undefined,
|
||||||
error: authenticated ? undefined : 'Unauthorized',
|
error: authenticated ? undefined : 'Unauthorized',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import type { StorageContext } from '@/lib/uploads/config'
|
import type { StorageContext } from '@/lib/uploads/config'
|
||||||
import { deleteFile, hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
import { deleteFile, hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
||||||
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
||||||
@@ -24,7 +24,7 @@ const logger = createLogger('FilesDeleteAPI')
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn('Unauthorized file delete request', {
|
logger.warn('Unauthorized file delete request', {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import type { StorageContext } from '@/lib/uploads/config'
|
import type { StorageContext } from '@/lib/uploads/config'
|
||||||
import { hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
import { hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
||||||
import { verifyFileAccess } from '@/app/api/files/authorization'
|
import { verifyFileAccess } from '@/app/api/files/authorization'
|
||||||
@@ -12,7 +12,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn('Unauthorized download URL request', {
|
logger.warn('Unauthorized download URL request', {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function setupFileApiMocks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: authenticated,
|
success: authenticated,
|
||||||
userId: authenticated ? 'test-user-id' : undefined,
|
userId: authenticated ? 'test-user-id' : undefined,
|
||||||
error: authenticated ? undefined : 'Unauthorized',
|
error: authenticated ? undefined : 'Unauthorized',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from 'path'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import binaryExtensionsList from 'binary-extensions'
|
import binaryExtensionsList from 'binary-extensions'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
secureFetchWithPinnedIP,
|
secureFetchWithPinnedIP,
|
||||||
validateUrlWithDNS,
|
validateUrlWithDNS,
|
||||||
@@ -66,7 +66,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: true })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: true })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn('Unauthorized file parse request', {
|
logger.warn('Unauthorized file parse request', {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe('File Serve API Route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -165,7 +165,7 @@ describe('File Serve API Route', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -226,7 +226,7 @@ describe('File Serve API Route', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -291,7 +291,7 @@ describe('File Serve API Route', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -350,7 +350,7 @@ describe('File Serve API Route', () => {
|
|||||||
for (const test of contentTypeTests) {
|
for (const test of contentTypeTests) {
|
||||||
it(`should serve ${test.ext} file with correct content type`, async () => {
|
it(`should serve ${test.ext} file with correct content type`, async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { readFile } from 'fs/promises'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
|
import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
|
||||||
import type { StorageContext } from '@/lib/uploads/config'
|
import type { StorageContext } from '@/lib/uploads/config'
|
||||||
import { downloadFile } from '@/lib/uploads/core/storage-service'
|
import { downloadFile } from '@/lib/uploads/core/storage-service'
|
||||||
@@ -49,7 +49,7 @@ export async function GET(
|
|||||||
return await handleLocalFilePublic(fullPath)
|
return await handleLocalFilePublic(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn('Unauthorized file access attempt', {
|
logger.warn('Unauthorized file access attempt', {
|
||||||
|
|||||||
@@ -845,6 +845,8 @@ export async function POST(req: NextRequest) {
|
|||||||
contextVariables,
|
contextVariables,
|
||||||
timeoutMs: timeout,
|
timeoutMs: timeout,
|
||||||
requestId,
|
requestId,
|
||||||
|
ownerKey: `user:${auth.userId}`,
|
||||||
|
ownerWeight: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const executionTime = Date.now() - startTime
|
const executionTime = Date.now() - startTime
|
||||||
|
|||||||
@@ -23,7 +23,16 @@ export async function POST(request: NextRequest) {
|
|||||||
topK,
|
topK,
|
||||||
model,
|
model,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
azureEndpoint,
|
||||||
|
azureApiVersion,
|
||||||
|
vertexProject,
|
||||||
|
vertexLocation,
|
||||||
|
vertexCredential,
|
||||||
|
bedrockAccessKeyId,
|
||||||
|
bedrockSecretKey,
|
||||||
|
bedrockRegion,
|
||||||
workflowId,
|
workflowId,
|
||||||
|
workspaceId,
|
||||||
piiEntityTypes,
|
piiEntityTypes,
|
||||||
piiMode,
|
piiMode,
|
||||||
piiLanguage,
|
piiLanguage,
|
||||||
@@ -110,7 +119,18 @@ export async function POST(request: NextRequest) {
|
|||||||
topK,
|
topK,
|
||||||
model,
|
model,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
{
|
||||||
|
azureEndpoint,
|
||||||
|
azureApiVersion,
|
||||||
|
vertexProject,
|
||||||
|
vertexLocation,
|
||||||
|
vertexCredential,
|
||||||
|
bedrockAccessKeyId,
|
||||||
|
bedrockSecretKey,
|
||||||
|
bedrockRegion,
|
||||||
|
},
|
||||||
workflowId,
|
workflowId,
|
||||||
|
workspaceId,
|
||||||
piiEntityTypes,
|
piiEntityTypes,
|
||||||
piiMode,
|
piiMode,
|
||||||
piiLanguage,
|
piiLanguage,
|
||||||
@@ -178,7 +198,18 @@ async function executeValidation(
|
|||||||
topK: string | undefined,
|
topK: string | undefined,
|
||||||
model: string,
|
model: string,
|
||||||
apiKey: string | undefined,
|
apiKey: string | undefined,
|
||||||
|
providerCredentials: {
|
||||||
|
azureEndpoint?: string
|
||||||
|
azureApiVersion?: string
|
||||||
|
vertexProject?: string
|
||||||
|
vertexLocation?: string
|
||||||
|
vertexCredential?: string
|
||||||
|
bedrockAccessKeyId?: string
|
||||||
|
bedrockSecretKey?: string
|
||||||
|
bedrockRegion?: string
|
||||||
|
},
|
||||||
workflowId: string | undefined,
|
workflowId: string | undefined,
|
||||||
|
workspaceId: string | undefined,
|
||||||
piiEntityTypes: string[] | undefined,
|
piiEntityTypes: string[] | undefined,
|
||||||
piiMode: string | undefined,
|
piiMode: string | undefined,
|
||||||
piiLanguage: string | undefined,
|
piiLanguage: string | undefined,
|
||||||
@@ -219,7 +250,9 @@ async function executeValidation(
|
|||||||
topK: topK ? Number.parseInt(topK) : 10, // Default topK is 10
|
topK: topK ? Number.parseInt(topK) : 10, // Default topK is 10
|
||||||
model: model,
|
model: model,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
providerCredentials,
|
||||||
workflowId,
|
workflowId,
|
||||||
|
workspaceId,
|
||||||
requestId,
|
requestId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
||||||
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||||
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
||||||
@@ -19,19 +19,11 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
try {
|
try {
|
||||||
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
|
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
|
||||||
|
|
||||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow session and internal JWT auth (not API key)
|
|
||||||
if (auth.authType === 'api_key') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'API key auth not supported for this endpoint' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For session auth, verify KB access. Internal JWT is trusted.
|
// For session auth, verify KB access. Internal JWT is trusted.
|
||||||
if (auth.authType === 'session' && auth.userId) {
|
if (auth.authType === 'session' && auth.userId) {
|
||||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||||
@@ -64,19 +56,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
try {
|
try {
|
||||||
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
|
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
|
||||||
|
|
||||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow session and internal JWT auth (not API key)
|
|
||||||
if (auth.authType === 'api_key') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'API key auth not supported for this endpoint' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For session auth, verify KB access. Internal JWT is trusted.
|
// For session auth, verify KB access. Internal JWT is trusted.
|
||||||
if (auth.authType === 'session' && auth.userId) {
|
if (auth.authType === 'session' && auth.userId) {
|
||||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export async function GET(
|
|||||||
try {
|
try {
|
||||||
const { executionId } = await params
|
const { executionId } = await params
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`)
|
logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpAuthorizationServerMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
||||||
return createMcpProtectedResourceMetadataResponse(request)
|
|
||||||
}
|
|
||||||
@@ -1,790 +0,0 @@
|
|||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
||||||
import {
|
|
||||||
CallToolRequestSchema,
|
|
||||||
type CallToolResult,
|
|
||||||
ErrorCode,
|
|
||||||
type JSONRPCError,
|
|
||||||
type ListToolsResult,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
McpError,
|
|
||||||
type RequestId,
|
|
||||||
} from '@modelcontextprotocol/sdk/types.js'
|
|
||||||
import { db } from '@sim/db'
|
|
||||||
import { userStats } from '@sim/db/schema'
|
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { randomUUID } from 'node:crypto'
|
|
||||||
import { eq, sql } from 'drizzle-orm'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
|
||||||
import { getCopilotModel } from '@/lib/copilot/config'
|
|
||||||
import {
|
|
||||||
ORCHESTRATION_TIMEOUT_MS,
|
|
||||||
SIM_AGENT_API_URL,
|
|
||||||
SIM_AGENT_VERSION,
|
|
||||||
} from '@/lib/copilot/constants'
|
|
||||||
import { RateLimiter } from '@/lib/core/rate-limiter'
|
|
||||||
import { env } from '@/lib/core/config/env'
|
|
||||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
|
||||||
import { orchestrateSubagentStream } from '@/lib/copilot/orchestrator/subagent'
|
|
||||||
import {
|
|
||||||
executeToolServerSide,
|
|
||||||
prepareExecutionContext,
|
|
||||||
} from '@/lib/copilot/orchestrator/tool-executor'
|
|
||||||
import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions'
|
|
||||||
import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
|
||||||
|
|
||||||
const logger = createLogger('CopilotMcpAPI')
|
|
||||||
const mcpRateLimiter = new RateLimiter()
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
export const runtime = 'nodejs'
|
|
||||||
export const maxDuration = 300
|
|
||||||
|
|
||||||
interface CopilotKeyAuthResult {
|
|
||||||
success: boolean
|
|
||||||
userId?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a copilot API key by forwarding it to the Go copilot service's
|
|
||||||
* `/api/validate-key` endpoint. Returns the associated userId on success.
|
|
||||||
*/
|
|
||||||
async function authenticateCopilotApiKey(apiKey: string): Promise<CopilotKeyAuthResult> {
|
|
||||||
try {
|
|
||||||
const internalSecret = env.INTERNAL_API_SECRET
|
|
||||||
if (!internalSecret) {
|
|
||||||
logger.error('INTERNAL_API_SECRET not configured')
|
|
||||||
return { success: false, error: 'Server configuration error' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-api-key': internalSecret,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ targetApiKey: apiKey }),
|
|
||||||
signal: AbortSignal.timeout(10_000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.json().catch(() => null)
|
|
||||||
const upstream = (body as Record<string, unknown>)?.message
|
|
||||||
const status = res.status
|
|
||||||
|
|
||||||
if (status === 401 || status === 403) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Invalid Copilot API key. Generate a new key in Settings → Copilot and set it in the x-api-key header.`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (status === 402) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Usage limit exceeded for this Copilot API key. Upgrade your plan or wait for your quota to reset.`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, error: String(upstream ?? 'Copilot API key validation failed') }
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await res.json()) as { ok?: boolean; userId?: string }
|
|
||||||
if (!data.ok || !data.userId) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Invalid Copilot API key. Generate a new key in Settings → Copilot.',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, userId: data.userId }
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Copilot API key validation failed', { error })
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Could not validate Copilot API key — the authentication service is temporarily unreachable. This is NOT a problem with the API key itself; please retry shortly.',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP Server instructions that guide LLMs on how to use the Sim copilot tools.
|
|
||||||
* This is included in the initialize response to help external LLMs understand
|
|
||||||
* the workflow lifecycle and best practices.
|
|
||||||
*/
|
|
||||||
const MCP_SERVER_INSTRUCTIONS = `
|
|
||||||
## Sim Workflow Copilot
|
|
||||||
|
|
||||||
Sim is a workflow automation platform. Workflows are visual pipelines of connected blocks (Agent, Function, Condition, API, integrations, etc.). The Agent block is the core — an LLM with tools, memory, structured output, and knowledge bases.
|
|
||||||
|
|
||||||
### Workflow Lifecycle (Happy Path)
|
|
||||||
|
|
||||||
1. \`list_workspaces\` → know where to work
|
|
||||||
2. \`create_workflow(name, workspaceId)\` → get a workflowId
|
|
||||||
3. \`sim_build(request, workflowId)\` → plan and build in one pass
|
|
||||||
4. \`sim_test(request, workflowId)\` → verify it works
|
|
||||||
5. \`sim_deploy("deploy as api", workflowId)\` → make it accessible externally (optional)
|
|
||||||
|
|
||||||
For fine-grained control, use \`sim_plan\` → \`sim_edit\` instead of \`sim_build\`. Pass the plan object from sim_plan EXACTLY as-is to sim_edit's context.plan field.
|
|
||||||
|
|
||||||
### Working with Existing Workflows
|
|
||||||
|
|
||||||
When the user refers to a workflow by name or description ("the email one", "my Slack bot"):
|
|
||||||
1. Use \`sim_discovery\` to find it by functionality
|
|
||||||
2. Or use \`list_workflows\` and match by name
|
|
||||||
3. Then pass the workflowId to other tools
|
|
||||||
|
|
||||||
### Organization
|
|
||||||
|
|
||||||
- \`rename_workflow\` — rename a workflow
|
|
||||||
- \`move_workflow\` — move a workflow into a folder (or root with null)
|
|
||||||
- \`move_folder\` — nest a folder inside another (or root with null)
|
|
||||||
- \`create_folder(name, parentId)\` — create nested folder hierarchies
|
|
||||||
|
|
||||||
### Key Rules
|
|
||||||
|
|
||||||
- You can test workflows immediately after building — deployment is only needed for external access (API, chat, MCP).
|
|
||||||
- All copilot tools (build, plan, edit, deploy, test, debug) require workflowId.
|
|
||||||
- If the user reports errors → use \`sim_debug\` first, don't guess.
|
|
||||||
- Variable syntax: \`<blockname.field>\` for block outputs, \`{{ENV_VAR}}\` for env vars.
|
|
||||||
`
|
|
||||||
|
|
||||||
type HeaderMap = Record<string, string | string[] | undefined>
|
|
||||||
|
|
||||||
function createError(id: RequestId, code: ErrorCode | number, message: string): JSONRPCError {
|
|
||||||
return {
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id,
|
|
||||||
error: { code, message },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRequestHeaders(request: NextRequest): HeaderMap {
|
|
||||||
const headers: HeaderMap = {}
|
|
||||||
|
|
||||||
request.headers.forEach((value, key) => {
|
|
||||||
headers[key.toLowerCase()] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
function readHeader(headers: HeaderMap | undefined, name: string): string | undefined {
|
|
||||||
if (!headers) return undefined
|
|
||||||
const value = headers[name.toLowerCase()]
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value[0]
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
class NextResponseCapture {
|
|
||||||
private _status = 200
|
|
||||||
private _headers = new Headers()
|
|
||||||
private _controller: ReadableStreamDefaultController<Uint8Array> | null = null
|
|
||||||
private _pendingChunks: Uint8Array[] = []
|
|
||||||
private _closeHandlers: Array<() => void> = []
|
|
||||||
private _errorHandlers: Array<(error: Error) => void> = []
|
|
||||||
private _headersWritten = false
|
|
||||||
private _ended = false
|
|
||||||
private _headersPromise: Promise<void>
|
|
||||||
private _resolveHeaders: (() => void) | null = null
|
|
||||||
private _endedPromise: Promise<void>
|
|
||||||
private _resolveEnded: (() => void) | null = null
|
|
||||||
readonly readable: ReadableStream<Uint8Array>
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._headersPromise = new Promise<void>((resolve) => {
|
|
||||||
this._resolveHeaders = resolve
|
|
||||||
})
|
|
||||||
|
|
||||||
this._endedPromise = new Promise<void>((resolve) => {
|
|
||||||
this._resolveEnded = resolve
|
|
||||||
})
|
|
||||||
|
|
||||||
this.readable = new ReadableStream<Uint8Array>({
|
|
||||||
start: (controller) => {
|
|
||||||
this._controller = controller
|
|
||||||
if (this._pendingChunks.length > 0) {
|
|
||||||
for (const chunk of this._pendingChunks) {
|
|
||||||
controller.enqueue(chunk)
|
|
||||||
}
|
|
||||||
this._pendingChunks = []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel: () => {
|
|
||||||
this._ended = true
|
|
||||||
this._resolveEnded?.()
|
|
||||||
this.triggerCloseHandlers()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private markHeadersWritten(): void {
|
|
||||||
if (this._headersWritten) return
|
|
||||||
this._headersWritten = true
|
|
||||||
this._resolveHeaders?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
private triggerCloseHandlers(): void {
|
|
||||||
for (const handler of this._closeHandlers) {
|
|
||||||
try {
|
|
||||||
handler()
|
|
||||||
} catch (error) {
|
|
||||||
this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private triggerErrorHandlers(error: Error): void {
|
|
||||||
for (const errorHandler of this._errorHandlers) {
|
|
||||||
errorHandler(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeChunk(chunk: unknown): Uint8Array | null {
|
|
||||||
if (typeof chunk === 'string') {
|
|
||||||
return new TextEncoder().encode(chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk instanceof Uint8Array) {
|
|
||||||
return chunk
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk === undefined || chunk === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TextEncoder().encode(String(chunk))
|
|
||||||
}
|
|
||||||
|
|
||||||
writeHead(status: number, headers?: Record<string, string | number | string[]>): this {
|
|
||||||
this._status = status
|
|
||||||
|
|
||||||
if (headers) {
|
|
||||||
Object.entries(headers).forEach(([key, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
this._headers.set(key, value.join(', '))
|
|
||||||
} else {
|
|
||||||
this._headers.set(key, String(value))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.markHeadersWritten()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
flushHeaders(): this {
|
|
||||||
this.markHeadersWritten()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
write(chunk: unknown): boolean {
|
|
||||||
const normalized = this.normalizeChunk(chunk)
|
|
||||||
if (!normalized) return true
|
|
||||||
|
|
||||||
this.markHeadersWritten()
|
|
||||||
|
|
||||||
if (this._controller) {
|
|
||||||
try {
|
|
||||||
this._controller.enqueue(normalized)
|
|
||||||
} catch (error) {
|
|
||||||
this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._pendingChunks.push(normalized)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
end(chunk?: unknown): this {
|
|
||||||
if (chunk !== undefined) this.write(chunk)
|
|
||||||
this.markHeadersWritten()
|
|
||||||
if (this._ended) return this
|
|
||||||
|
|
||||||
this._ended = true
|
|
||||||
this._resolveEnded?.()
|
|
||||||
|
|
||||||
if (this._controller) {
|
|
||||||
try {
|
|
||||||
this._controller.close()
|
|
||||||
} catch (error) {
|
|
||||||
this.triggerErrorHandlers(error instanceof Error ? error : new Error(String(error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerCloseHandlers()
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForHeaders(timeoutMs = 30000): Promise<void> {
|
|
||||||
if (this._headersWritten) return
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
this._headersPromise,
|
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, timeoutMs)
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForEnd(timeoutMs = 30000): Promise<void> {
|
|
||||||
if (this._ended) return
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
this._endedPromise,
|
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, timeoutMs)
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
on(event: 'close' | 'error', handler: (() => void) | ((error: Error) => void)): this {
|
|
||||||
if (event === 'close') {
|
|
||||||
this._closeHandlers.push(handler as () => void)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event === 'error') {
|
|
||||||
this._errorHandlers.push(handler as (error: Error) => void)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
toNextResponse(): NextResponse {
|
|
||||||
return new NextResponse(this.readable, {
|
|
||||||
status: this._status,
|
|
||||||
headers: this._headers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMcpServer(abortSignal?: AbortSignal): Server {
|
|
||||||
const server = new Server(
|
|
||||||
{
|
|
||||||
name: 'sim-copilot',
|
|
||||||
version: '1.0.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capabilities: { tools: {} },
|
|
||||||
instructions: MCP_SERVER_INSTRUCTIONS,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
||||||
const directTools = DIRECT_TOOL_DEFS.map((tool) => ({
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
inputSchema: tool.inputSchema,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const subagentTools = SUBAGENT_TOOL_DEFS.map((tool) => ({
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
inputSchema: tool.inputSchema,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const result: ListToolsResult = {
|
|
||||||
tools: [...directTools, ...subagentTools],
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
||||||
const headers = (extra.requestInfo?.headers || {}) as HeaderMap
|
|
||||||
const apiKeyHeader = readHeader(headers, 'x-api-key')
|
|
||||||
|
|
||||||
if (!apiKeyHeader) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text' as const,
|
|
||||||
text: 'AUTHENTICATION ERROR: No Copilot API key provided. The user must set their Copilot API key in the x-api-key header. They can generate one in the Sim app under Settings → Copilot. Do NOT retry — this will fail until the key is configured.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const authResult = await authenticateCopilotApiKey(apiKeyHeader)
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
|
||||||
logger.warn('MCP copilot key auth failed', { method: request.method })
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text' as const,
|
|
||||||
text: `AUTHENTICATION ERROR: ${authResult.error} Do NOT retry — this will fail until the user fixes their Copilot API key.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rateLimitResult = await mcpRateLimiter.checkRateLimitWithSubscription(
|
|
||||||
authResult.userId,
|
|
||||||
await getHighestPrioritySubscription(authResult.userId),
|
|
||||||
'api-endpoint',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!rateLimitResult.allowed) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text' as const,
|
|
||||||
text: `RATE LIMIT: Too many requests. Please wait and retry after ${rateLimitResult.resetAt.toISOString()}.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = request.params as { name?: string; arguments?: Record<string, unknown> } | undefined
|
|
||||||
if (!params?.name) {
|
|
||||||
throw new McpError(ErrorCode.InvalidParams, 'Tool name required')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await handleToolsCall(
|
|
||||||
{
|
|
||||||
name: params.name,
|
|
||||||
arguments: params.arguments,
|
|
||||||
},
|
|
||||||
authResult.userId,
|
|
||||||
abortSignal
|
|
||||||
)
|
|
||||||
|
|
||||||
trackMcpCopilotCall(authResult.userId)
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMcpRequestWithSdk(
|
|
||||||
request: NextRequest,
|
|
||||||
parsedBody: unknown
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
const server = buildMcpServer(request.signal)
|
|
||||||
const transport = new StreamableHTTPServerTransport({
|
|
||||||
sessionIdGenerator: undefined,
|
|
||||||
enableJsonResponse: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const responseCapture = new NextResponseCapture()
|
|
||||||
const requestAdapter = {
|
|
||||||
method: request.method,
|
|
||||||
headers: normalizeRequestHeaders(request),
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.connect(transport)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await transport.handleRequest(requestAdapter as any, responseCapture as any, parsedBody)
|
|
||||||
await responseCapture.waitForHeaders()
|
|
||||||
// Must exceed the longest possible tool execution (build = 5 min).
|
|
||||||
// Using ORCHESTRATION_TIMEOUT_MS + 60 s buffer so the orchestrator can
|
|
||||||
// finish or time-out on its own before the transport is torn down.
|
|
||||||
await responseCapture.waitForEnd(ORCHESTRATION_TIMEOUT_MS + 60_000)
|
|
||||||
return responseCapture.toNextResponse()
|
|
||||||
} finally {
|
|
||||||
await server.close().catch(() => {})
|
|
||||||
await transport.close().catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
// Return 405 to signal that server-initiated SSE notifications are not
|
|
||||||
// supported. Without this, clients like mcp-remote will repeatedly
|
|
||||||
// reconnect trying to open an SSE stream, flooding the logs with GETs.
|
|
||||||
return new NextResponse(null, { status: 405 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
let parsedBody: unknown
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsedBody = await request.json()
|
|
||||||
} catch {
|
|
||||||
return NextResponse.json(createError(0, ErrorCode.ParseError, 'Invalid JSON body'), {
|
|
||||||
status: 400,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return await handleMcpRequestWithSdk(request, parsedBody)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error handling MCP request', { error })
|
|
||||||
return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), {
|
|
||||||
status: 500,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
|
||||||
void request
|
|
||||||
return NextResponse.json(createError(0, -32000, 'Method not allowed.'), { status: 405 })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment MCP copilot call counter in userStats (fire-and-forget).
|
|
||||||
*/
|
|
||||||
function trackMcpCopilotCall(userId: string): void {
|
|
||||||
db.update(userStats)
|
|
||||||
.set({
|
|
||||||
totalMcpCopilotCalls: sql`total_mcp_copilot_calls + 1`,
|
|
||||||
lastActive: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(userStats.userId, userId))
|
|
||||||
.then(() => {})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('Failed to track MCP copilot call', { error, userId })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToolsCall(
|
|
||||||
params: { name: string; arguments?: Record<string, unknown> },
|
|
||||||
userId: string,
|
|
||||||
abortSignal?: AbortSignal
|
|
||||||
): Promise<CallToolResult> {
|
|
||||||
const args = params.arguments || {}
|
|
||||||
|
|
||||||
const directTool = DIRECT_TOOL_DEFS.find((tool) => tool.name === params.name)
|
|
||||||
if (directTool) {
|
|
||||||
return handleDirectToolCall(directTool, args, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const subagentTool = SUBAGENT_TOOL_DEFS.find((tool) => tool.name === params.name)
|
|
||||||
if (subagentTool) {
|
|
||||||
return handleSubagentToolCall(subagentTool, args, userId, abortSignal)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${params.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDirectToolCall(
|
|
||||||
toolDef: (typeof DIRECT_TOOL_DEFS)[number],
|
|
||||||
args: Record<string, unknown>,
|
|
||||||
userId: string
|
|
||||||
): Promise<CallToolResult> {
|
|
||||||
try {
|
|
||||||
const execContext = await prepareExecutionContext(userId, (args.workflowId as string) || '')
|
|
||||||
|
|
||||||
const toolCall = {
|
|
||||||
id: randomUUID(),
|
|
||||||
name: toolDef.toolId,
|
|
||||||
status: 'pending' as const,
|
|
||||||
params: args as Record<string, any>,
|
|
||||||
startTime: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await executeToolServerSide(toolCall, execContext)
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: JSON.stringify(result.output ?? result, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: !result.success,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Direct tool execution failed', { tool: toolDef.name, error })
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build mode uses the main chat orchestrator with the 'fast' command instead of
|
|
||||||
* the subagent endpoint. In Go, 'build' is not a registered subagent — it's a mode
|
|
||||||
* (ModeFast) on the main chat processor that bypasses subagent orchestration and
|
|
||||||
* executes all tools directly.
|
|
||||||
*/
|
|
||||||
async function handleBuildToolCall(
|
|
||||||
args: Record<string, unknown>,
|
|
||||||
userId: string,
|
|
||||||
abortSignal?: AbortSignal
|
|
||||||
): Promise<CallToolResult> {
|
|
||||||
try {
|
|
||||||
const requestText = (args.request as string) || JSON.stringify(args)
|
|
||||||
const { model } = getCopilotModel('chat')
|
|
||||||
const workflowId = args.workflowId as string | undefined
|
|
||||||
|
|
||||||
const resolved = workflowId ? { workflowId } : await resolveWorkflowIdForUser(userId)
|
|
||||||
|
|
||||||
if (!resolved?.workflowId) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: JSON.stringify(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'workflowId is required for build. Call create_workflow first.',
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatId = randomUUID()
|
|
||||||
|
|
||||||
const requestPayload = {
|
|
||||||
message: requestText,
|
|
||||||
workflowId: resolved.workflowId,
|
|
||||||
userId,
|
|
||||||
model,
|
|
||||||
mode: 'agent',
|
|
||||||
commands: ['fast'],
|
|
||||||
messageId: randomUUID(),
|
|
||||||
version: SIM_AGENT_VERSION,
|
|
||||||
headless: true,
|
|
||||||
chatId,
|
|
||||||
source: 'mcp',
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await orchestrateCopilotStream(requestPayload, {
|
|
||||||
userId,
|
|
||||||
workflowId: resolved.workflowId,
|
|
||||||
chatId,
|
|
||||||
autoExecuteTools: true,
|
|
||||||
timeout: 300000,
|
|
||||||
interactive: false,
|
|
||||||
abortSignal,
|
|
||||||
})
|
|
||||||
|
|
||||||
const responseData = {
|
|
||||||
success: result.success,
|
|
||||||
content: result.content,
|
|
||||||
toolCalls: result.toolCalls,
|
|
||||||
error: result.error,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: 'text', text: JSON.stringify(responseData, null, 2) }],
|
|
||||||
isError: !result.success,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Build tool call failed', { error })
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Build failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubagentToolCall(
|
|
||||||
toolDef: (typeof SUBAGENT_TOOL_DEFS)[number],
|
|
||||||
args: Record<string, unknown>,
|
|
||||||
userId: string,
|
|
||||||
abortSignal?: AbortSignal
|
|
||||||
): Promise<CallToolResult> {
|
|
||||||
if (toolDef.agentId === 'build') {
|
|
||||||
return handleBuildToolCall(args, userId, abortSignal)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestText =
|
|
||||||
(args.request as string) ||
|
|
||||||
(args.message as string) ||
|
|
||||||
(args.error as string) ||
|
|
||||||
JSON.stringify(args)
|
|
||||||
|
|
||||||
const context = (args.context as Record<string, unknown>) || {}
|
|
||||||
if (args.plan && !context.plan) {
|
|
||||||
context.plan = args.plan
|
|
||||||
}
|
|
||||||
|
|
||||||
const { model } = getCopilotModel('chat')
|
|
||||||
|
|
||||||
const result = await orchestrateSubagentStream(
|
|
||||||
toolDef.agentId,
|
|
||||||
{
|
|
||||||
message: requestText,
|
|
||||||
workflowId: args.workflowId,
|
|
||||||
workspaceId: args.workspaceId,
|
|
||||||
context,
|
|
||||||
model,
|
|
||||||
headless: true,
|
|
||||||
source: 'mcp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
workflowId: args.workflowId as string | undefined,
|
|
||||||
workspaceId: args.workspaceId as string | undefined,
|
|
||||||
abortSignal,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
let responseData: unknown
|
|
||||||
|
|
||||||
if (result.structuredResult) {
|
|
||||||
responseData = {
|
|
||||||
success: result.structuredResult.success ?? result.success,
|
|
||||||
type: result.structuredResult.type,
|
|
||||||
summary: result.structuredResult.summary,
|
|
||||||
data: result.structuredResult.data,
|
|
||||||
}
|
|
||||||
} else if (result.error) {
|
|
||||||
responseData = {
|
|
||||||
success: false,
|
|
||||||
error: result.error,
|
|
||||||
errors: result.errors,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
responseData = {
|
|
||||||
success: result.success,
|
|
||||||
content: result.content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: JSON.stringify(responseData, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: !result.success,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Subagent tool call failed', {
|
|
||||||
tool: toolDef.name,
|
|
||||||
agentId: toolDef.agentId,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Subagent call failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ async function validateMemoryAccess(
|
|||||||
requestId: string,
|
requestId: string,
|
||||||
action: 'read' | 'write'
|
action: 'read' | 'write'
|
||||||
): Promise<{ userId: string } | { error: NextResponse }> {
|
): Promise<{ userId: string } | { error: NextResponse }> {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { memory } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, isNull, like } from 'drizzle-orm'
|
import { and, eq, isNull, like } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory access attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory access attempt`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -89,7 +89,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory creation attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory creation attempt`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -228,7 +228,7 @@ export async function DELETE(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory deletion attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory deletion attempt`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
const logger = createLogger('A2ACancelTaskAPI')
|
const logger = createLogger('A2ACancelTaskAPI')
|
||||||
@@ -20,7 +20,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`)
|
logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -20,7 +20,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
const logger = createLogger('A2AResubscribeAPI')
|
const logger = createLogger('A2AResubscribeAPI')
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`)
|
logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, {
|
logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
|
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
|
||||||
|
|
||||||
const logger = createLogger('UsageLogsAPI')
|
const logger = createLogger('UsageLogsAPI')
|
||||||
@@ -20,7 +20,7 @@ const QuerySchema = z.object({
|
|||||||
*/
|
*/
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { getCopilotModel } from '@/lib/copilot/config'
|
|
||||||
import { SIM_AGENT_VERSION } from '@/lib/copilot/constants'
|
|
||||||
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
|
|
||||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
|
||||||
import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
|
||||||
import { authenticateV1Request } from '@/app/api/v1/auth'
|
|
||||||
|
|
||||||
const logger = createLogger('CopilotHeadlessAPI')
|
|
||||||
|
|
||||||
const RequestSchema = z.object({
|
|
||||||
message: z.string().min(1, 'message is required'),
|
|
||||||
workflowId: z.string().optional(),
|
|
||||||
workflowName: z.string().optional(),
|
|
||||||
chatId: z.string().optional(),
|
|
||||||
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
|
|
||||||
model: z.string().optional(),
|
|
||||||
autoExecuteTools: z.boolean().optional().default(true),
|
|
||||||
timeout: z.number().optional().default(300000),
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/v1/copilot/chat
|
|
||||||
* Headless copilot endpoint for server-side orchestration.
|
|
||||||
*
|
|
||||||
* workflowId is optional - if not provided:
|
|
||||||
* - If workflowName is provided, finds that workflow
|
|
||||||
* - Otherwise uses the user's first workflow as context
|
|
||||||
* - The copilot can still operate on any workflow using list_user_workflows
|
|
||||||
*/
|
|
||||||
export async function POST(req: NextRequest) {
|
|
||||||
const auth = await authenticateV1Request(req)
|
|
||||||
if (!auth.authenticated || !auth.userId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: auth.error || 'Unauthorized' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = await req.json()
|
|
||||||
const parsed = RequestSchema.parse(body)
|
|
||||||
const defaults = getCopilotModel('chat')
|
|
||||||
const selectedModel = parsed.model || defaults.model
|
|
||||||
|
|
||||||
// Resolve workflow ID
|
|
||||||
const resolved = await resolveWorkflowIdForUser(
|
|
||||||
auth.userId,
|
|
||||||
parsed.workflowId,
|
|
||||||
parsed.workflowName
|
|
||||||
)
|
|
||||||
if (!resolved) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'No workflows found. Create a workflow first or provide a valid workflowId.',
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform mode to transport mode (same as client API)
|
|
||||||
// build and agent both map to 'agent' on the backend
|
|
||||||
const effectiveMode = parsed.mode === 'agent' ? 'build' : parsed.mode
|
|
||||||
const transportMode = effectiveMode === 'build' ? 'agent' : effectiveMode
|
|
||||||
|
|
||||||
// Always generate a chatId - required for artifacts system to work with subagents
|
|
||||||
const chatId = parsed.chatId || crypto.randomUUID()
|
|
||||||
|
|
||||||
const requestPayload = {
|
|
||||||
message: parsed.message,
|
|
||||||
workflowId: resolved.workflowId,
|
|
||||||
userId: auth.userId,
|
|
||||||
model: selectedModel,
|
|
||||||
mode: transportMode,
|
|
||||||
messageId: crypto.randomUUID(),
|
|
||||||
version: SIM_AGENT_VERSION,
|
|
||||||
headless: true,
|
|
||||||
chatId,
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await orchestrateCopilotStream(requestPayload, {
|
|
||||||
userId: auth.userId,
|
|
||||||
workflowId: resolved.workflowId,
|
|
||||||
chatId,
|
|
||||||
autoExecuteTools: parsed.autoExecuteTools,
|
|
||||||
timeout: parsed.timeout,
|
|
||||||
interactive: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: result.success,
|
|
||||||
content: result.content,
|
|
||||||
toolCalls: result.toolCalls,
|
|
||||||
chatId: result.chatId || chatId, // Return the chatId for conversation continuity
|
|
||||||
conversationId: result.conversationId,
|
|
||||||
error: result.error,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Invalid request', details: error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('Headless copilot request failed', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
|
||||||
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -325,6 +325,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
requestId
|
requestId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Client-side sessions and personal API keys bill/permission-check the
|
||||||
|
// authenticated user, not the workspace billed account.
|
||||||
|
const useAuthenticatedUserAsActor =
|
||||||
|
isClientSession || (auth.authType === 'api_key' && auth.apiKeyType === 'personal')
|
||||||
|
|
||||||
const preprocessResult = await preprocessExecution({
|
const preprocessResult = await preprocessExecution({
|
||||||
workflowId,
|
workflowId,
|
||||||
userId,
|
userId,
|
||||||
@@ -334,6 +339,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
checkDeployment: !shouldUseDraftState,
|
checkDeployment: !shouldUseDraftState,
|
||||||
loggingSession,
|
loggingSession,
|
||||||
useDraftState: shouldUseDraftState,
|
useDraftState: shouldUseDraftState,
|
||||||
|
useAuthenticatedUserAsActor,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!preprocessResult.success) {
|
if (!preprocessResult.success) {
|
||||||
|
|||||||
@@ -74,8 +74,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isExecutionFile) {
|
if (isExecutionFile) {
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -88,16 +87,12 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
||||||
)
|
)
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to download file ${file.name}:`, error)
|
logger.error(`Failed to download file ${file.name}:`, error)
|
||||||
if (file.url) {
|
|
||||||
window.open(file.url, '_blank')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsDownloading(false)
|
setIsDownloading(false)
|
||||||
}
|
}
|
||||||
@@ -198,8 +193,7 @@ export function FileDownload({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isExecutionFile) {
|
if (isExecutionFile) {
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -212,16 +206,12 @@ export function FileDownload({
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
||||||
)
|
)
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to download file ${file.name}:`, error)
|
logger.error(`Failed to download file ${file.name}:`, error)
|
||||||
if (file.url) {
|
|
||||||
window.open(file.url, '_blank')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsDownloading(false)
|
setIsDownloading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function WorkflowSelector({
|
|||||||
onMouseDown={(e) => handleRemove(e, w.id)}
|
onMouseDown={(e) => handleRemove(e, w.id)}
|
||||||
>
|
>
|
||||||
{w.name}
|
{w.name}
|
||||||
<X className='h-3 w-3' />
|
<X className='!text-[var(--text-primary)] h-4 w-4 flex-shrink-0 opacity-50' />
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{selectedWorkflows.length > 2 && (
|
{selectedWorkflows.length > 2 && (
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
|||||||
if (block.type === 'text') {
|
if (block.type === 'text') {
|
||||||
const isLastTextBlock =
|
const isLastTextBlock =
|
||||||
index === message.contentBlocks!.length - 1 && block.type === 'text'
|
index === message.contentBlocks!.length - 1 && block.type === 'text'
|
||||||
const parsed = parseSpecialTags(block.content ?? '')
|
const parsed = parseSpecialTags(block.content)
|
||||||
// Mask credential IDs in the displayed content
|
// Mask credential IDs in the displayed content
|
||||||
const cleanBlockContent = maskCredentialValue(
|
const cleanBlockContent = maskCredentialValue(
|
||||||
parsed.cleanContent.replace(/\n{3,}/g, '\n\n')
|
parsed.cleanContent.replace(/\n{3,}/g, '\n\n')
|
||||||
@@ -243,7 +243,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
|||||||
return (
|
return (
|
||||||
<div key={blockKey} className='w-full'>
|
<div key={blockKey} className='w-full'>
|
||||||
<ThinkingBlock
|
<ThinkingBlock
|
||||||
content={maskCredentialValue(block.content ?? '')}
|
content={maskCredentialValue(block.content)}
|
||||||
isStreaming={isActivelyStreaming}
|
isStreaming={isActivelyStreaming}
|
||||||
hasFollowingContent={hasFollowingContent}
|
hasFollowingContent={hasFollowingContent}
|
||||||
hasSpecialTags={hasSpecialTags}
|
hasSpecialTags={hasSpecialTags}
|
||||||
@@ -251,7 +251,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (block.type === 'tool_call' && block.toolCall) {
|
if (block.type === 'tool_call') {
|
||||||
const blockKey = `tool-${block.toolCall.id}`
|
const blockKey = `tool-${block.toolCall.id}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { ChevronUp, LayoutList } from 'lucide-react'
|
import { ChevronUp, LayoutList } from 'lucide-react'
|
||||||
import Editor from 'react-simple-code-editor'
|
import Editor from 'react-simple-code-editor'
|
||||||
import { Button, Code, getCodeEditorProps, highlight, languages } from '@/components/emcn'
|
import { Button, Code, getCodeEditorProps, highlight, languages } from '@/components/emcn'
|
||||||
|
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
|
||||||
|
import { getClientTool } from '@/lib/copilot/tools/client/manager'
|
||||||
|
import { getRegisteredTools } from '@/lib/copilot/tools/client/registry'
|
||||||
|
import '@/lib/copilot/tools/client/init-tool-configs'
|
||||||
import {
|
import {
|
||||||
ClientToolCallState,
|
getSubagentLabels as getSubagentLabelsFromConfig,
|
||||||
TOOL_DISPLAY_REGISTRY,
|
getToolUIConfig,
|
||||||
} from '@/lib/copilot/tools/client/tool-display-registry'
|
hasInterrupt as hasInterruptFromConfig,
|
||||||
|
isSpecialTool as isSpecialToolFromConfig,
|
||||||
|
} from '@/lib/copilot/tools/client/ui-config'
|
||||||
import { formatDuration } from '@/lib/core/utils/formatting'
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
|
import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
|
||||||
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
|
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
|
||||||
@@ -20,6 +25,7 @@ import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/co
|
|||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { CopilotToolCall } from '@/stores/panel'
|
import type { CopilotToolCall } from '@/stores/panel'
|
||||||
import { useCopilotStore } from '@/stores/panel'
|
import { useCopilotStore } from '@/stores/panel'
|
||||||
|
import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store'
|
||||||
import type { SubAgentContentBlock } from '@/stores/panel/copilot/types'
|
import type { SubAgentContentBlock } from '@/stores/panel/copilot/types'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
|
|
||||||
@@ -704,8 +710,8 @@ const ShimmerOverlayText = memo(function ShimmerOverlayText({
|
|||||||
* @returns The completion label from UI config, defaults to 'Thought'
|
* @returns The completion label from UI config, defaults to 'Thought'
|
||||||
*/
|
*/
|
||||||
function getSubagentCompletionLabel(toolName: string): string {
|
function getSubagentCompletionLabel(toolName: string): string {
|
||||||
const labels = TOOL_DISPLAY_REGISTRY[toolName]?.uiConfig?.subagentLabels
|
const labels = getSubagentLabelsFromConfig(toolName, false)
|
||||||
return labels?.completed || 'Thought'
|
return labels?.completed ?? 'Thought'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -937,7 +943,7 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
|
|||||||
* Determines if a tool call should display with special gradient styling.
|
* Determines if a tool call should display with special gradient styling.
|
||||||
*/
|
*/
|
||||||
function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
|
function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
|
||||||
return TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.isSpecial === true
|
return isSpecialToolFromConfig(toolCall.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1217,11 +1223,28 @@ const WorkflowEditSummary = memo(function WorkflowEditSummary({
|
|||||||
|
|
||||||
/** Checks if a tool is server-side executed (not a client tool) */
|
/** Checks if a tool is server-side executed (not a client tool) */
|
||||||
function isIntegrationTool(toolName: string): boolean {
|
function isIntegrationTool(toolName: string): boolean {
|
||||||
return !TOOL_DISPLAY_REGISTRY[toolName]
|
return !CLASS_TOOL_METADATA[toolName]
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
||||||
const hasInterrupt = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.interrupt === true
|
if (hasInterruptFromConfig(toolCall.name) && toolCall.state === 'pending') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = getClientTool(toolCall.id)
|
||||||
|
let hasInterrupt = !!instance?.getInterruptDisplays?.()
|
||||||
|
if (!hasInterrupt) {
|
||||||
|
try {
|
||||||
|
const def = getRegisteredTools()[toolCall.name]
|
||||||
|
if (def) {
|
||||||
|
hasInterrupt =
|
||||||
|
typeof def.hasInterrupt === 'function'
|
||||||
|
? !!def.hasInterrupt(toolCall.params || {})
|
||||||
|
: !!def.hasInterrupt
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasInterrupt && toolCall.state === 'pending') {
|
if (hasInterrupt && toolCall.state === 'pending') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1234,50 +1257,109 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolCallLogger = createLogger('CopilotToolCall')
|
|
||||||
|
|
||||||
async function sendToolDecision(
|
|
||||||
toolCallId: string,
|
|
||||||
status: 'accepted' | 'rejected' | 'background'
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await fetch('/api/copilot/confirm', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ toolCallId, status }),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
toolCallLogger.warn('Failed to send tool decision', {
|
|
||||||
toolCallId,
|
|
||||||
status,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRun(
|
async function handleRun(
|
||||||
toolCall: CopilotToolCall,
|
toolCall: CopilotToolCall,
|
||||||
setToolCallState: any,
|
setToolCallState: any,
|
||||||
onStateChange?: any,
|
onStateChange?: any,
|
||||||
editedParams?: any
|
editedParams?: any
|
||||||
) {
|
) {
|
||||||
setToolCallState(toolCall, 'executing', editedParams ? { params: editedParams } : undefined)
|
const instance = getClientTool(toolCall.id)
|
||||||
onStateChange?.('executing')
|
|
||||||
await sendToolDecision(toolCall.id, 'accepted')
|
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||||
|
onStateChange?.('executing')
|
||||||
|
try {
|
||||||
|
await useCopilotStore.getState().executeIntegrationTool(toolCall.id)
|
||||||
|
} catch (e) {
|
||||||
|
setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
|
||||||
|
onStateChange?.('error')
|
||||||
|
try {
|
||||||
|
await fetch('/api/copilot/tools/mark-complete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: toolCall.id,
|
||||||
|
name: toolCall.name,
|
||||||
|
status: 500,
|
||||||
|
message: e instanceof Error ? e.message : 'Tool execution failed',
|
||||||
|
data: { error: e instanceof Error ? e.message : String(e) },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
console.error('[handleRun] Failed to notify backend of tool error:', toolCall.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instance) return
|
||||||
|
try {
|
||||||
|
const mergedParams =
|
||||||
|
editedParams ||
|
||||||
|
(toolCall as any).params ||
|
||||||
|
(toolCall as any).parameters ||
|
||||||
|
(toolCall as any).input ||
|
||||||
|
{}
|
||||||
|
await instance.handleAccept?.(mergedParams)
|
||||||
|
onStateChange?.('executing')
|
||||||
|
} catch (e) {
|
||||||
|
setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) {
|
async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) {
|
||||||
|
const instance = getClientTool(toolCall.id)
|
||||||
|
|
||||||
|
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||||
|
setToolCallState(toolCall, 'rejected')
|
||||||
|
onStateChange?.('rejected')
|
||||||
|
|
||||||
|
let notified = false
|
||||||
|
for (let attempt = 0; attempt < 3 && !notified; attempt++) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/copilot/tools/mark-complete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: toolCall.id,
|
||||||
|
name: toolCall.name,
|
||||||
|
status: 400,
|
||||||
|
message: 'Tool execution skipped by user',
|
||||||
|
data: { skipped: true, reason: 'user_skipped' },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
notified = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (attempt < 2) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notified) {
|
||||||
|
console.error('[handleSkip] Failed to notify backend after 3 attempts:', toolCall.id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
try {
|
||||||
|
await instance.handleReject?.()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
setToolCallState(toolCall, 'rejected')
|
setToolCallState(toolCall, 'rejected')
|
||||||
onStateChange?.('rejected')
|
onStateChange?.('rejected')
|
||||||
await sendToolDecision(toolCall.id, 'rejected')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayName(toolCall: CopilotToolCall): string {
|
function getDisplayName(toolCall: CopilotToolCall): string {
|
||||||
const fromStore = (toolCall as any).display?.text
|
const fromStore = (toolCall as any).display?.text
|
||||||
if (fromStore) return fromStore
|
if (fromStore) return fromStore
|
||||||
const registryEntry = TOOL_DISPLAY_REGISTRY[toolCall.name]
|
try {
|
||||||
const byState = registryEntry?.displayNames?.[toolCall.state as ClientToolCallState]
|
const def = getRegisteredTools()[toolCall.name] as any
|
||||||
if (byState?.text) return byState.text
|
const byState = def?.metadata?.displayNames?.[toolCall.state]
|
||||||
|
if (byState?.text) return byState.text
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const stateVerb = getStateVerb(toolCall.state)
|
const stateVerb = getStateVerb(toolCall.state)
|
||||||
const formattedName = formatToolName(toolCall.name)
|
const formattedName = formatToolName(toolCall.name)
|
||||||
@@ -1427,7 +1509,7 @@ export function ToolCall({
|
|||||||
// Check if this integration tool is auto-allowed
|
// Check if this integration tool is auto-allowed
|
||||||
// Subscribe to autoAllowedTools so we re-render when it changes
|
// Subscribe to autoAllowedTools so we re-render when it changes
|
||||||
const autoAllowedTools = useCopilotStore((s) => s.autoAllowedTools)
|
const autoAllowedTools = useCopilotStore((s) => s.autoAllowedTools)
|
||||||
const { removeAutoAllowedTool, setToolCallState } = useCopilotStore()
|
const { removeAutoAllowedTool } = useCopilotStore()
|
||||||
const isAutoAllowed = isIntegrationTool(toolCall.name) && autoAllowedTools.includes(toolCall.name)
|
const isAutoAllowed = isIntegrationTool(toolCall.name) && autoAllowedTools.includes(toolCall.name)
|
||||||
|
|
||||||
// Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change)
|
// Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change)
|
||||||
@@ -1444,12 +1526,34 @@ export function ToolCall({
|
|||||||
toolCall.name === 'mark_todo_in_progress' ||
|
toolCall.name === 'mark_todo_in_progress' ||
|
||||||
toolCall.name === 'tool_search_tool_regex' ||
|
toolCall.name === 'tool_search_tool_regex' ||
|
||||||
toolCall.name === 'user_memory' ||
|
toolCall.name === 'user_memory' ||
|
||||||
toolCall.name.endsWith('_respond')
|
toolCall.name === 'edit_respond' ||
|
||||||
|
toolCall.name === 'debug_respond' ||
|
||||||
|
toolCall.name === 'plan_respond' ||
|
||||||
|
toolCall.name === 'research_respond' ||
|
||||||
|
toolCall.name === 'info_respond' ||
|
||||||
|
toolCall.name === 'deploy_respond' ||
|
||||||
|
toolCall.name === 'superagent_respond'
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
// Special rendering for subagent tools - show as thinking text with tool calls at top level
|
// Special rendering for subagent tools - show as thinking text with tool calls at top level
|
||||||
const isSubagentTool = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.subagent === true
|
const SUBAGENT_TOOLS = [
|
||||||
|
'plan',
|
||||||
|
'edit',
|
||||||
|
'debug',
|
||||||
|
'test',
|
||||||
|
'deploy',
|
||||||
|
'evaluate',
|
||||||
|
'auth',
|
||||||
|
'research',
|
||||||
|
'knowledge',
|
||||||
|
'custom_tool',
|
||||||
|
'tour',
|
||||||
|
'info',
|
||||||
|
'workflow',
|
||||||
|
'superagent',
|
||||||
|
]
|
||||||
|
const isSubagentTool = SUBAGENT_TOOLS.includes(toolCall.name)
|
||||||
|
|
||||||
// For ALL subagent tools, don't show anything until we have blocks with content
|
// For ALL subagent tools, don't show anything until we have blocks with content
|
||||||
if (isSubagentTool) {
|
if (isSubagentTool) {
|
||||||
@@ -1489,18 +1593,17 @@ export function ToolCall({
|
|||||||
stateStr === 'aborted'
|
stateStr === 'aborted'
|
||||||
|
|
||||||
// Allow rendering if:
|
// Allow rendering if:
|
||||||
// 1. Tool is in TOOL_DISPLAY_REGISTRY (client tools), OR
|
// 1. Tool is in CLASS_TOOL_METADATA (client tools), OR
|
||||||
// 2. We're in build mode (integration tools are executed server-side), OR
|
// 2. We're in build mode (integration tools are executed server-side), OR
|
||||||
// 3. Tool call is already completed (historical - should always render)
|
// 3. Tool call is already completed (historical - should always render)
|
||||||
const isClientTool = !!TOOL_DISPLAY_REGISTRY[toolCall.name]
|
const isClientTool = !!CLASS_TOOL_METADATA[toolCall.name]
|
||||||
const isIntegrationToolInBuildMode = mode === 'build' && !isClientTool
|
const isIntegrationToolInBuildMode = mode === 'build' && !isClientTool
|
||||||
|
|
||||||
if (!isClientTool && !isIntegrationToolInBuildMode && !isCompletedToolCall) {
|
if (!isClientTool && !isIntegrationToolInBuildMode && !isCompletedToolCall) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const toolUIConfig = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig
|
|
||||||
// Check if tool has params table config (meaning it's expandable)
|
// Check if tool has params table config (meaning it's expandable)
|
||||||
const hasParamsTable = !!toolUIConfig?.paramsTable
|
const hasParamsTable = !!getToolUIConfig(toolCall.name)?.paramsTable
|
||||||
const isRunWorkflow = toolCall.name === 'run_workflow'
|
const isRunWorkflow = toolCall.name === 'run_workflow'
|
||||||
const isExpandableTool =
|
const isExpandableTool =
|
||||||
hasParamsTable ||
|
hasParamsTable ||
|
||||||
@@ -1510,6 +1613,7 @@ export function ToolCall({
|
|||||||
const showButtons = isCurrentMessage && shouldShowRunSkipButtons(toolCall)
|
const showButtons = isCurrentMessage && shouldShowRunSkipButtons(toolCall)
|
||||||
|
|
||||||
// Check UI config for secondary action - only show for current message tool calls
|
// Check UI config for secondary action - only show for current message tool calls
|
||||||
|
const toolUIConfig = getToolUIConfig(toolCall.name)
|
||||||
const secondaryAction = toolUIConfig?.secondaryAction
|
const secondaryAction = toolUIConfig?.secondaryAction
|
||||||
const showSecondaryAction = secondaryAction?.showInStates.includes(
|
const showSecondaryAction = secondaryAction?.showInStates.includes(
|
||||||
toolCall.state as ClientToolCallState
|
toolCall.state as ClientToolCallState
|
||||||
@@ -2107,9 +2211,16 @@ export function ToolCall({
|
|||||||
<div className='mt-[10px]'>
|
<div className='mt-[10px]'>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setToolCallState(toolCall, ClientToolCallState.background)
|
try {
|
||||||
onStateChange?.('background')
|
const instance = getClientTool(toolCall.id)
|
||||||
await sendToolDecision(toolCall.id, 'background')
|
instance?.setState?.((ClientToolCallState as any).background)
|
||||||
|
await instance?.markToolComplete?.(
|
||||||
|
200,
|
||||||
|
'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete'
|
||||||
|
)
|
||||||
|
forceUpdate({})
|
||||||
|
onStateChange?.('background')
|
||||||
|
} catch {}
|
||||||
}}
|
}}
|
||||||
variant='tertiary'
|
variant='tertiary'
|
||||||
title='Move to Background'
|
title='Move to Background'
|
||||||
@@ -2121,9 +2232,21 @@ export function ToolCall({
|
|||||||
<div className='mt-[10px]'>
|
<div className='mt-[10px]'>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setToolCallState(toolCall, ClientToolCallState.background)
|
try {
|
||||||
onStateChange?.('background')
|
const instance = getClientTool(toolCall.id)
|
||||||
await sendToolDecision(toolCall.id, 'background')
|
const elapsedSeconds = instance?.getElapsedSeconds?.() || 0
|
||||||
|
instance?.setState?.((ClientToolCallState as any).background, {
|
||||||
|
result: { _elapsedSeconds: elapsedSeconds },
|
||||||
|
})
|
||||||
|
const { updateToolCallParams } = useCopilotStore.getState()
|
||||||
|
updateToolCallParams?.(toolCall.id, { _elapsedSeconds: Math.round(elapsedSeconds) })
|
||||||
|
await instance?.markToolComplete?.(
|
||||||
|
200,
|
||||||
|
`User woke you up after ${Math.round(elapsedSeconds)} seconds`
|
||||||
|
)
|
||||||
|
forceUpdate({})
|
||||||
|
onStateChange?.('background')
|
||||||
|
} catch {}
|
||||||
}}
|
}}
|
||||||
variant='tertiary'
|
variant='tertiary'
|
||||||
title='Wake'
|
title='Wake'
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ export function getCommandDisplayLabel(commandId: string): string {
|
|||||||
* Model configuration options
|
* Model configuration options
|
||||||
*/
|
*/
|
||||||
export const MODEL_OPTIONS = [
|
export const MODEL_OPTIONS = [
|
||||||
{ value: 'claude-4.6-opus', label: 'Claude 4.6 Opus' },
|
|
||||||
{ value: 'claude-4.5-opus', label: 'Claude 4.5 Opus' },
|
{ value: 'claude-4.5-opus', label: 'Claude 4.5 Opus' },
|
||||||
{ value: 'claude-4.5-sonnet', label: 'Claude 4.5 Sonnet' },
|
{ value: 'claude-4.5-sonnet', label: 'Claude 4.5 Sonnet' },
|
||||||
{ value: 'claude-4.5-haiku', label: 'Claude 4.5 Haiku' },
|
{ value: 'claude-4.5-haiku', label: 'Claude 4.5 Haiku' },
|
||||||
|
|||||||
@@ -107,13 +107,13 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
|||||||
currentChat,
|
currentChat,
|
||||||
selectChat,
|
selectChat,
|
||||||
deleteChat,
|
deleteChat,
|
||||||
|
areChatsFresh,
|
||||||
workflowId: copilotWorkflowId,
|
workflowId: copilotWorkflowId,
|
||||||
setPlanTodos,
|
setPlanTodos,
|
||||||
closePlanTodos,
|
closePlanTodos,
|
||||||
clearPlanArtifact,
|
clearPlanArtifact,
|
||||||
savePlanArtifact,
|
savePlanArtifact,
|
||||||
loadAutoAllowedTools,
|
loadAutoAllowedTools,
|
||||||
resumeActiveStream,
|
|
||||||
} = useCopilotStore()
|
} = useCopilotStore()
|
||||||
|
|
||||||
// Initialize copilot
|
// Initialize copilot
|
||||||
@@ -126,7 +126,6 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
|||||||
loadAutoAllowedTools,
|
loadAutoAllowedTools,
|
||||||
currentChat,
|
currentChat,
|
||||||
isSendingMessage,
|
isSendingMessage,
|
||||||
resumeActiveStream,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle scroll management (80px stickiness for copilot)
|
// Handle scroll management (80px stickiness for copilot)
|
||||||
@@ -141,6 +140,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
|||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
copilotWorkflowId,
|
copilotWorkflowId,
|
||||||
loadChats,
|
loadChats,
|
||||||
|
areChatsFresh,
|
||||||
isSendingMessage,
|
isSendingMessage,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -421,8 +421,8 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show loading state until fully initialized, but skip if actively streaming (resume case) */}
|
{/* Show loading state until fully initialized */}
|
||||||
{!isInitialized && !isSendingMessage ? (
|
{!isInitialized ? (
|
||||||
<div className='flex h-full w-full items-center justify-center'>
|
<div className='flex h-full w-full items-center justify-center'>
|
||||||
<div className='flex flex-col items-center gap-3'>
|
<div className='flex flex-col items-center gap-3'>
|
||||||
<p className='text-muted-foreground text-sm'>Loading copilot</p>
|
<p className='text-muted-foreground text-sm'>Loading copilot</p>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface UseChatHistoryProps {
|
|||||||
activeWorkflowId: string | null
|
activeWorkflowId: string | null
|
||||||
copilotWorkflowId: string | null
|
copilotWorkflowId: string | null
|
||||||
loadChats: (forceRefresh: boolean) => Promise<void>
|
loadChats: (forceRefresh: boolean) => Promise<void>
|
||||||
|
areChatsFresh: (workflowId: string) => boolean
|
||||||
isSendingMessage: boolean
|
isSendingMessage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +21,8 @@ interface UseChatHistoryProps {
|
|||||||
* @returns Chat history utilities
|
* @returns Chat history utilities
|
||||||
*/
|
*/
|
||||||
export function useChatHistory(props: UseChatHistoryProps) {
|
export function useChatHistory(props: UseChatHistoryProps) {
|
||||||
const { chats, activeWorkflowId, copilotWorkflowId, loadChats, isSendingMessage } = props
|
const { chats, activeWorkflowId, copilotWorkflowId, loadChats, areChatsFresh, isSendingMessage } =
|
||||||
|
props
|
||||||
|
|
||||||
/** Groups chats by time period (Today, Yesterday, This Week, etc.) */
|
/** Groups chats by time period (Today, Yesterday, This Week, etc.) */
|
||||||
const groupedChats = useMemo(() => {
|
const groupedChats = useMemo(() => {
|
||||||
@@ -78,7 +80,7 @@ export function useChatHistory(props: UseChatHistoryProps) {
|
|||||||
/** Handles history dropdown opening and loads chats if needed (non-blocking) */
|
/** Handles history dropdown opening and loads chats if needed (non-blocking) */
|
||||||
const handleHistoryDropdownOpen = useCallback(
|
const handleHistoryDropdownOpen = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (open && activeWorkflowId && !isSendingMessage) {
|
if (open && activeWorkflowId && !isSendingMessage && !areChatsFresh(activeWorkflowId)) {
|
||||||
loadChats(false).catch((error) => {
|
loadChats(false).catch((error) => {
|
||||||
logger.error('Failed to load chat history:', error)
|
logger.error('Failed to load chat history:', error)
|
||||||
})
|
})
|
||||||
@@ -88,7 +90,7 @@ export function useChatHistory(props: UseChatHistoryProps) {
|
|||||||
logger.info('Chat history opened during stream - showing cached data only')
|
logger.info('Chat history opened during stream - showing cached data only')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeWorkflowId, isSendingMessage, loadChats]
|
[activeWorkflowId, areChatsFresh, isSendingMessage, loadChats]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ interface UseCopilotInitializationProps {
|
|||||||
loadAutoAllowedTools: () => Promise<void>
|
loadAutoAllowedTools: () => Promise<void>
|
||||||
currentChat: any
|
currentChat: any
|
||||||
isSendingMessage: boolean
|
isSendingMessage: boolean
|
||||||
resumeActiveStream: () => Promise<boolean>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,13 +32,11 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) {
|
|||||||
loadAutoAllowedTools,
|
loadAutoAllowedTools,
|
||||||
currentChat,
|
currentChat,
|
||||||
isSendingMessage,
|
isSendingMessage,
|
||||||
resumeActiveStream,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
const lastWorkflowIdRef = useRef<string | null>(null)
|
const lastWorkflowIdRef = useRef<string | null>(null)
|
||||||
const hasMountedRef = useRef(false)
|
const hasMountedRef = useRef(false)
|
||||||
const hasResumedRef = useRef(false)
|
|
||||||
|
|
||||||
/** Initialize on mount - loads chats if needed. Never loads during streaming */
|
/** Initialize on mount - loads chats if needed. Never loads during streaming */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -108,16 +105,6 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) {
|
|||||||
isSendingMessage,
|
isSendingMessage,
|
||||||
])
|
])
|
||||||
|
|
||||||
/** Try to resume active stream on mount - runs early, before waiting for chats */
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasResumedRef.current || isSendingMessage) return
|
|
||||||
hasResumedRef.current = true
|
|
||||||
// Resume immediately on mount - don't wait for isInitialized
|
|
||||||
resumeActiveStream().catch((err) => {
|
|
||||||
logger.warn('[Copilot] Failed to resume active stream', err)
|
|
||||||
})
|
|
||||||
}, [isSendingMessage, resumeActiveStream])
|
|
||||||
|
|
||||||
/** Load auto-allowed tools once on mount - runs immediately, independent of workflow */
|
/** Load auto-allowed tools once on mount - runs immediately, independent of workflow */
|
||||||
const hasLoadedAutoAllowedToolsRef = useRef(false)
|
const hasLoadedAutoAllowedToolsRef = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface CredentialSelectorProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: any | null
|
previewValue?: any | null
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CredentialSelector({
|
export function CredentialSelector({
|
||||||
@@ -43,6 +44,7 @@ export function CredentialSelector({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: CredentialSelectorProps) {
|
}: CredentialSelectorProps) {
|
||||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||||
const [editingValue, setEditingValue] = useState('')
|
const [editingValue, setEditingValue] = useState('')
|
||||||
@@ -67,7 +69,11 @@ export function CredentialSelector({
|
|||||||
canUseCredentialSets
|
canUseCredentialSets
|
||||||
)
|
)
|
||||||
|
|
||||||
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, {
|
||||||
|
disabled,
|
||||||
|
isPreview,
|
||||||
|
previewContextValues,
|
||||||
|
})
|
||||||
const hasDependencies = dependsOn.length > 0
|
const hasDependencies = dependsOn.length > 0
|
||||||
|
|
||||||
const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied)
|
const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Tooltip } from '@/components/emcn'
|
|||||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||||
|
|
||||||
@@ -33,7 +34,9 @@ export function DocumentSelector({
|
|||||||
previewContextValues,
|
previewContextValues,
|
||||||
})
|
})
|
||||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
const knowledgeBaseIdValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||||
|
: knowledgeBaseIdFromStore
|
||||||
const normalizedKnowledgeBaseId =
|
const normalizedKnowledgeBaseId =
|
||||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||||
? knowledgeBaseIdValue
|
? knowledgeBaseIdValue
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
|
|||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||||
@@ -77,7 +78,9 @@ export function DocumentTagEntry({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
const knowledgeBaseIdValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||||
|
: knowledgeBaseIdFromStore
|
||||||
const knowledgeBaseId =
|
const knowledgeBaseId =
|
||||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||||
? knowledgeBaseIdValue
|
? knowledgeBaseIdValue
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { isDependency } from '@/blocks/utils'
|
import { isDependency } from '@/blocks/utils'
|
||||||
@@ -62,42 +63,56 @@ export function FileSelectorInput({
|
|||||||
|
|
||||||
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
||||||
|
|
||||||
const connectedCredential = previewContextValues?.credential ?? blockValues.credential
|
const connectedCredential = previewContextValues
|
||||||
const domainValue = previewContextValues?.domain ?? domainValueFromStore
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: blockValues.credential
|
||||||
|
const domainValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.domain)
|
||||||
|
: domainValueFromStore
|
||||||
|
|
||||||
const teamIdValue = useMemo(
|
const teamIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.teamId ??
|
previewContextValues
|
||||||
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||||
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const siteIdValue = useMemo(
|
const siteIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.siteId ??
|
previewContextValues
|
||||||
resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.siteId)
|
||||||
[previewContextValues?.siteId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collectionIdValue = useMemo(
|
const collectionIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.collectionId ??
|
previewContextValues
|
||||||
resolveDependencyValue('collectionId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.collectionId)
|
||||||
[previewContextValues?.collectionId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue(
|
||||||
|
'collectionId',
|
||||||
|
blockValues,
|
||||||
|
canonicalIndex,
|
||||||
|
canonicalModeOverrides
|
||||||
|
),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const projectIdValue = useMemo(
|
const projectIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.projectId ??
|
previewContextValues
|
||||||
resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.projectId)
|
||||||
[previewContextValues?.projectId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const planIdValue = useMemo(
|
const planIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.planId ??
|
previewContextValues
|
||||||
resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.planId)
|
||||||
[previewContextValues?.planId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const normalizedCredentialId =
|
const normalizedCredentialId =
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||||
@@ -17,6 +18,7 @@ interface FolderSelectorInputProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: any | null
|
previewValue?: any | null
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FolderSelectorInput({
|
export function FolderSelectorInput({
|
||||||
@@ -25,9 +27,13 @@ export function FolderSelectorInput({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: FolderSelectorInputProps) {
|
}: FolderSelectorInputProps) {
|
||||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
const [credentialFromStore] = useSubBlockValue(blockId, 'credential')
|
||||||
|
const connectedCredential = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: credentialFromStore
|
||||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||||
const { activeWorkflowId } = useWorkflowRegistry()
|
const { activeWorkflowId } = useWorkflowRegistry()
|
||||||
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
|
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
|
||||||
@@ -47,7 +53,11 @@ export function FolderSelectorInput({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Central dependsOn gating
|
// Central dependsOn gating
|
||||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||||
|
disabled,
|
||||||
|
isPreview,
|
||||||
|
previewContextValues,
|
||||||
|
})
|
||||||
|
|
||||||
// Get the current value from the store or prop value if in preview mode
|
// Get the current value from the store or prop value if in preview mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
|
|||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import { useWorkflowState } from '@/hooks/queries/workflows'
|
import { useWorkflowState } from '@/hooks/queries/workflows'
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ interface InputMappingProps {
|
|||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: Record<string, unknown>
|
previewValue?: Record<string, unknown>
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
/** Sub-block values from the preview context for resolving sibling sub-block values */
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,9 +53,13 @@ export function InputMapping({
|
|||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
previewContextValues,
|
||||||
}: InputMappingProps) {
|
}: InputMappingProps) {
|
||||||
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
||||||
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
const [storeWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
||||||
|
const selectedWorkflowId = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.workflowId)
|
||||||
|
: storeWorkflowId
|
||||||
|
|
||||||
const inputController = useSubBlockInput({
|
const inputController = useSubBlockInput({
|
||||||
blockId,
|
blockId,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/
|
|||||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||||
@@ -69,7 +70,9 @@ export function KnowledgeTagFilters({
|
|||||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||||
|
|
||||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
const knowledgeBaseIdValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||||
|
: knowledgeBaseIdFromStore
|
||||||
const knowledgeBaseId =
|
const knowledgeBaseId =
|
||||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||||
? knowledgeBaseIdValue
|
? knowledgeBaseIdValue
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input'
|
import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input'
|
||||||
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input'
|
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
||||||
import { formatParameterLabel } from '@/tools/params'
|
import { formatParameterLabel } from '@/tools/params'
|
||||||
@@ -18,6 +19,7 @@ interface McpDynamicArgsProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: any
|
previewValue?: any
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,12 +49,19 @@ export function McpDynamicArgs({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: McpDynamicArgsProps) {
|
}: McpDynamicArgsProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = params.workspaceId as string
|
const workspaceId = params.workspaceId as string
|
||||||
const { mcpTools, isLoading } = useMcpTools(workspaceId)
|
const { mcpTools, isLoading } = useMcpTools(workspaceId)
|
||||||
const [selectedTool] = useSubBlockValue(blockId, 'tool')
|
const [toolFromStore] = useSubBlockValue(blockId, 'tool')
|
||||||
const [cachedSchema] = useSubBlockValue(blockId, '_toolSchema')
|
const selectedTool = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.tool)
|
||||||
|
: toolFromStore
|
||||||
|
const [schemaFromStore] = useSubBlockValue(blockId, '_toolSchema')
|
||||||
|
const cachedSchema = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues._toolSchema)
|
||||||
|
: schemaFromStore
|
||||||
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
|
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
|
||||||
|
|
||||||
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
|
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { Combobox } from '@/components/emcn/components'
|
import { Combobox } from '@/components/emcn/components'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ interface McpToolSelectorProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: string | null
|
previewValue?: string | null
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function McpToolSelector({
|
export function McpToolSelector({
|
||||||
@@ -21,6 +23,7 @@ export function McpToolSelector({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: McpToolSelectorProps) {
|
}: McpToolSelectorProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = params.workspaceId as string
|
const workspaceId = params.workspaceId as string
|
||||||
@@ -31,7 +34,10 @@ export function McpToolSelector({
|
|||||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||||
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
|
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
|
||||||
|
|
||||||
const [serverValue] = useSubBlockValue(blockId, 'server')
|
const [serverFromStore] = useSubBlockValue(blockId, 'server')
|
||||||
|
const serverValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.server)
|
||||||
|
: serverFromStore
|
||||||
|
|
||||||
const label = subBlock.placeholder || 'Select tool'
|
const label = subBlock.placeholder || 'Select tool'
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||||
@@ -55,14 +56,19 @@ export function ProjectSelectorInput({
|
|||||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const connectedCredential = previewContextValues?.credential ?? blockValues.credential
|
const connectedCredential = previewContextValues
|
||||||
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: blockValues.credential
|
||||||
|
const jiraDomain = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.domain)
|
||||||
|
: jiraDomainFromStore
|
||||||
|
|
||||||
const linearTeamId = useMemo(
|
const linearTeamId = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.teamId ??
|
previewContextValues
|
||||||
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||||
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceId = subBlock.serviceId || ''
|
const serviceId = subBlock.serviceId || ''
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/sub
|
|||||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
||||||
@@ -66,9 +67,12 @@ export function SheetSelectorInput({
|
|||||||
[blockValues, canonicalIndex, canonicalModeOverrides]
|
[blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
|
const connectedCredential = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: connectedCredentialFromStore
|
||||||
const spreadsheetId = previewContextValues
|
const spreadsheetId = previewContextValues
|
||||||
? (previewContextValues.spreadsheetId ?? previewContextValues.manualSpreadsheetId)
|
? (resolvePreviewContextValue(previewContextValues.spreadsheetId) ??
|
||||||
|
resolvePreviewContextValue(previewContextValues.manualSpreadsheetId))
|
||||||
: spreadsheetIdFromStore
|
: spreadsheetIdFromStore
|
||||||
|
|
||||||
const normalizedCredentialId =
|
const normalizedCredentialId =
|
||||||
|
|||||||
@@ -130,39 +130,52 @@ export function SkillInput({
|
|||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedSkills.length > 0 && (
|
{selectedSkills.length > 0 &&
|
||||||
<div className='flex flex-wrap gap-[4px]'>
|
selectedSkills.map((stored) => {
|
||||||
{selectedSkills.map((stored) => {
|
const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId)
|
||||||
const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId)
|
return (
|
||||||
return (
|
<div
|
||||||
|
key={stored.skillId}
|
||||||
|
className='group relative flex flex-col overflow-hidden rounded-[4px] border border-[var(--border-1)] transition-all duration-200 ease-in-out'
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={stored.skillId}
|
className='flex cursor-pointer items-center justify-between gap-[8px] rounded-t-[4px] bg-[var(--surface-4)] px-[8px] py-[6.5px]'
|
||||||
className='flex cursor-pointer items-center gap-[4px] rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[2px] font-medium text-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-6)]'
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (fullSkill && !disabled && !isPreview) {
|
if (fullSkill && !disabled && !isPreview) {
|
||||||
setEditingSkill(fullSkill)
|
setEditingSkill(fullSkill)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AgentSkillsIcon className='h-[10px] w-[10px] text-[var(--text-tertiary)]' />
|
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||||
<span className='max-w-[140px] truncate'>{resolveSkillName(stored)}</span>
|
<div
|
||||||
{!disabled && !isPreview && (
|
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||||
<button
|
style={{ backgroundColor: '#e0e0e0' }}
|
||||||
type='button'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleRemove(stored.skillId)
|
|
||||||
}}
|
|
||||||
className='ml-[2px] rounded-[2px] p-[1px] text-[var(--text-tertiary)] hover:bg-[var(--surface-7)] hover:text-[var(--text-secondary)]'
|
|
||||||
>
|
>
|
||||||
<XIcon className='h-[10px] w-[10px]' />
|
<AgentSkillsIcon className='h-[10px] w-[10px] text-[#333]' />
|
||||||
</button>
|
</div>
|
||||||
)}
|
<span className='truncate font-medium text-[13px] text-[var(--text-primary)]'>
|
||||||
|
{resolveSkillName(stored)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||||
|
{!disabled && !isPreview && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleRemove(stored.skillId)
|
||||||
|
}}
|
||||||
|
className='flex items-center justify-center text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
|
||||||
|
aria-label='Remove skill'
|
||||||
|
>
|
||||||
|
<XIcon className='h-[13px] w-[13px]' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
})}
|
)
|
||||||
</div>
|
})}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SkillModal
|
<SkillModal
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||||
|
|
||||||
@@ -58,9 +59,15 @@ export function SlackSelectorInput({
|
|||||||
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
||||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||||
|
|
||||||
const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod
|
const effectiveAuthMethod = previewContextValues
|
||||||
const effectiveBotToken = previewContextValues?.botToken ?? botToken
|
? resolvePreviewContextValue(previewContextValues.authMethod)
|
||||||
const effectiveCredential = previewContextValues?.credential ?? connectedCredential
|
: authMethod
|
||||||
|
const effectiveBotToken = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.botToken)
|
||||||
|
: botToken
|
||||||
|
const effectiveCredential = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: connectedCredential
|
||||||
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
||||||
|
|
||||||
const serviceId = subBlock.serviceId || ''
|
const serviceId = subBlock.serviceId || ''
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ function FolderSelectorSyncWrapper({
|
|||||||
dependsOn: uiComponent.dependsOn,
|
dependsOn: uiComponent.dependsOn,
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
/>
|
/>
|
||||||
</GenericSyncWrapper>
|
</GenericSyncWrapper>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -797,6 +797,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -832,6 +833,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -843,6 +845,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -865,6 +868,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -876,6 +880,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -887,6 +892,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -911,6 +917,7 @@ function SubBlockComponent({
|
|||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -946,6 +953,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -979,6 +987,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -990,6 +999,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Extracts the raw value from a preview context entry.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* In the sub-block preview context, values are wrapped as `{ value: T }` objects
|
||||||
|
* (the full sub-block state). In the tool-input preview context, values are already
|
||||||
|
* raw. This function normalizes both cases to return the underlying value.
|
||||||
|
*
|
||||||
|
* @param raw - The preview context entry, which may be a raw value or a `{ value: T }` wrapper
|
||||||
|
* @returns The unwrapped value, or `null` if the input is nullish
|
||||||
|
*/
|
||||||
|
export function resolvePreviewContextValue(raw: unknown): unknown {
|
||||||
|
if (raw === null || raw === undefined) return null
|
||||||
|
if (typeof raw === 'object' && !Array.isArray(raw) && 'value' in raw) {
|
||||||
|
return (raw as Record<string, unknown>).value ?? null
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
isSubBlockVisibleForMode,
|
isSubBlockVisibleForMode,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
|
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
|
||||||
|
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
@@ -35,6 +36,7 @@ export function useEditorSubblockLayout(
|
|||||||
const blockDataFromStore = useWorkflowStore(
|
const blockDataFromStore = useWorkflowStore(
|
||||||
useCallback((state) => state.blocks?.[blockId]?.data, [blockId])
|
useCallback((state) => state.blocks?.[blockId]?.data, [blockId])
|
||||||
)
|
)
|
||||||
|
const { config: permissionConfig } = usePermissionConfig()
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Guard against missing config or block selection
|
// Guard against missing config or block selection
|
||||||
@@ -100,6 +102,9 @@ export function useEditorSubblockLayout(
|
|||||||
const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
|
const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
|
||||||
if (block.hidden) return false
|
if (block.hidden) return false
|
||||||
|
|
||||||
|
// Hide skill-input subblock when skills are disabled via permissions
|
||||||
|
if (block.type === 'skill-input' && permissionConfig.disableSkills) return false
|
||||||
|
|
||||||
// Check required feature if specified - declarative feature gating
|
// Check required feature if specified - declarative feature gating
|
||||||
if (!isSubBlockFeatureEnabled(block)) return false
|
if (!isSubBlockFeatureEnabled(block)) return false
|
||||||
|
|
||||||
@@ -149,5 +154,6 @@ export function useEditorSubblockLayout(
|
|||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
isSnapshotView,
|
isSnapshotView,
|
||||||
blockDataFromStore,
|
blockDataFromStore,
|
||||||
|
permissionConfig.disableSkills,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { useCustomTools } from '@/hooks/queries/custom-tools'
|
|||||||
import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp'
|
import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp'
|
||||||
import { useCredentialName } from '@/hooks/queries/oauth-credentials'
|
import { useCredentialName } from '@/hooks/queries/oauth-credentials'
|
||||||
import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedules'
|
import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedules'
|
||||||
|
import { useSkills } from '@/hooks/queries/skills'
|
||||||
import { useDeployChildWorkflow } from '@/hooks/queries/workflows'
|
import { useDeployChildWorkflow } from '@/hooks/queries/workflows'
|
||||||
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
|
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/panel'
|
||||||
@@ -618,6 +619,48 @@ const SubBlockRow = memo(function SubBlockRow({
|
|||||||
return `${toolNames[0]}, ${toolNames[1]} +${toolNames.length - 2}`
|
return `${toolNames[0]}, ${toolNames[1]} +${toolNames.length - 2}`
|
||||||
}, [subBlock?.type, rawValue, customTools, workspaceId])
|
}, [subBlock?.type, rawValue, customTools, workspaceId])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrates skill references to display names.
|
||||||
|
* Resolves skill IDs to their current names from the skills query.
|
||||||
|
*/
|
||||||
|
const { data: workspaceSkills = [] } = useSkills(workspaceId || '')
|
||||||
|
|
||||||
|
const skillsDisplayValue = useMemo(() => {
|
||||||
|
if (subBlock?.type !== 'skill-input' || !Array.isArray(rawValue) || rawValue.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoredSkill {
|
||||||
|
skillId: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillNames = rawValue
|
||||||
|
.map((skill: StoredSkill) => {
|
||||||
|
if (!skill || typeof skill !== 'object') return null
|
||||||
|
|
||||||
|
// Priority 1: Resolve skill name from the skills query (fresh data)
|
||||||
|
if (skill.skillId) {
|
||||||
|
const foundSkill = workspaceSkills.find((s) => s.id === skill.skillId)
|
||||||
|
if (foundSkill?.name) return foundSkill.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Fall back to stored name (for deleted skills)
|
||||||
|
if (skill.name && typeof skill.name === 'string') return skill.name
|
||||||
|
|
||||||
|
// Priority 3: Use skillId as last resort
|
||||||
|
if (skill.skillId) return skill.skillId
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter((name): name is string => !!name)
|
||||||
|
|
||||||
|
if (skillNames.length === 0) return null
|
||||||
|
if (skillNames.length === 1) return skillNames[0]
|
||||||
|
if (skillNames.length === 2) return `${skillNames[0]}, ${skillNames[1]}`
|
||||||
|
return `${skillNames[0]}, ${skillNames[1]} +${skillNames.length - 2}`
|
||||||
|
}, [subBlock?.type, rawValue, workspaceSkills])
|
||||||
|
|
||||||
const isPasswordField = subBlock?.password === true
|
const isPasswordField = subBlock?.password === true
|
||||||
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
|
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
|
||||||
|
|
||||||
@@ -627,6 +670,7 @@ const SubBlockRow = memo(function SubBlockRow({
|
|||||||
dropdownLabel ||
|
dropdownLabel ||
|
||||||
variablesDisplayValue ||
|
variablesDisplayValue ||
|
||||||
toolsDisplayValue ||
|
toolsDisplayValue ||
|
||||||
|
skillsDisplayValue ||
|
||||||
knowledgeBaseDisplayName ||
|
knowledgeBaseDisplayName ||
|
||||||
workflowSelectionName ||
|
workflowSelectionName ||
|
||||||
mcpServerDisplayName ||
|
mcpServerDisplayName ||
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import 'reactflow/dist/style.css'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useSession } from '@/lib/auth/auth-client'
|
import { useSession } from '@/lib/auth/auth-client'
|
||||||
import type { OAuthConnectEventDetail } from '@/lib/copilot/tools/client/base-tool'
|
import type { OAuthConnectEventDetail } from '@/lib/copilot/tools/client/other/oauth-request-access'
|
||||||
import type { OAuthProvider } from '@/lib/oauth'
|
import type { OAuthProvider } from '@/lib/oauth'
|
||||||
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
|
|||||||
@@ -784,8 +784,12 @@ function PreviewEditorContent({
|
|||||||
? childWorkflowSnapshotState
|
? childWorkflowSnapshotState
|
||||||
: childWorkflowState
|
: childWorkflowState
|
||||||
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
|
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
|
||||||
|
const isBlockNotExecuted = isExecutionMode && !executionData
|
||||||
const isMissingChildWorkflow =
|
const isMissingChildWorkflow =
|
||||||
Boolean(childWorkflowId) && !resolvedIsLoadingChildWorkflow && !resolvedChildWorkflowState
|
Boolean(childWorkflowId) &&
|
||||||
|
!isBlockNotExecuted &&
|
||||||
|
!resolvedIsLoadingChildWorkflow &&
|
||||||
|
!resolvedChildWorkflowState
|
||||||
|
|
||||||
/** Drills down into the child workflow or opens it in a new tab */
|
/** Drills down into the child workflow or opens it in a new tab */
|
||||||
const handleExpandChildWorkflow = useCallback(() => {
|
const handleExpandChildWorkflow = useCallback(() => {
|
||||||
@@ -1192,7 +1196,7 @@ function PreviewEditorContent({
|
|||||||
<div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'>
|
<div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'>
|
||||||
<div className='flex-1 overflow-y-auto overflow-x-hidden'>
|
<div className='flex-1 overflow-y-auto overflow-x-hidden'>
|
||||||
{/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
|
{/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
|
||||||
{isExecutionMode && !executionData && (
|
{isBlockNotExecuted && (
|
||||||
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden border-[var(--border)] border-b px-[12px] py-[10px]'>
|
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden border-[var(--border)] border-b px-[12px] py-[10px]'>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<Badge variant='gray-secondary' size='sm' dot>
|
<Badge variant='gray-secondary' size='sm' dot>
|
||||||
@@ -1419,9 +1423,11 @@ function PreviewEditorContent({
|
|||||||
) : (
|
) : (
|
||||||
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
||||||
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
||||||
{isMissingChildWorkflow
|
{isBlockNotExecuted
|
||||||
? DELETED_WORKFLOW_LABEL
|
? 'Not Executed'
|
||||||
: 'Unable to load preview'}
|
: isMissingChildWorkflow
|
||||||
|
? DELETED_WORKFLOW_LABEL
|
||||||
|
: 'Unable to load preview'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ interface SkillModalProps {
|
|||||||
|
|
||||||
const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
||||||
|
|
||||||
|
interface FieldErrors {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
content?: string
|
||||||
|
general?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function SkillModal({
|
export function SkillModal({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@@ -43,7 +50,7 @@ export function SkillModal({
|
|||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
const [formError, setFormError] = useState('')
|
const [errors, setErrors] = useState<FieldErrors>({})
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,7 +64,7 @@ export function SkillModal({
|
|||||||
setDescription('')
|
setDescription('')
|
||||||
setContent('')
|
setContent('')
|
||||||
}
|
}
|
||||||
setFormError('')
|
setErrors({})
|
||||||
}
|
}
|
||||||
}, [open, initialValues])
|
}, [open, initialValues])
|
||||||
|
|
||||||
@@ -71,24 +78,26 @@ export function SkillModal({
|
|||||||
}, [name, description, content, initialValues])
|
}, [name, description, content, initialValues])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
const newErrors: FieldErrors = {}
|
||||||
|
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
setFormError('Name is required')
|
newErrors.name = 'Name is required'
|
||||||
return
|
} else if (name.length > 64) {
|
||||||
}
|
newErrors.name = 'Name must be 64 characters or less'
|
||||||
if (name.length > 64) {
|
} else if (!KEBAB_CASE_REGEX.test(name)) {
|
||||||
setFormError('Name must be 64 characters or less')
|
newErrors.name = 'Name must be kebab-case (e.g. my-skill)'
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!KEBAB_CASE_REGEX.test(name)) {
|
|
||||||
setFormError('Name must be kebab-case (e.g. my-skill)')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description.trim()) {
|
if (!description.trim()) {
|
||||||
setFormError('Description is required')
|
newErrors.description = 'Description is required'
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
setFormError('Content is required')
|
newErrors.content = 'Content is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) {
|
||||||
|
setErrors(newErrors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +122,7 @@ export function SkillModal({
|
|||||||
error instanceof Error && error.message.includes('already exists')
|
error instanceof Error && error.message.includes('already exists')
|
||||||
? error.message
|
? error.message
|
||||||
: 'Failed to save skill. Please try again.'
|
: 'Failed to save skill. Please try again.'
|
||||||
setFormError(message)
|
setErrors({ general: message })
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -135,12 +144,17 @@ export function SkillModal({
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setName(e.target.value)
|
setName(e.target.value)
|
||||||
if (formError) setFormError('')
|
if (errors.name || errors.general)
|
||||||
|
setErrors((prev) => ({ ...prev, name: undefined, general: undefined }))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className='text-[11px] text-[var(--text-muted)]'>
|
{errors.name ? (
|
||||||
Lowercase letters, numbers, and hyphens (e.g. my-skill)
|
<p className='text-[12px] text-[var(--text-error)]'>{errors.name}</p>
|
||||||
</span>
|
) : (
|
||||||
|
<span className='text-[11px] text-[var(--text-muted)]'>
|
||||||
|
Lowercase letters, numbers, and hyphens (e.g. my-skill)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-[4px]'>
|
<div className='flex flex-col gap-[4px]'>
|
||||||
@@ -153,10 +167,14 @@ export function SkillModal({
|
|||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setDescription(e.target.value)
|
setDescription(e.target.value)
|
||||||
if (formError) setFormError('')
|
if (errors.description || errors.general)
|
||||||
|
setErrors((prev) => ({ ...prev, description: undefined, general: undefined }))
|
||||||
}}
|
}}
|
||||||
maxLength={1024}
|
maxLength={1024}
|
||||||
/>
|
/>
|
||||||
|
{errors.description && (
|
||||||
|
<p className='text-[12px] text-[var(--text-error)]'>{errors.description}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-[4px]'>
|
<div className='flex flex-col gap-[4px]'>
|
||||||
@@ -169,13 +187,19 @@ export function SkillModal({
|
|||||||
value={content}
|
value={content}
|
||||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setContent(e.target.value)
|
setContent(e.target.value)
|
||||||
if (formError) setFormError('')
|
if (errors.content || errors.general)
|
||||||
|
setErrors((prev) => ({ ...prev, content: undefined, general: undefined }))
|
||||||
}}
|
}}
|
||||||
className='min-h-[200px] resize-y font-mono text-[13px]'
|
className='min-h-[200px] resize-y font-mono text-[13px]'
|
||||||
/>
|
/>
|
||||||
|
{errors.content && (
|
||||||
|
<p className='text-[12px] text-[var(--text-error)]'>{errors.content}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formError && <span className='text-[11px] text-[var(--text-error)]'>{formError}</span>}
|
{errors.general && (
|
||||||
|
<p className='text-[12px] text-[var(--text-error)]'>{errors.general}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className='items-center justify-between'>
|
<ModalFooter className='items-center justify-between'>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { AgentIcon } from '@/components/icons'
|
import { AgentIcon } from '@/components/icons'
|
||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { AuthMode } from '@/blocks/types'
|
import { AuthMode } from '@/blocks/types'
|
||||||
|
import { getApiKeyCondition } from '@/blocks/utils'
|
||||||
import {
|
import {
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
getHostedModels,
|
|
||||||
getMaxTemperature,
|
getMaxTemperature,
|
||||||
getProviderIcon,
|
getProviderIcon,
|
||||||
getReasoningEffortValuesForModel,
|
getReasoningEffortValuesForModel,
|
||||||
@@ -17,15 +16,6 @@ import {
|
|||||||
providers,
|
providers,
|
||||||
supportsTemperature,
|
supportsTemperature,
|
||||||
} from '@/providers/utils'
|
} from '@/providers/utils'
|
||||||
|
|
||||||
const getCurrentOllamaModels = () => {
|
|
||||||
return useProvidersStore.getState().providers.ollama.models
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCurrentVLLMModels = () => {
|
|
||||||
return useProvidersStore.getState().providers.vllm.models
|
|
||||||
}
|
|
||||||
|
|
||||||
import { useProvidersStore } from '@/stores/providers'
|
import { useProvidersStore } from '@/stores/providers'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
@@ -164,6 +154,7 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
placeholder: 'Select reasoning effort...',
|
placeholder: 'Select reasoning effort...',
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'auto', id: 'auto' },
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -173,9 +164,12 @@ Return ONLY the JSON array.`,
|
|||||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||||
|
|
||||||
|
const autoOption = { label: 'auto', id: 'auto' }
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (!activeWorkflowId) {
|
if (!activeWorkflowId) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -188,6 +182,7 @@ Return ONLY the JSON array.`,
|
|||||||
|
|
||||||
if (!modelValue) {
|
if (!modelValue) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -197,15 +192,16 @@ Return ONLY the JSON array.`,
|
|||||||
const validOptions = getReasoningEffortValuesForModel(modelValue)
|
const validOptions = getReasoningEffortValuesForModel(modelValue)
|
||||||
if (!validOptions) {
|
if (!validOptions) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||||
},
|
},
|
||||||
value: () => 'medium',
|
mode: 'advanced',
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: MODELS_WITH_REASONING_EFFORT,
|
value: MODELS_WITH_REASONING_EFFORT,
|
||||||
@@ -217,6 +213,7 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
placeholder: 'Select verbosity...',
|
placeholder: 'Select verbosity...',
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'auto', id: 'auto' },
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -226,9 +223,12 @@ Return ONLY the JSON array.`,
|
|||||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||||
|
|
||||||
|
const autoOption = { label: 'auto', id: 'auto' }
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (!activeWorkflowId) {
|
if (!activeWorkflowId) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -241,6 +241,7 @@ Return ONLY the JSON array.`,
|
|||||||
|
|
||||||
if (!modelValue) {
|
if (!modelValue) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -250,15 +251,16 @@ Return ONLY the JSON array.`,
|
|||||||
const validOptions = getVerbosityValuesForModel(modelValue)
|
const validOptions = getVerbosityValuesForModel(modelValue)
|
||||||
if (!validOptions) {
|
if (!validOptions) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||||
},
|
},
|
||||||
value: () => 'medium',
|
mode: 'advanced',
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: MODELS_WITH_VERBOSITY,
|
value: MODELS_WITH_VERBOSITY,
|
||||||
@@ -270,6 +272,7 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
placeholder: 'Select thinking level...',
|
placeholder: 'Select thinking level...',
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'none', id: 'none' },
|
||||||
{ label: 'minimal', id: 'minimal' },
|
{ label: 'minimal', id: 'minimal' },
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
@@ -281,12 +284,11 @@ Return ONLY the JSON array.`,
|
|||||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||||
|
|
||||||
|
const noneOption = { label: 'none', id: 'none' }
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (!activeWorkflowId) {
|
if (!activeWorkflowId) {
|
||||||
return [
|
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||||
{ label: 'low', id: 'low' },
|
|
||||||
{ label: 'high', id: 'high' },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
|
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
|
||||||
@@ -294,23 +296,17 @@ Return ONLY the JSON array.`,
|
|||||||
const modelValue = blockValues?.model as string
|
const modelValue = blockValues?.model as string
|
||||||
|
|
||||||
if (!modelValue) {
|
if (!modelValue) {
|
||||||
return [
|
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||||
{ label: 'low', id: 'low' },
|
|
||||||
{ label: 'high', id: 'high' },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validOptions = getThinkingLevelsForModel(modelValue)
|
const validOptions = getThinkingLevelsForModel(modelValue)
|
||||||
if (!validOptions) {
|
if (!validOptions) {
|
||||||
return [
|
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||||
{ label: 'low', id: 'low' },
|
|
||||||
{ label: 'high', id: 'high' },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
return [noneOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||||
},
|
},
|
||||||
value: () => 'high',
|
mode: 'advanced',
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: MODELS_WITH_THINKING,
|
value: MODELS_WITH_THINKING,
|
||||||
@@ -333,11 +329,11 @@ Return ONLY the JSON array.`,
|
|||||||
id: 'azureApiVersion',
|
id: 'azureApiVersion',
|
||||||
title: 'Azure API Version',
|
title: 'Azure API Version',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
placeholder: '2024-07-01-preview',
|
placeholder: 'Enter API version',
|
||||||
connectionDroppable: false,
|
connectionDroppable: false,
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: providers['azure-openai'].models,
|
value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -401,6 +397,16 @@ Return ONLY the JSON array.`,
|
|||||||
value: providers.bedrock.models,
|
value: providers.bedrock.models,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'apiKey',
|
||||||
|
title: 'API Key',
|
||||||
|
type: 'short-input',
|
||||||
|
placeholder: 'Enter your API key',
|
||||||
|
password: true,
|
||||||
|
connectionDroppable: false,
|
||||||
|
required: true,
|
||||||
|
condition: getApiKeyCondition(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tools',
|
id: 'tools',
|
||||||
title: 'Tools',
|
title: 'Tools',
|
||||||
@@ -413,32 +419,6 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'skill-input',
|
type: 'skill-input',
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'apiKey',
|
|
||||||
title: 'API Key',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Enter your API key',
|
|
||||||
password: true,
|
|
||||||
connectionDroppable: false,
|
|
||||||
required: true,
|
|
||||||
// Hide API key for hosted models, Ollama models, vLLM models, Vertex models (uses OAuth), and Bedrock (uses AWS credentials)
|
|
||||||
condition: isHosted
|
|
||||||
? {
|
|
||||||
field: 'model',
|
|
||||||
value: [...getHostedModels(), ...providers.vertex.models, ...providers.bedrock.models],
|
|
||||||
not: true, // Show for all models EXCEPT those listed
|
|
||||||
}
|
|
||||||
: () => ({
|
|
||||||
field: 'model',
|
|
||||||
value: [
|
|
||||||
...getCurrentOllamaModels(),
|
|
||||||
...getCurrentVLLMModels(),
|
|
||||||
...providers.vertex.models,
|
|
||||||
...providers.bedrock.models,
|
|
||||||
],
|
|
||||||
not: true, // Show for all models EXCEPT Ollama, vLLM, Vertex, and Bedrock models
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'memoryType',
|
id: 'memoryType',
|
||||||
title: 'Memory',
|
title: 'Memory',
|
||||||
@@ -493,6 +473,7 @@ Return ONLY the JSON array.`,
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
defaultValue: 0.3,
|
defaultValue: 0.3,
|
||||||
|
mode: 'advanced',
|
||||||
condition: () => ({
|
condition: () => ({
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: (() => {
|
value: (() => {
|
||||||
@@ -510,6 +491,7 @@ Return ONLY the JSON array.`,
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 2,
|
max: 2,
|
||||||
defaultValue: 0.3,
|
defaultValue: 0.3,
|
||||||
|
mode: 'advanced',
|
||||||
condition: () => ({
|
condition: () => ({
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: (() => {
|
value: (() => {
|
||||||
@@ -525,6 +507,7 @@ Return ONLY the JSON array.`,
|
|||||||
title: 'Max Output Tokens',
|
title: 'Max Output Tokens',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
placeholder: 'Enter max tokens (e.g., 4096)...',
|
placeholder: 'Enter max tokens (e.g., 4096)...',
|
||||||
|
mode: 'advanced',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'responseFormat',
|
id: 'responseFormat',
|
||||||
@@ -715,7 +698,7 @@ Example 3 (Array Input):
|
|||||||
},
|
},
|
||||||
model: { type: 'string', description: 'AI model to use' },
|
model: { type: 'string', description: 'AI model to use' },
|
||||||
apiKey: { type: 'string', description: 'Provider API key' },
|
apiKey: { type: 'string', description: 'Provider API key' },
|
||||||
azureEndpoint: { type: 'string', description: 'Azure OpenAI endpoint URL' },
|
azureEndpoint: { type: 'string', description: 'Azure endpoint URL' },
|
||||||
azureApiVersion: { type: 'string', description: 'Azure API version' },
|
azureApiVersion: { type: 'string', description: 'Azure API version' },
|
||||||
vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' },
|
vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' },
|
||||||
vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' },
|
vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' },
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ export const TranslateBlock: BlockConfig = {
|
|||||||
vertexProject: params.vertexProject,
|
vertexProject: params.vertexProject,
|
||||||
vertexLocation: params.vertexLocation,
|
vertexLocation: params.vertexLocation,
|
||||||
vertexCredential: params.vertexCredential,
|
vertexCredential: params.vertexCredential,
|
||||||
bedrockRegion: params.bedrockRegion,
|
bedrockAccessKeyId: params.bedrockAccessKeyId,
|
||||||
bedrockSecretKey: params.bedrockSecretKey,
|
bedrockSecretKey: params.bedrockSecretKey,
|
||||||
|
bedrockRegion: params.bedrockRegion,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export interface SubBlockConfig {
|
|||||||
not?: boolean
|
not?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (() => {
|
| ((values?: Record<string, unknown>) => {
|
||||||
field: string
|
field: string
|
||||||
value: string | number | boolean | Array<string | number | boolean>
|
value: string | number | boolean | Array<string | number | boolean>
|
||||||
not?: boolean
|
not?: boolean
|
||||||
@@ -261,7 +261,7 @@ export interface SubBlockConfig {
|
|||||||
not?: boolean
|
not?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (() => {
|
| ((values?: Record<string, unknown>) => {
|
||||||
field: string
|
field: string
|
||||||
value: string | number | boolean | Array<string | number | boolean>
|
value: string | number | boolean | Array<string | number | boolean>
|
||||||
not?: boolean
|
not?: boolean
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
||||||
import { getHostedModels, providers } from '@/providers/utils'
|
import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils'
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,11 +48,54 @@ const getCurrentOllamaModels = () => {
|
|||||||
return useProvidersStore.getState().providers.ollama.models
|
return useProvidersStore.getState().providers.ollama.models
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function buildModelVisibilityCondition(model: string, shouldShow: boolean) {
|
||||||
* Helper to get current vLLM models from store
|
if (!model) {
|
||||||
*/
|
return { field: 'model', value: '__no_model_selected__' }
|
||||||
const getCurrentVLLMModels = () => {
|
}
|
||||||
return useProvidersStore.getState().providers.vllm.models
|
|
||||||
|
return shouldShow ? { field: 'model', value: model } : { field: 'model', value: model, not: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRequireApiKeyForModel(model: string): boolean {
|
||||||
|
const normalizedModel = model.trim().toLowerCase()
|
||||||
|
if (!normalizedModel) return false
|
||||||
|
|
||||||
|
const hostedModels = getHostedModels()
|
||||||
|
const isHostedModel = hostedModels.some(
|
||||||
|
(hostedModel) => hostedModel.toLowerCase() === normalizedModel
|
||||||
|
)
|
||||||
|
if (isHosted && isHostedModel) return false
|
||||||
|
|
||||||
|
if (normalizedModel.startsWith('vertex/') || normalizedModel.startsWith('bedrock/')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedModel.startsWith('vllm/')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentOllamaModels = getCurrentOllamaModels()
|
||||||
|
if (currentOllamaModels.some((ollamaModel) => ollamaModel.toLowerCase() === normalizedModel)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHosted) {
|
||||||
|
try {
|
||||||
|
const providerId = getProviderFromModel(model)
|
||||||
|
if (
|
||||||
|
providerId === 'ollama' ||
|
||||||
|
providerId === 'vllm' ||
|
||||||
|
providerId === 'vertex' ||
|
||||||
|
providerId === 'bedrock'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If model resolution fails, fall through and require an API key.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,27 +103,16 @@ const getCurrentVLLMModels = () => {
|
|||||||
* Handles hosted vs self-hosted environments and excludes providers that don't need API key.
|
* Handles hosted vs self-hosted environments and excludes providers that don't need API key.
|
||||||
*/
|
*/
|
||||||
export function getApiKeyCondition() {
|
export function getApiKeyCondition() {
|
||||||
return isHosted
|
return (values?: Record<string, unknown>) => {
|
||||||
? {
|
const model = typeof values?.model === 'string' ? values.model : ''
|
||||||
field: 'model',
|
const shouldShow = shouldRequireApiKeyForModel(model)
|
||||||
value: [...getHostedModels(), ...providers.vertex.models, ...providers.bedrock.models],
|
return buildModelVisibilityCondition(model, shouldShow)
|
||||||
not: true,
|
}
|
||||||
}
|
|
||||||
: () => ({
|
|
||||||
field: 'model',
|
|
||||||
value: [
|
|
||||||
...getCurrentOllamaModels(),
|
|
||||||
...getCurrentVLLMModels(),
|
|
||||||
...providers.vertex.models,
|
|
||||||
...providers.bedrock.models,
|
|
||||||
],
|
|
||||||
not: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the standard provider credential subblocks used by LLM-based blocks.
|
* Returns the standard provider credential subblocks used by LLM-based blocks.
|
||||||
* This includes: Vertex AI OAuth, API Key, Azure OpenAI, Vertex AI config, and Bedrock config.
|
* This includes: Vertex AI OAuth, API Key, Azure (OpenAI + Anthropic), Vertex AI config, and Bedrock config.
|
||||||
*
|
*
|
||||||
* Usage: Spread into your block's subBlocks array after block-specific fields
|
* Usage: Spread into your block's subBlocks array after block-specific fields
|
||||||
*/
|
*/
|
||||||
@@ -111,25 +143,25 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'azureEndpoint',
|
id: 'azureEndpoint',
|
||||||
title: 'Azure OpenAI Endpoint',
|
title: 'Azure Endpoint',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
password: true,
|
password: true,
|
||||||
placeholder: 'https://your-resource.openai.azure.com',
|
placeholder: 'https://your-resource.services.ai.azure.com',
|
||||||
connectionDroppable: false,
|
connectionDroppable: false,
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: providers['azure-openai'].models,
|
value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'azureApiVersion',
|
id: 'azureApiVersion',
|
||||||
title: 'Azure API Version',
|
title: 'Azure API Version',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
placeholder: '2024-07-01-preview',
|
placeholder: 'Enter API version',
|
||||||
connectionDroppable: false,
|
connectionDroppable: false,
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: providers['azure-openai'].models,
|
value: [...providers['azure-openai'].models, ...providers['azure-anthropic'].models],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -202,7 +234,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] {
|
|||||||
*/
|
*/
|
||||||
export const PROVIDER_CREDENTIAL_INPUTS = {
|
export const PROVIDER_CREDENTIAL_INPUTS = {
|
||||||
apiKey: { type: 'string', description: 'Provider API key' },
|
apiKey: { type: 'string', description: 'Provider API key' },
|
||||||
azureEndpoint: { type: 'string', description: 'Azure OpenAI endpoint URL' },
|
azureEndpoint: { type: 'string', description: 'Azure endpoint URL' },
|
||||||
azureApiVersion: { type: 'string', description: 'Azure API version' },
|
azureApiVersion: { type: 'string', description: 'Azure API version' },
|
||||||
vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' },
|
vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' },
|
||||||
vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' },
|
vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' },
|
||||||
|
|||||||
@@ -5468,18 +5468,18 @@ export function AgentSkillsIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
<svg
|
<svg
|
||||||
{...props}
|
{...props}
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
width='24'
|
width='16'
|
||||||
height='24'
|
height='16'
|
||||||
viewBox='0 0 32 32'
|
viewBox='0 0 16 16'
|
||||||
fill='none'
|
fill='none'
|
||||||
>
|
>
|
||||||
<path d='M16 0.5L29.4234 8.25V23.75L16 31.5L2.57661 23.75V8.25L16 0.5Z' fill='currentColor' />
|
|
||||||
<path
|
<path
|
||||||
d='M16 6L24.6603 11V21L16 26L7.33975 21V11L16 6Z'
|
d='M8 1L14.0622 4.5V11.5L8 15L1.93782 11.5V4.5L8 1Z'
|
||||||
fill='currentColor'
|
stroke='currentColor'
|
||||||
stroke='var(--background, white)'
|
strokeWidth='1.5'
|
||||||
strokeWidth='3'
|
fill='none'
|
||||||
/>
|
/>
|
||||||
|
<path d='M8 4.5L11 6.25V9.75L8 11.5L5 9.75V6.25L8 4.5Z' fill='currentColor' />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,43 +5,10 @@ import { CheckCircle, ChevronDown, ChevronRight, Loader2, Settings, XCircle } fr
|
|||||||
import { Badge } from '@/components/emcn'
|
import { Badge } from '@/components/emcn'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||||
|
import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { formatDuration } from '@/lib/core/utils/formatting'
|
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||||
|
|
||||||
interface ToolCallState {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
displayName?: string
|
|
||||||
parameters?: Record<string, unknown>
|
|
||||||
state:
|
|
||||||
| 'detecting'
|
|
||||||
| 'pending'
|
|
||||||
| 'executing'
|
|
||||||
| 'completed'
|
|
||||||
| 'error'
|
|
||||||
| 'rejected'
|
|
||||||
| 'applied'
|
|
||||||
| 'ready_for_review'
|
|
||||||
| 'aborted'
|
|
||||||
| 'skipped'
|
|
||||||
| 'background'
|
|
||||||
startTime?: number
|
|
||||||
endTime?: number
|
|
||||||
duration?: number
|
|
||||||
result?: unknown
|
|
||||||
error?: string
|
|
||||||
progress?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolCallGroup {
|
|
||||||
id: string
|
|
||||||
toolCalls: ToolCallState[]
|
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'error'
|
|
||||||
startTime?: number
|
|
||||||
endTime?: number
|
|
||||||
summary?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolCall: ToolCallState
|
toolCall: ToolCallState
|
||||||
isCompact?: boolean
|
isCompact?: boolean
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { BlockType } from '@/executor/constants'
|
|||||||
import type { DAG } from '@/executor/dag/builder'
|
import type { DAG } from '@/executor/dag/builder'
|
||||||
import type { EdgeManager } from '@/executor/execution/edge-manager'
|
import type { EdgeManager } from '@/executor/execution/edge-manager'
|
||||||
import { serializePauseSnapshot } from '@/executor/execution/snapshot-serializer'
|
import { serializePauseSnapshot } from '@/executor/execution/snapshot-serializer'
|
||||||
import type { SerializableExecutionState } from '@/executor/execution/types'
|
|
||||||
import type { NodeExecutionOrchestrator } from '@/executor/orchestrators/node'
|
import type { NodeExecutionOrchestrator } from '@/executor/orchestrators/node'
|
||||||
import type {
|
import type {
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
@@ -136,7 +135,6 @@ export class ExecutionEngine {
|
|||||||
success: false,
|
success: false,
|
||||||
output: this.finalOutput,
|
output: this.finalOutput,
|
||||||
logs: this.context.blockLogs,
|
logs: this.context.blockLogs,
|
||||||
executionState: this.getSerializableExecutionState(),
|
|
||||||
metadata: this.context.metadata,
|
metadata: this.context.metadata,
|
||||||
status: 'cancelled',
|
status: 'cancelled',
|
||||||
}
|
}
|
||||||
@@ -146,7 +144,6 @@ export class ExecutionEngine {
|
|||||||
success: true,
|
success: true,
|
||||||
output: this.finalOutput,
|
output: this.finalOutput,
|
||||||
logs: this.context.blockLogs,
|
logs: this.context.blockLogs,
|
||||||
executionState: this.getSerializableExecutionState(),
|
|
||||||
metadata: this.context.metadata,
|
metadata: this.context.metadata,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -160,7 +157,6 @@ export class ExecutionEngine {
|
|||||||
success: false,
|
success: false,
|
||||||
output: this.finalOutput,
|
output: this.finalOutput,
|
||||||
logs: this.context.blockLogs,
|
logs: this.context.blockLogs,
|
||||||
executionState: this.getSerializableExecutionState(),
|
|
||||||
metadata: this.context.metadata,
|
metadata: this.context.metadata,
|
||||||
status: 'cancelled',
|
status: 'cancelled',
|
||||||
}
|
}
|
||||||
@@ -463,7 +459,6 @@ export class ExecutionEngine {
|
|||||||
success: true,
|
success: true,
|
||||||
output: this.collectPauseResponses(),
|
output: this.collectPauseResponses(),
|
||||||
logs: this.context.blockLogs,
|
logs: this.context.blockLogs,
|
||||||
executionState: this.getSerializableExecutionState(snapshotSeed),
|
|
||||||
metadata: this.context.metadata,
|
metadata: this.context.metadata,
|
||||||
status: 'paused',
|
status: 'paused',
|
||||||
pausePoints,
|
pausePoints,
|
||||||
@@ -471,24 +466,6 @@ export class ExecutionEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSerializableExecutionState(snapshotSeed?: {
|
|
||||||
snapshot: string
|
|
||||||
}): SerializableExecutionState | undefined {
|
|
||||||
try {
|
|
||||||
const serializedSnapshot =
|
|
||||||
snapshotSeed?.snapshot ?? serializePauseSnapshot(this.context, [], this.dag).snapshot
|
|
||||||
const parsedSnapshot = JSON.parse(serializedSnapshot) as {
|
|
||||||
state?: SerializableExecutionState
|
|
||||||
}
|
|
||||||
return parsedSnapshot.state
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Failed to serialize execution state', {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private collectPauseResponses(): NormalizedBlockOutput {
|
private collectPauseResponses(): NormalizedBlockOutput {
|
||||||
const responses = Array.from(this.pausedBlocks.values()).map((pause) => pause.response)
|
const responses = Array.from(this.pausedBlocks.values()).map((pause) => pause.response)
|
||||||
|
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
_context: {
|
_context: {
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
|
userId: ctx.userId,
|
||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -377,6 +378,9 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
if (ctx.workflowId) {
|
if (ctx.workflowId) {
|
||||||
params.workflowId = ctx.workflowId
|
params.workflowId = ctx.workflowId
|
||||||
}
|
}
|
||||||
|
if (ctx.userId) {
|
||||||
|
params.userId = ctx.userId
|
||||||
|
}
|
||||||
|
|
||||||
const url = buildAPIUrl('/api/tools/custom', params)
|
const url = buildAPIUrl('/api/tools/custom', params)
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
@@ -487,7 +491,9 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
usageControl: tool.usageControl || 'auto',
|
usageControl: tool.usageControl || 'auto',
|
||||||
executeFunction: async (callParams: Record<string, any>) => {
|
executeFunction: async (callParams: Record<string, any>) => {
|
||||||
const headers = await buildAuthHeaders()
|
const headers = await buildAuthHeaders()
|
||||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute')
|
const execParams: Record<string, string> = {}
|
||||||
|
if (ctx.userId) execParams.userId = ctx.userId
|
||||||
|
const execUrl = buildAPIUrl('/api/mcp/tools/execute', execParams)
|
||||||
|
|
||||||
const execResponse = await fetch(execUrl.toString(), {
|
const execResponse = await fetch(execUrl.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -596,6 +602,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
serverId,
|
serverId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
|
...(ctx.userId ? { userId: ctx.userId } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const maxAttempts = 2
|
const maxAttempts = 2
|
||||||
@@ -670,7 +677,9 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
usageControl: tool.usageControl || 'auto',
|
usageControl: tool.usageControl || 'auto',
|
||||||
executeFunction: async (callParams: Record<string, any>) => {
|
executeFunction: async (callParams: Record<string, any>) => {
|
||||||
const headers = await buildAuthHeaders()
|
const headers = await buildAuthHeaders()
|
||||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute')
|
const discoverExecParams: Record<string, string> = {}
|
||||||
|
if (ctx.userId) discoverExecParams.userId = ctx.userId
|
||||||
|
const execUrl = buildAPIUrl('/api/mcp/tools/execute', discoverExecParams)
|
||||||
|
|
||||||
const execResponse = await fetch(execUrl.toString(), {
|
const execResponse = await fetch(execUrl.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -906,24 +915,17 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find first system message
|
|
||||||
const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system')
|
const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system')
|
||||||
|
|
||||||
if (firstSystemIndex === -1) {
|
if (firstSystemIndex === -1) {
|
||||||
// No system message exists - add at position 0
|
|
||||||
messages.unshift({ role: 'system', content })
|
messages.unshift({ role: 'system', content })
|
||||||
} else if (firstSystemIndex === 0) {
|
} else if (firstSystemIndex === 0) {
|
||||||
// System message already at position 0 - replace it
|
|
||||||
// Explicit systemPrompt parameter takes precedence over memory/messages
|
|
||||||
messages[0] = { role: 'system', content }
|
messages[0] = { role: 'system', content }
|
||||||
} else {
|
} else {
|
||||||
// System message exists but not at position 0 - move it to position 0
|
|
||||||
// and update with new content
|
|
||||||
messages.splice(firstSystemIndex, 1)
|
messages.splice(firstSystemIndex, 1)
|
||||||
messages.unshift({ role: 'system', content })
|
messages.unshift({ role: 'system', content })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any additional system messages (keep only the first one)
|
|
||||||
for (let i = messages.length - 1; i >= 1; i--) {
|
for (let i = messages.length - 1; i >= 1; i--) {
|
||||||
if (messages[i].role === 'system') {
|
if (messages[i].role === 'system') {
|
||||||
messages.splice(i, 1)
|
messages.splice(i, 1)
|
||||||
@@ -989,13 +991,14 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
stream: streaming,
|
stream: streaming,
|
||||||
messages,
|
messages: messages?.map(({ executionId, ...msg }) => msg),
|
||||||
environmentVariables: ctx.environmentVariables || {},
|
environmentVariables: ctx.environmentVariables || {},
|
||||||
workflowVariables: ctx.workflowVariables || {},
|
workflowVariables: ctx.workflowVariables || {},
|
||||||
blockData,
|
blockData,
|
||||||
blockNameMapping,
|
blockNameMapping,
|
||||||
reasoningEffort: inputs.reasoningEffort,
|
reasoningEffort: inputs.reasoningEffort,
|
||||||
verbosity: inputs.verbosity,
|
verbosity: inputs.verbosity,
|
||||||
|
thinkingLevel: inputs.thinkingLevel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,6 +1058,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
responseFormat: providerRequest.responseFormat,
|
responseFormat: providerRequest.responseFormat,
|
||||||
workflowId: providerRequest.workflowId,
|
workflowId: providerRequest.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
|
userId: ctx.userId,
|
||||||
stream: providerRequest.stream,
|
stream: providerRequest.stream,
|
||||||
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
||||||
environmentVariables: ctx.environmentVariables || {},
|
environmentVariables: ctx.environmentVariables || {},
|
||||||
@@ -1064,6 +1068,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
reasoningEffort: providerRequest.reasoningEffort,
|
reasoningEffort: providerRequest.reasoningEffort,
|
||||||
verbosity: providerRequest.verbosity,
|
verbosity: providerRequest.verbosity,
|
||||||
|
thinkingLevel: providerRequest.thinkingLevel,
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.processProviderResponse(response, block, responseFormat)
|
return this.processProviderResponse(response, block, responseFormat)
|
||||||
@@ -1081,8 +1086,6 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)
|
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)
|
||||||
|
|
||||||
// Get the credential - we need to find the owner
|
|
||||||
// Since we're in a workflow context, we can query the credential directly
|
|
||||||
const credential = await db.query.account.findFirst({
|
const credential = await db.query.account.findFirst({
|
||||||
where: eq(account.id, credentialId),
|
where: eq(account.id, credentialId),
|
||||||
})
|
})
|
||||||
@@ -1091,7 +1094,6 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the token if needed
|
|
||||||
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface AgentInputs {
|
|||||||
bedrockRegion?: string
|
bedrockRegion?: string
|
||||||
reasoningEffort?: string
|
reasoningEffort?: string
|
||||||
verbosity?: string
|
verbosity?: string
|
||||||
|
thinkingLevel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolInput {
|
export interface ToolInput {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export class ApiBlockHandler implements BlockHandler {
|
|||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
executionId: ctx.executionId,
|
executionId: ctx.executionId,
|
||||||
|
userId: ctx.userId,
|
||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export async function evaluateConditionExpression(
|
|||||||
_context: {
|
_context: {
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
|
userId: ctx.userId,
|
||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = buildAPIUrl('/api/providers')
|
const url = buildAPIUrl('/api/providers', ctx.userId ? { userId: ctx.userId } : {})
|
||||||
|
|
||||||
const providerRequest: Record<string, any> = {
|
const providerRequest: Record<string, any> = {
|
||||||
provider: providerId,
|
provider: providerId,
|
||||||
@@ -121,26 +121,17 @@ export class EvaluatorBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
temperature: EVALUATOR.DEFAULT_TEMPERATURE,
|
temperature: EVALUATOR.DEFAULT_TEMPERATURE,
|
||||||
apiKey: finalApiKey,
|
apiKey: finalApiKey,
|
||||||
|
azureEndpoint: inputs.azureEndpoint,
|
||||||
|
azureApiVersion: inputs.azureApiVersion,
|
||||||
|
vertexProject: evaluatorConfig.vertexProject,
|
||||||
|
vertexLocation: evaluatorConfig.vertexLocation,
|
||||||
|
bedrockAccessKeyId: evaluatorConfig.bedrockAccessKeyId,
|
||||||
|
bedrockSecretKey: evaluatorConfig.bedrockSecretKey,
|
||||||
|
bedrockRegion: evaluatorConfig.bedrockRegion,
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'vertex') {
|
|
||||||
providerRequest.vertexProject = evaluatorConfig.vertexProject
|
|
||||||
providerRequest.vertexLocation = evaluatorConfig.vertexLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'azure-openai') {
|
|
||||||
providerRequest.azureEndpoint = inputs.azureEndpoint
|
|
||||||
providerRequest.azureApiVersion = inputs.azureApiVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'bedrock') {
|
|
||||||
providerRequest.bedrockAccessKeyId = evaluatorConfig.bedrockAccessKeyId
|
|
||||||
providerRequest.bedrockSecretKey = evaluatorConfig.bedrockSecretKey
|
|
||||||
providerRequest.bedrockRegion = evaluatorConfig.bedrockRegion
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await buildAuthHeaders(),
|
headers: await buildAuthHeaders(),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export class FunctionBlockHandler implements BlockHandler {
|
|||||||
_context: {
|
_context: {
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
|
userId: ctx.userId,
|
||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class GenericBlockHandler implements BlockHandler {
|
|||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
executionId: ctx.executionId,
|
executionId: ctx.executionId,
|
||||||
|
userId: ctx.userId,
|
||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -605,6 +605,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
|
|||||||
_context: {
|
_context: {
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
|
userId: ctx.userId,
|
||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
},
|
},
|
||||||
blockData: blockDataWithPause,
|
blockData: blockDataWithPause,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL('/api/providers', getBaseUrl())
|
const url = new URL('/api/providers', getBaseUrl())
|
||||||
|
if (ctx.userId) url.searchParams.set('userId', ctx.userId)
|
||||||
|
|
||||||
const messages = [{ role: 'user', content: routerConfig.prompt }]
|
const messages = [{ role: 'user', content: routerConfig.prompt }]
|
||||||
const systemPrompt = generateRouterPrompt(routerConfig.prompt, targetBlocks)
|
const systemPrompt = generateRouterPrompt(routerConfig.prompt, targetBlocks)
|
||||||
@@ -96,26 +97,17 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
context: JSON.stringify(messages),
|
context: JSON.stringify(messages),
|
||||||
temperature: ROUTER.INFERENCE_TEMPERATURE,
|
temperature: ROUTER.INFERENCE_TEMPERATURE,
|
||||||
apiKey: finalApiKey,
|
apiKey: finalApiKey,
|
||||||
|
azureEndpoint: inputs.azureEndpoint,
|
||||||
|
azureApiVersion: inputs.azureApiVersion,
|
||||||
|
vertexProject: routerConfig.vertexProject,
|
||||||
|
vertexLocation: routerConfig.vertexLocation,
|
||||||
|
bedrockAccessKeyId: routerConfig.bedrockAccessKeyId,
|
||||||
|
bedrockSecretKey: routerConfig.bedrockSecretKey,
|
||||||
|
bedrockRegion: routerConfig.bedrockRegion,
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'vertex') {
|
|
||||||
providerRequest.vertexProject = routerConfig.vertexProject
|
|
||||||
providerRequest.vertexLocation = routerConfig.vertexLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'azure-openai') {
|
|
||||||
providerRequest.azureEndpoint = inputs.azureEndpoint
|
|
||||||
providerRequest.azureApiVersion = inputs.azureApiVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'bedrock') {
|
|
||||||
providerRequest.bedrockAccessKeyId = routerConfig.bedrockAccessKeyId
|
|
||||||
providerRequest.bedrockSecretKey = routerConfig.bedrockSecretKey
|
|
||||||
providerRequest.bedrockRegion = routerConfig.bedrockRegion
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await buildAuthHeaders(),
|
headers: await buildAuthHeaders(),
|
||||||
@@ -218,6 +210,7 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL('/api/providers', getBaseUrl())
|
const url = new URL('/api/providers', getBaseUrl())
|
||||||
|
if (ctx.userId) url.searchParams.set('userId', ctx.userId)
|
||||||
|
|
||||||
const messages = [{ role: 'user', content: routerConfig.context }]
|
const messages = [{ role: 'user', content: routerConfig.context }]
|
||||||
const systemPrompt = generateRouterV2Prompt(routerConfig.context, routes)
|
const systemPrompt = generateRouterV2Prompt(routerConfig.context, routes)
|
||||||
@@ -234,6 +227,13 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
context: JSON.stringify(messages),
|
context: JSON.stringify(messages),
|
||||||
temperature: ROUTER.INFERENCE_TEMPERATURE,
|
temperature: ROUTER.INFERENCE_TEMPERATURE,
|
||||||
apiKey: finalApiKey,
|
apiKey: finalApiKey,
|
||||||
|
azureEndpoint: inputs.azureEndpoint,
|
||||||
|
azureApiVersion: inputs.azureApiVersion,
|
||||||
|
vertexProject: routerConfig.vertexProject,
|
||||||
|
vertexLocation: routerConfig.vertexLocation,
|
||||||
|
bedrockAccessKeyId: routerConfig.bedrockAccessKeyId,
|
||||||
|
bedrockSecretKey: routerConfig.bedrockSecretKey,
|
||||||
|
bedrockRegion: routerConfig.bedrockRegion,
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
responseFormat: {
|
responseFormat: {
|
||||||
@@ -257,22 +257,6 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (providerId === 'vertex') {
|
|
||||||
providerRequest.vertexProject = routerConfig.vertexProject
|
|
||||||
providerRequest.vertexLocation = routerConfig.vertexLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'azure-openai') {
|
|
||||||
providerRequest.azureEndpoint = inputs.azureEndpoint
|
|
||||||
providerRequest.azureApiVersion = inputs.azureApiVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerId === 'bedrock') {
|
|
||||||
providerRequest.bedrockAccessKeyId = routerConfig.bedrockAccessKeyId
|
|
||||||
providerRequest.bedrockSecretKey = routerConfig.bedrockSecretKey
|
|
||||||
providerRequest.bedrockRegion = routerConfig.bedrockRegion
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await buildAuthHeaders(),
|
headers: await buildAuthHeaders(),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user