mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat/codegen: added initial outline for sdk, added api keys table & view (#126)
* feat[sdk]: added scaffolding for sdk pkg * feat[sdk]: simplified usage of tools for agent block * fix[sdk]: added api key directly into AgentOptions * fix[sdk]: simplified the conditional block by removing the code input since we don't need it * feat[sdk]: added account-level api keys & db table to manage them
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# Sim Studio SDK
|
||||
|
||||
The Sim Studio SDK allows developers to create, manage, and deploy workflows programmatically.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @sim-studio/sdk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Simple Workflow
|
||||
|
||||
```typescript
|
||||
import { SimStudio, AgentBlock, FunctionBlock } from '@sim-studio/sdk';
|
||||
|
||||
// Initialize the SDK
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key',
|
||||
});
|
||||
|
||||
// Create a workflow
|
||||
const workflow = simStudio.createWorkflow(
|
||||
'Simple Workflow',
|
||||
'A simple workflow that uses an agent to generate content'
|
||||
);
|
||||
|
||||
// Create an agent block
|
||||
const agentBlock = new AgentBlock({
|
||||
model: 'claude-3-7-sonnet',
|
||||
prompt: 'Write a blog post about <input.topic>',
|
||||
systemPrompt: 'You are a professional content writer.',
|
||||
});
|
||||
|
||||
// Create a function block to format the content
|
||||
const formatterBlock = new FunctionBlock({
|
||||
code: `
|
||||
function formatContent(input) {
|
||||
return {
|
||||
title: "Blog: " + input.topic,
|
||||
content: input.content,
|
||||
wordCount: input.content.split(' ').length
|
||||
};
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
// Add blocks to the workflow
|
||||
workflow.addBlock(agentBlock);
|
||||
workflow.addBlock(formatterBlock);
|
||||
|
||||
// Connect blocks
|
||||
const starterBlock = workflow.getStarterBlock();
|
||||
workflow.connect(starterBlock.id, agentBlock.id);
|
||||
workflow.connect(agentBlock.id, formatterBlock.id);
|
||||
|
||||
// Build and save the workflow
|
||||
const builtWorkflow = workflow.build();
|
||||
simStudio.saveWorkflow(builtWorkflow)
|
||||
.then(savedWorkflow => {
|
||||
console.log(`Workflow saved with ID: ${savedWorkflow.id}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Using Tools with Agents
|
||||
|
||||
Agent blocks can use tools for enhanced capabilities. You can simply reference built-in tools by ID or define custom tools:
|
||||
|
||||
```typescript
|
||||
import { SimStudio, AgentBlock } from '@sim-studio/sdk';
|
||||
|
||||
// Initialize the SDK
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key',
|
||||
});
|
||||
|
||||
// Create a workflow
|
||||
const workflow = simStudio.createWorkflow('Research Assistant');
|
||||
|
||||
// Create an agent block
|
||||
const researchAgent = new AgentBlock({
|
||||
model: 'claude-3-opus',
|
||||
prompt: 'Research the topic "{{input.topic}}" and provide a comprehensive summary.',
|
||||
systemPrompt: 'You are a research assistant.',
|
||||
tools: [] // Will be populated with tool references
|
||||
});
|
||||
|
||||
// Reference built-in tools by their IDs
|
||||
// The system will automatically transform these references into tool definitions
|
||||
researchAgent.data.toolReferences = ['tavily_search', 'serper_search'];
|
||||
|
||||
// Configure tool settings with required parameters
|
||||
researchAgent.data.toolSettings = {
|
||||
tavily_search: {
|
||||
apiKey: 'your-tavily-api-key'
|
||||
},
|
||||
serper_search: {
|
||||
apiKey: 'your-serper-api-key'
|
||||
}
|
||||
};
|
||||
|
||||
// For custom tools, you can define them directly
|
||||
const customTool = {
|
||||
name: 'findCompany',
|
||||
description: 'Find information about a company by name',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company'
|
||||
}
|
||||
},
|
||||
required: ['companyName']
|
||||
}
|
||||
};
|
||||
|
||||
// Add the custom tool to the agent
|
||||
researchAgent.data.tools.push(customTool);
|
||||
|
||||
// Add to workflow
|
||||
workflow.addBlock(researchAgent);
|
||||
|
||||
// Connect to starter
|
||||
const starterBlock = workflow.getStarterBlock();
|
||||
workflow.connect(starterBlock.id, researchAgent.id);
|
||||
|
||||
// Build and save
|
||||
const builtWorkflow = workflow.build();
|
||||
```
|
||||
|
||||
### Creating Conditional Workflows
|
||||
|
||||
You can create workflows with conditional branching using the ConditionBlock:
|
||||
|
||||
```typescript
|
||||
import { SimStudio, AgentBlock, ConditionBlock } from '@sim-studio/sdk';
|
||||
|
||||
// Create a condition block
|
||||
const conditionBlock = new ConditionBlock({
|
||||
conditions: [
|
||||
{ expression: 'input.score > 0.8', id: 'high' },
|
||||
{ expression: 'input.score > 0.5', id: 'medium' },
|
||||
{ expression: 'true', id: 'low' }
|
||||
]
|
||||
});
|
||||
|
||||
// Connect condition outcomes to different blocks
|
||||
workflow.connect(
|
||||
conditionBlock.id,
|
||||
highQualityBlock.id,
|
||||
{ sourceHandle: 'condition-high' }
|
||||
);
|
||||
|
||||
workflow.connect(
|
||||
conditionBlock.id,
|
||||
mediumQualityBlock.id,
|
||||
{ sourceHandle: 'condition-medium' }
|
||||
);
|
||||
|
||||
workflow.connect(
|
||||
conditionBlock.id,
|
||||
lowQualityBlock.id,
|
||||
{ sourceHandle: 'condition-low' }
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### SimStudio
|
||||
|
||||
- `createWorkflow(name, description?)`: Creates a new workflow builder
|
||||
- `saveWorkflow(workflow)`: Saves a workflow and returns the saved workflow with ID
|
||||
- `deployWorkflow(workflowId, options?)`: Deploys a workflow
|
||||
- `executeWorkflow(workflowId, input)`: Executes a workflow with the given input
|
||||
|
||||
### WorkflowBuilder
|
||||
|
||||
- `addBlock(block)`: Adds a block to the workflow
|
||||
- `connect(sourceId, targetId, options?)`: Connects two blocks
|
||||
- `getStarterBlock()`: Gets the starter block
|
||||
- `build()`: Builds the workflow
|
||||
|
||||
### Block Types
|
||||
|
||||
- `AgentBlock`: Agent that can generate content using LLMs
|
||||
- `FunctionBlock`: Executes JavaScript code
|
||||
- `ConditionBlock`: Routes based on conditions
|
||||
- `RouterBlock`: Routes based on agent decisions
|
||||
- `ApiBlock`: Makes HTTP requests
|
||||
- `EvaluatorBlock`: Evaluates content quality
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples](./examples) directory for more examples of using the SDK.
|
||||
|
||||
## Features
|
||||
|
||||
- Create and manage workflows programmatically
|
||||
- Build complex workflows with various block types
|
||||
- Execute workflows and retrieve results
|
||||
- Deploy workflows as API endpoints
|
||||
- Schedule workflows for automatic execution
|
||||
|
||||
## Block Types
|
||||
|
||||
The SDK supports all Sim Studio block types:
|
||||
|
||||
- **Agent**: LLM-powered operations
|
||||
- **Function**: Custom JavaScript code execution
|
||||
- **Condition**: Branching based on logical expressions
|
||||
- **Router**: Dynamic path selection
|
||||
- **API**: HTTP requests
|
||||
- **Evaluator**: LLM-based output assessment
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed documentation, visit [docs.simstudio.dev](https://docs.simstudio.dev)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,87 @@
|
||||
import { SimStudio, AgentBlock, FunctionBlock } from '../src'
|
||||
|
||||
/**
|
||||
* Creates a simple workflow with Sim Studio SDK
|
||||
*/
|
||||
async function createBasicWorkflow() {
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key',
|
||||
})
|
||||
|
||||
// Create a new workflow with a name and description
|
||||
const workflowBuilder = simStudio.createWorkflow(
|
||||
'Content Creation Workflow',
|
||||
'Generate and optimize content for blog posts'
|
||||
)
|
||||
|
||||
// Add blocks
|
||||
const contentGeneratorBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: 'Write a detailed blog post about {{input.topic}}',
|
||||
systemPrompt: 'You are a professional content writer with expertise in creating engaging blog posts.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Content Generator')
|
||||
|
||||
const formatterBlock = new FunctionBlock({
|
||||
code: `
|
||||
function formatContent(input) {
|
||||
// Simple formatting function
|
||||
const title = input.topic.toUpperCase()
|
||||
const content = input.content
|
||||
const wordCount = content.split(' ').length
|
||||
|
||||
return {
|
||||
title,
|
||||
content,
|
||||
wordCount,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
`,
|
||||
}).setName('Formatter')
|
||||
|
||||
const seoOptimizerBlock = new AgentBlock({
|
||||
model: 'claude-3-haiku',
|
||||
prompt: 'Optimize this content for SEO: {{input.content}}',
|
||||
systemPrompt: 'You are an SEO expert. Add relevant keywords, improve readability, and optimize for search engines.',
|
||||
temperature: 0.4,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('SEO Optimizer')
|
||||
|
||||
// Add blocks to workflow
|
||||
workflowBuilder
|
||||
.addBlock(contentGeneratorBlock)
|
||||
.addBlock(formatterBlock)
|
||||
.addBlock(seoOptimizerBlock)
|
||||
|
||||
// Connect blocks
|
||||
const starterBlock = workflowBuilder.getStarterBlock()
|
||||
workflowBuilder
|
||||
.connect(starterBlock.id, contentGeneratorBlock.id)
|
||||
.connect(contentGeneratorBlock.id, formatterBlock.id)
|
||||
.connect(formatterBlock.id, seoOptimizerBlock.id)
|
||||
|
||||
// Build the workflow
|
||||
const workflow = workflowBuilder.build()
|
||||
|
||||
// Execute the workflow
|
||||
try {
|
||||
const workflowId = '123456' // In a real scenario, this would be the ID returned from saveWorkflow
|
||||
const result = await simStudio.executeWorkflow(workflowId, {
|
||||
topic: 'The benefits of AI for content creation'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error executing workflow:', error)
|
||||
}
|
||||
|
||||
return workflow
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
createBasicWorkflow().catch(console.error)
|
||||
}
|
||||
|
||||
export default createBasicWorkflow
|
||||
@@ -0,0 +1,88 @@
|
||||
import { SimStudio } from '../src'
|
||||
import { AgentBlock } from '../src/blocks/agent'
|
||||
import { FunctionBlock } from '../src/blocks/function'
|
||||
|
||||
/**
|
||||
* Example showing how to build and execute a workflow
|
||||
*/
|
||||
async function buildExecuteWorkflowExample() {
|
||||
try {
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key', // Replace with your actual API key
|
||||
})
|
||||
|
||||
// Create a workflow
|
||||
const workflow = simStudio.createWorkflow(
|
||||
'Content Creator',
|
||||
'Generate and format a blog post from a topic'
|
||||
)
|
||||
|
||||
// Create an agent block for content generation
|
||||
const contentAgentBlock = new AgentBlock({
|
||||
model: 'claude-3-opus',
|
||||
prompt: 'Generate a blog post about {{input.topic}}. Include an introduction, 3 main points, and a conclusion.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Content Generator')
|
||||
|
||||
// Create a function block for formatting
|
||||
const formatterBlock = new FunctionBlock({
|
||||
code: `
|
||||
function formatContent(input) {
|
||||
const content = input.content
|
||||
const formattedContent = {
|
||||
title: "Blog Post: " + input.topic,
|
||||
body: content,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
wordCount: content.split(' ').length
|
||||
}
|
||||
return formattedContent
|
||||
}
|
||||
`
|
||||
}).setName('Content Formatter')
|
||||
|
||||
// Add blocks to the workflow
|
||||
workflow.addBlock(contentAgentBlock)
|
||||
workflow.addBlock(formatterBlock)
|
||||
|
||||
// Connect blocks
|
||||
const starterBlock = workflow.getStarterBlock()
|
||||
workflow.connect(starterBlock.id, contentAgentBlock.id)
|
||||
workflow.connect(contentAgentBlock.id, formatterBlock.id)
|
||||
|
||||
// Build the workflow
|
||||
const builtWorkflow = workflow.build()
|
||||
|
||||
// Save the workflow
|
||||
const savedWorkflow = await simStudio.saveWorkflow(builtWorkflow)
|
||||
|
||||
// Deploy the workflow
|
||||
if (savedWorkflow.id) {
|
||||
const deployment = await simStudio.deployWorkflow(savedWorkflow.id)
|
||||
|
||||
// Execute the workflow
|
||||
const executionInput = {
|
||||
topic: 'The Future of Artificial Intelligence'
|
||||
}
|
||||
|
||||
const executionResult = await simStudio.executeWorkflow(
|
||||
savedWorkflow.id,
|
||||
executionInput
|
||||
)
|
||||
|
||||
return executionResult
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in workflow execution:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
buildExecuteWorkflowExample().catch(console.error)
|
||||
}
|
||||
|
||||
export default buildExecuteWorkflowExample
|
||||
@@ -0,0 +1,152 @@
|
||||
import { SimStudio } from '../src'
|
||||
import { AgentBlock } from '../src/blocks/agent'
|
||||
import { ConditionBlock } from '../src/blocks/condition'
|
||||
import { FunctionBlock } from '../src/blocks/function'
|
||||
|
||||
/**
|
||||
* Example showing how to create a workflow with conditional branches
|
||||
* Customer service workflow that routes inquiries to different departments
|
||||
*/
|
||||
async function conditionalWorkflowExample() {
|
||||
try {
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key', // Replace with your actual API key
|
||||
})
|
||||
|
||||
// Create a workflow for handling customer inquiries
|
||||
const workflow = simStudio.createWorkflow(
|
||||
'Customer Inquiry Router',
|
||||
'Routes customer inquiries to appropriate departments based on content'
|
||||
)
|
||||
|
||||
// Create a classifier agent block
|
||||
const classifierBlock = new AgentBlock({
|
||||
model: 'claude-3-haiku',
|
||||
prompt: `
|
||||
Analyze the customer inquiry: "{{input.inquiry}}"
|
||||
Determine which category it belongs to:
|
||||
- technical: Technical support requests or product troubleshooting
|
||||
- billing: Questions about billing, subscriptions, or payments
|
||||
- general: General inquiries, feedback, or other topics
|
||||
|
||||
Output ONLY the category name as your response: technical, billing, or general.
|
||||
`,
|
||||
systemPrompt: 'You are a customer inquiry classifier. Categorize inquiries into technical, billing, or general categories.',
|
||||
temperature: 0.3,
|
||||
apiKey: 'your-agent-api-key' // Include apiKey directly
|
||||
}).setName('Inquiry Classifier')
|
||||
|
||||
// Create a condition block that routes based on the classification
|
||||
const conditionData = {
|
||||
conditions: [
|
||||
{ expression: 'input.classification === "technical"', id: 'technical' },
|
||||
{ expression: 'input.classification === "billing"', id: 'billing' },
|
||||
{ expression: 'input.classification === "general"', id: 'general' }
|
||||
]
|
||||
}
|
||||
|
||||
const conditionBlock = new ConditionBlock(conditionData).setName('Route Inquiry')
|
||||
|
||||
// Create department-specific handler blocks
|
||||
const technicalSupportBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: 'Provide technical support for the following inquiry: {{input.inquiry}}',
|
||||
systemPrompt: 'You are a technical support specialist. Provide detailed technical assistance.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Technical Support')
|
||||
|
||||
const billingSupportBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: 'Address the following billing inquiry: {{input.inquiry}}',
|
||||
systemPrompt: 'You are a billing specialist. Help customers with payment and subscription issues.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Billing Support')
|
||||
|
||||
const generalSupportBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: 'Address the following general inquiry: {{input.inquiry}}',
|
||||
systemPrompt: 'You are a customer support agent. Provide helpful and friendly assistance.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('General Support')
|
||||
|
||||
// Add response formatter
|
||||
const formatterBlock = new FunctionBlock({
|
||||
code: `
|
||||
function formatResponse(input) {
|
||||
return {
|
||||
inquiry: input.inquiry,
|
||||
department: input.classification || "unknown",
|
||||
response: input.content,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
`
|
||||
}).setName('Response Formatter')
|
||||
|
||||
// Add all blocks to the workflow
|
||||
workflow
|
||||
.addBlock(classifierBlock)
|
||||
.addBlock(conditionBlock)
|
||||
.addBlock(technicalSupportBlock)
|
||||
.addBlock(billingSupportBlock)
|
||||
.addBlock(generalSupportBlock)
|
||||
.addBlock(formatterBlock)
|
||||
|
||||
// Connect the blocks
|
||||
const starterBlock = workflow.getStarterBlock()
|
||||
|
||||
// Connect starter to classifier
|
||||
workflow.connect(starterBlock.id, classifierBlock.id)
|
||||
|
||||
// Connect classifier to condition
|
||||
workflow.connect(classifierBlock.id, conditionBlock.id)
|
||||
|
||||
// Connect condition outcomes to department-specific blocks
|
||||
workflow.connect(
|
||||
conditionBlock.id,
|
||||
technicalSupportBlock.id,
|
||||
{ sourceHandle: 'condition-technical' }
|
||||
)
|
||||
|
||||
workflow.connect(
|
||||
conditionBlock.id,
|
||||
billingSupportBlock.id,
|
||||
{ sourceHandle: 'condition-billing' }
|
||||
)
|
||||
|
||||
workflow.connect(
|
||||
conditionBlock.id,
|
||||
generalSupportBlock.id,
|
||||
{ sourceHandle: 'condition-general' }
|
||||
)
|
||||
|
||||
// Connect all support blocks to the formatter
|
||||
workflow.connect(technicalSupportBlock.id, formatterBlock.id)
|
||||
workflow.connect(billingSupportBlock.id, formatterBlock.id)
|
||||
workflow.connect(generalSupportBlock.id, formatterBlock.id)
|
||||
|
||||
// Build the workflow
|
||||
const builtWorkflow = workflow.build()
|
||||
console.log('Conditional workflow built successfully')
|
||||
|
||||
// Example of executing the workflow
|
||||
const workflowId = '123456' // In a real scenario, this would be the ID returned from saveWorkflow
|
||||
console.log(`To execute this workflow with an inquiry: simStudio.executeWorkflow('${workflowId}', { inquiry: "I can't login to my account" })`)
|
||||
|
||||
return builtWorkflow
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating conditional workflow:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
conditionalWorkflowExample().catch(console.error)
|
||||
}
|
||||
|
||||
export default conditionalWorkflowExample
|
||||
@@ -0,0 +1,149 @@
|
||||
import { SimStudio, AgentBlock, FunctionBlock, ConditionBlock, ApiBlock } from '../src'
|
||||
|
||||
// Initialize the SDK
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key',
|
||||
})
|
||||
|
||||
async function createAndDeployWorkflow() {
|
||||
// Create a workflow
|
||||
const workflowBuilder = simStudio.createWorkflow(
|
||||
'Customer Support Workflow',
|
||||
'Automatically categorize and respond to customer inquiries'
|
||||
)
|
||||
|
||||
// Create blocks
|
||||
const agentBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: '{{input.message}}',
|
||||
systemPrompt: 'You are a helpful customer support assistant.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Analyze Request')
|
||||
|
||||
const categoryBlock = new FunctionBlock({
|
||||
code: `
|
||||
function categorize(input) {
|
||||
const message = input.content.toLowerCase()
|
||||
if (message.includes('refund')) return { category: 'refund' }
|
||||
if (message.includes('shipping')) return { category: 'shipping' }
|
||||
return { category: 'general' }
|
||||
}
|
||||
return categorize(input)
|
||||
`
|
||||
}).setName('Categorize Request')
|
||||
|
||||
const routerBlock = new ConditionBlock({
|
||||
conditions: [
|
||||
{ id: 'refund', expression: 'input.category === "refund"' },
|
||||
{ id: 'shipping', expression: 'input.category === "shipping"' },
|
||||
{ id: 'general', expression: 'true' }
|
||||
]
|
||||
}).setName('Route Request')
|
||||
|
||||
const refundBlock = new AgentBlock({
|
||||
model: 'claude-3-haiku',
|
||||
prompt: 'Create a response for this refund request: {{input.message}}',
|
||||
systemPrompt: 'You are a refund specialist.',
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Handle Refund')
|
||||
|
||||
const shippingBlock = new AgentBlock({
|
||||
model: 'claude-3-haiku',
|
||||
prompt: 'Create a response for this shipping request: {{input.message}}',
|
||||
systemPrompt: 'You are a shipping specialist.',
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Handle Shipping')
|
||||
|
||||
const generalBlock = new AgentBlock({
|
||||
model: 'claude-3-haiku',
|
||||
prompt: 'Create a response for this general request: {{input.message}}',
|
||||
systemPrompt: 'You are a general support specialist.',
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Handle General')
|
||||
|
||||
const slackNotificationBlock = new ApiBlock({
|
||||
url: 'https://hooks.slack.com/services/your-webhook-url',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
text: 'New customer support request processed: {{input.category}}'
|
||||
}
|
||||
}).setName('Send Slack Notification')
|
||||
|
||||
// Add blocks to workflow
|
||||
workflowBuilder
|
||||
.addBlock(agentBlock)
|
||||
.addBlock(categoryBlock)
|
||||
.addBlock(routerBlock)
|
||||
.addBlock(refundBlock)
|
||||
.addBlock(shippingBlock)
|
||||
.addBlock(generalBlock)
|
||||
.addBlock(slackNotificationBlock)
|
||||
|
||||
// Connect blocks
|
||||
const starterBlock = workflowBuilder.getStarterBlock()
|
||||
if (!starterBlock) {
|
||||
throw new Error("Starter block not found in workflow")
|
||||
}
|
||||
|
||||
workflowBuilder
|
||||
.connect(starterBlock.id, agentBlock.id)
|
||||
.connect(agentBlock.id, categoryBlock.id)
|
||||
.connect(categoryBlock.id, routerBlock.id)
|
||||
.connect(routerBlock.id, refundBlock.id, { sourceHandle: 'condition-refund' })
|
||||
.connect(routerBlock.id, shippingBlock.id, { sourceHandle: 'condition-shipping' })
|
||||
.connect(routerBlock.id, generalBlock.id, { sourceHandle: 'condition-general' })
|
||||
.connect(refundBlock.id, slackNotificationBlock.id)
|
||||
.connect(shippingBlock.id, slackNotificationBlock.id)
|
||||
.connect(generalBlock.id, slackNotificationBlock.id)
|
||||
|
||||
// Position blocks for visual layout
|
||||
workflowBuilder
|
||||
.positionBlock(starterBlock.id, 0, 0)
|
||||
.positionBlock(agentBlock.id, 200, 0)
|
||||
.positionBlock(categoryBlock.id, 400, 0)
|
||||
.positionBlock(routerBlock.id, 600, 0)
|
||||
.positionBlock(refundBlock.id, 800, -100)
|
||||
.positionBlock(shippingBlock.id, 800, 0)
|
||||
.positionBlock(generalBlock.id, 800, 100)
|
||||
.positionBlock(slackNotificationBlock.id, 1000, 0)
|
||||
|
||||
// Save the workflow
|
||||
const workflow = await simStudio.saveWorkflow(workflowBuilder.build())
|
||||
|
||||
if (!workflow.id) {
|
||||
throw new Error("Workflow ID was not returned from the server")
|
||||
}
|
||||
|
||||
// Deploy the workflow
|
||||
const deployment = await simStudio.deployWorkflow(workflow.id, {
|
||||
isPublic: true,
|
||||
authentication: 'api_key',
|
||||
rateLimit: 100,
|
||||
})
|
||||
|
||||
// Schedule the workflow to run daily
|
||||
const schedule = await simStudio.scheduleWorkflow(workflow.id, {
|
||||
cron: '0 9 * * 1-5', // 9 AM on weekdays
|
||||
timezone: 'America/New_York',
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
// Execute the workflow with sample input
|
||||
const result = await simStudio.executeWorkflow(workflow.id, {
|
||||
message: "I haven't received my order yet and it's been 2 weeks. Can you help me track my package?"
|
||||
})
|
||||
|
||||
return {
|
||||
workflowId: workflow.id,
|
||||
deploymentUrl: deployment.url,
|
||||
scheduleId: schedule.id,
|
||||
executionResult: result
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
createAndDeployWorkflow().catch(console.error)
|
||||
@@ -0,0 +1,101 @@
|
||||
import { SimStudio, AgentBlock, WorkflowCodeGenerator } from '../src'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const __dirname = path.resolve()
|
||||
|
||||
/**
|
||||
* Example demonstrating how to generate TypeScript code from a workflow
|
||||
*/
|
||||
async function generateWorkflowCode() {
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key',
|
||||
})
|
||||
|
||||
const workflowBuilder = simStudio.createWorkflow(
|
||||
'Content Creation Workflow',
|
||||
'Generate and optimize content for blog posts'
|
||||
)
|
||||
|
||||
// Add blocks
|
||||
const ideaGeneratorBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: 'Generate 5 blog post ideas about {{input.topic}}',
|
||||
systemPrompt: 'You are a creative content strategist.',
|
||||
temperature: 0.8,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Generate Ideas')
|
||||
|
||||
const outlineBlock = new AgentBlock({
|
||||
model: 'claude-3-sonnet',
|
||||
prompt: 'Create a detailed outline for this blog post idea: {{input.selectedIdea}}',
|
||||
systemPrompt: 'You are a skilled content outliner.',
|
||||
temperature: 0.7,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Create Outline')
|
||||
|
||||
const draftBlock = new AgentBlock({
|
||||
model: 'claude-3-opus',
|
||||
prompt: 'Write a blog post based on this outline: {{input.outline}}',
|
||||
systemPrompt: 'You are a professional blog writer with expertise in creating engaging content.',
|
||||
temperature: 0.6,
|
||||
maxTokens: 2500,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Write Draft')
|
||||
|
||||
const optimizeBlock = new AgentBlock({
|
||||
model: 'claude-3-haiku',
|
||||
prompt: 'Optimize this blog post for SEO: {{input.draft}}',
|
||||
systemPrompt: 'You are an SEO expert. Add relevant keywords, improve readability, and optimize for search engines.',
|
||||
temperature: 0.4,
|
||||
apiKey: 'your-agent-api-key'
|
||||
}).setName('Optimize for SEO')
|
||||
|
||||
// Add blocks to workflow
|
||||
workflowBuilder
|
||||
.addBlock(ideaGeneratorBlock)
|
||||
.addBlock(outlineBlock)
|
||||
.addBlock(draftBlock)
|
||||
.addBlock(optimizeBlock)
|
||||
|
||||
// Connect blocks
|
||||
const starterBlock = workflowBuilder.getStarterBlock()
|
||||
workflowBuilder
|
||||
.connect(starterBlock.id, ideaGeneratorBlock.id)
|
||||
.connect(ideaGeneratorBlock.id, outlineBlock.id)
|
||||
.connect(outlineBlock.id, draftBlock.id)
|
||||
.connect(draftBlock.id, optimizeBlock.id)
|
||||
|
||||
// Build the workflow
|
||||
const workflow = workflowBuilder.build()
|
||||
|
||||
// Generate TypeScript code
|
||||
const codeGenerator = new WorkflowCodeGenerator(workflow, {
|
||||
includeComments: true,
|
||||
prettyPrint: true,
|
||||
addGenerationInfo: true,
|
||||
exportAsModule: true,
|
||||
mainFunctionName: 'createContentWorkflow',
|
||||
moduleFormat: 'commonjs',
|
||||
})
|
||||
|
||||
const generatedCode = codeGenerator.generate()
|
||||
|
||||
// Save the generated code to a file
|
||||
const outputDir = path.join(__dirname, 'examples', 'output')
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
const outputFile = path.join(outputDir, 'content-workflow.ts')
|
||||
fs.writeFileSync(outputFile, generatedCode)
|
||||
|
||||
return {
|
||||
workflow,
|
||||
generatedCode,
|
||||
outputFile,
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
generateWorkflowCode().catch(console.error)
|
||||
@@ -0,0 +1,99 @@
|
||||
import { SimStudio } from '../src'
|
||||
import { AgentBlock } from '../src/blocks/agent'
|
||||
|
||||
/**
|
||||
* Example showing how to use tools with agent blocks
|
||||
*/
|
||||
async function usingToolsExample() {
|
||||
const simStudio = new SimStudio({
|
||||
apiKey: 'your-api-key', // Replace with your actual API key
|
||||
})
|
||||
|
||||
// Create a workflow
|
||||
const workflow = simStudio.createWorkflow(
|
||||
'Research Assistant',
|
||||
'Research a topic and summarize findings'
|
||||
)
|
||||
|
||||
// Create the agent block with tools
|
||||
const researchAgentBlock = new AgentBlock({
|
||||
model: 'claude-3-opus',
|
||||
prompt: 'Research the topic "{{input.topic}}" and provide a comprehensive summary. Use the search tools to gather information.',
|
||||
systemPrompt: 'You are a research assistant that can search the web for information and compile comprehensive reports.',
|
||||
temperature: 0.5,
|
||||
tools: [], // Will be populated with tool references
|
||||
apiKey: 'your-agent-api-key' // Now included directly in options
|
||||
}).setName('Research Agent')
|
||||
|
||||
// Reference built-in tools by their IDs
|
||||
// The system will automatically transform these references into tool definitions
|
||||
researchAgentBlock.data.toolReferences = ['tavily_search', 'serper_search']
|
||||
|
||||
// Configure tool settings with required parameters
|
||||
researchAgentBlock.data.toolSettings = {
|
||||
// Each tool gets its required parameters
|
||||
tavily_search: {
|
||||
apiKey: 'your-tavily-api-key'
|
||||
},
|
||||
serper_search: {
|
||||
apiKey: 'your-serper-api-key'
|
||||
}
|
||||
}
|
||||
|
||||
// Add the agent to the workflow
|
||||
workflow.addBlock(researchAgentBlock)
|
||||
|
||||
// For custom tools, users can still define them directly if needed
|
||||
// This is equivalent to when a user creates a custom tool in the UI
|
||||
const customTool = {
|
||||
name: 'findCompany',
|
||||
description: 'Find information about a company by name',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to find information about'
|
||||
},
|
||||
detailLevel: {
|
||||
type: 'string',
|
||||
enum: ['basic', 'detailed'],
|
||||
description: 'The level of detail to return',
|
||||
default: 'basic'
|
||||
}
|
||||
},
|
||||
required: ['companyName']
|
||||
}
|
||||
}
|
||||
|
||||
// Add the custom tool to the agent
|
||||
if (!researchAgentBlock.data.tools) {
|
||||
researchAgentBlock.data.tools = []
|
||||
}
|
||||
researchAgentBlock.data.tools.push(customTool)
|
||||
|
||||
console.log('Added tools to the agent')
|
||||
|
||||
// Connect the starter block to the agent
|
||||
const starterBlock = workflow.getStarterBlock()
|
||||
workflow.connect(starterBlock.id, researchAgentBlock.id)
|
||||
|
||||
// Build the workflow
|
||||
const builtWorkflow = workflow.build()
|
||||
console.log('Workflow built successfully with search tools')
|
||||
|
||||
// In a real scenario, you would save and deploy the workflow
|
||||
console.log('To execute this workflow, you would:')
|
||||
console.log('1. Save the workflow: simStudio.saveWorkflow(workflow)')
|
||||
console.log('2. Deploy the workflow: simStudio.deployWorkflow(workflowId)')
|
||||
console.log('3. Execute the workflow: simStudio.executeWorkflow(workflowId, { topic: "AI ethics" })')
|
||||
|
||||
return builtWorkflow
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
usingToolsExample().catch(console.error)
|
||||
}
|
||||
|
||||
export default usingToolsExample
|
||||
Generated
+5173
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@sim-studio/sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "Official SDK for Sim Studio - build, deploy, and manage agentic workflows",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"prebuild": "ts-node src/scripts/sync-tools.ts",
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"sync-tools": "ts-node src/scripts/sync-tools.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^18.15.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"eslint": "^8.36.0",
|
||||
"jest": "^29.5.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Block } from './base'
|
||||
import { Tool } from '../tools'
|
||||
|
||||
export interface AgentOptions {
|
||||
model: string
|
||||
prompt: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
systemPrompt?: string
|
||||
tools?: AgentTool[]
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface AgentTool {
|
||||
name: string
|
||||
description: string
|
||||
parameters: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent block for LLM-powered operations
|
||||
*/
|
||||
export class AgentBlock extends Block {
|
||||
// Define required parameters for agent blocks
|
||||
static requiredParameters: string[] = ['model', 'prompt']
|
||||
|
||||
constructor(options: AgentOptions) {
|
||||
super('agent', options)
|
||||
this.metadata.id = 'agent'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the model to use for this agent
|
||||
*/
|
||||
setModel(model: string): this {
|
||||
this.data.model = model
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prompt for this agent
|
||||
*/
|
||||
setPrompt(prompt: string): this {
|
||||
this.data.prompt = prompt
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the system prompt for this agent
|
||||
*/
|
||||
setSystemPrompt(systemPrompt: string): this {
|
||||
this.data.systemPrompt = systemPrompt
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the temperature for response generation
|
||||
*/
|
||||
setTemperature(temperature: number): this {
|
||||
this.data.temperature = temperature
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum token count for response
|
||||
*/
|
||||
setMaxTokens(maxTokens: number): this {
|
||||
this.data.maxTokens = maxTokens
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tool for the agent to use
|
||||
*/
|
||||
addTool(tool: Tool | AgentTool): this {
|
||||
if (!this.data.tools) {
|
||||
this.data.tools = []
|
||||
}
|
||||
|
||||
// Convert Tool to AgentTool if needed
|
||||
const agentTool: AgentTool = 'parameters' in tool
|
||||
? { name: tool.name, description: tool.description, parameters: tool.parameters }
|
||||
: { name: tool.id, description: tool.description, parameters: tool.schema }
|
||||
|
||||
this.data.tools.push(agentTool)
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Block } from './base'
|
||||
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
|
||||
|
||||
export interface ApiOptions {
|
||||
url: string
|
||||
method: HttpMethod
|
||||
headers?: Record<string, string>
|
||||
body?: any
|
||||
query?: Record<string, string>
|
||||
timeout?: number
|
||||
authentication?: {
|
||||
type: 'basic' | 'bearer' | 'api_key'
|
||||
credentials?: {
|
||||
username?: string
|
||||
password?: string
|
||||
token?: string
|
||||
key?: string
|
||||
value?: string
|
||||
in?: 'header' | 'query'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API block for making HTTP requests
|
||||
*/
|
||||
export class ApiBlock extends Block {
|
||||
constructor(options: ApiOptions) {
|
||||
super('api', options)
|
||||
this.metadata.id = 'api'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request URL
|
||||
*/
|
||||
setUrl(url: string): this {
|
||||
this.data.url = url
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP method
|
||||
*/
|
||||
setMethod(method: HttpMethod): this {
|
||||
this.data.method = method
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request headers
|
||||
*/
|
||||
setHeaders(headers: Record<string, string>): this {
|
||||
this.data.headers = headers
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a header
|
||||
*/
|
||||
addHeader(key: string, value: string): this {
|
||||
if (!this.data.headers) {
|
||||
this.data.headers = {}
|
||||
}
|
||||
this.data.headers[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request body
|
||||
*/
|
||||
setBody(body: any): this {
|
||||
this.data.body = body
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set query parameters
|
||||
*/
|
||||
setQueryParams(query: Record<string, string>): this {
|
||||
this.data.query = query
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query parameter
|
||||
*/
|
||||
addQueryParam(key: string, value: string): this {
|
||||
if (!this.data.query) {
|
||||
this.data.query = {}
|
||||
}
|
||||
this.data.query[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request timeout
|
||||
*/
|
||||
setTimeout(timeout: number): this {
|
||||
this.data.timeout = timeout
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure basic authentication
|
||||
*/
|
||||
setBasicAuth(username: string, password: string): this {
|
||||
this.data.authentication = {
|
||||
type: 'basic',
|
||||
credentials: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure bearer token authentication
|
||||
*/
|
||||
setBearerAuth(token: string): this {
|
||||
this.data.authentication = {
|
||||
type: 'bearer',
|
||||
credentials: {
|
||||
token
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure API key authentication
|
||||
*/
|
||||
setApiKeyAuth(key: string, value: string, location: 'header' | 'query' = 'header'): this {
|
||||
this.data.authentication = {
|
||||
type: 'api_key',
|
||||
credentials: {
|
||||
key,
|
||||
value,
|
||||
in: location
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { BlockType, BlockData } from '../types'
|
||||
import { Tool } from '../tools'
|
||||
import { generateId as generateUniqueId } from '../utils'
|
||||
import { blockRequiredParameters, getToolRequiredParameters } from '../generated'
|
||||
|
||||
/**
|
||||
* Base class for all workflow blocks
|
||||
*/
|
||||
export abstract class Block {
|
||||
id: string
|
||||
type: BlockType
|
||||
data: BlockData
|
||||
enabled: boolean
|
||||
metadata: {
|
||||
name?: string
|
||||
description?: string
|
||||
[key: string]: any
|
||||
}
|
||||
private tools: Tool[] = []
|
||||
|
||||
// Define static property for required parameters that subclasses should override
|
||||
static requiredParameters: string[] = []
|
||||
|
||||
constructor(type: BlockType, data: BlockData = {}) {
|
||||
this.id = generateUniqueId()
|
||||
this.type = type
|
||||
this.data = data
|
||||
this.enabled = true
|
||||
this.metadata = {}
|
||||
|
||||
// Validate required parameters
|
||||
this.validateRequiredParameters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that all required parameters are present
|
||||
*/
|
||||
private validateRequiredParameters(): void {
|
||||
// First check static requirements from the class (if any)
|
||||
const staticRequiredParams = (this.constructor as typeof Block).requiredParameters
|
||||
if (staticRequiredParams.length > 0) {
|
||||
const missingParams = staticRequiredParams.filter(param => this.data[param] === undefined)
|
||||
if (missingParams.length > 0) {
|
||||
throw new Error(`Missing required parameters for ${this.type} block: ${missingParams.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Then check the dynamically generated requirements from the main app
|
||||
const dynamicRequiredParams = blockRequiredParameters[this.type] || []
|
||||
if (dynamicRequiredParams.length > 0) {
|
||||
const missingParams = dynamicRequiredParams.filter(param => this.data[param] === undefined)
|
||||
if (missingParams.length > 0) {
|
||||
throw new Error(`Missing required parameters for ${this.type} block: ${missingParams.join(', ')}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block's name
|
||||
*/
|
||||
setName(name: string): this {
|
||||
this.metadata.name = name
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block's description
|
||||
*/
|
||||
setDescription(description: string): this {
|
||||
this.metadata.description = description
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the block
|
||||
*/
|
||||
setEnabled(enabled: boolean): this {
|
||||
this.enabled = enabled
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom metadata
|
||||
*/
|
||||
setMetadata(key: string, value: any): this {
|
||||
this.metadata[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tool to this block
|
||||
*/
|
||||
addTool(tool: Tool): this {
|
||||
this.tools.push(tool)
|
||||
|
||||
// If this is an agent block, update the data.tools array
|
||||
if (this.type === 'agent' && Array.isArray(this.data.tools)) {
|
||||
// Validate required tool parameters
|
||||
const requiredParams = getToolRequiredParameters(tool.id)
|
||||
if (requiredParams.length > 0) {
|
||||
const toolSettings = this.data.toolSettings?.[tool.id] || {}
|
||||
const missingParams = requiredParams.filter(param =>
|
||||
toolSettings[param] === undefined
|
||||
)
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
throw new Error(`Missing required parameters for tool '${tool.id}': ${missingParams.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
this.data.tools.push({
|
||||
name: tool.id,
|
||||
description: tool.description,
|
||||
parameters: tool.schema
|
||||
})
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tools attached to this block
|
||||
*/
|
||||
getTools(): Tool[] {
|
||||
return this.tools
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the block for API requests
|
||||
*/
|
||||
toJSON(): any {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
data: this.data,
|
||||
enabled: this.enabled,
|
||||
metadata: this.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique ID for blocks
|
||||
*/
|
||||
function generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 9)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Block } from './base'
|
||||
|
||||
export interface Condition {
|
||||
id: string
|
||||
expression: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface ConditionOptions {
|
||||
conditions: Condition[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition block for branching workflows based on logical expressions
|
||||
*/
|
||||
export class ConditionBlock extends Block {
|
||||
constructor(options: ConditionOptions) {
|
||||
super('condition', options)
|
||||
this.metadata.id = 'condition'
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition branch
|
||||
*/
|
||||
addCondition(condition: Condition): this {
|
||||
if (!this.data.conditions) {
|
||||
this.data.conditions = []
|
||||
}
|
||||
this.data.conditions.push(condition)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a condition by ID
|
||||
*/
|
||||
removeCondition(conditionId: string): this {
|
||||
if (this.data.conditions) {
|
||||
this.data.conditions = this.data.conditions.filter(
|
||||
(c: Condition) => c.id !== conditionId
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all conditions at once
|
||||
*/
|
||||
setConditions(conditions: Condition[]): this {
|
||||
this.data.conditions = conditions
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Block } from './base'
|
||||
|
||||
export interface EvaluatorOptions {
|
||||
criteria: Criterion[]
|
||||
model?: string
|
||||
systemPrompt?: string
|
||||
}
|
||||
|
||||
export interface Criterion {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
weight?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator block for assessing outputs using LLM-based evaluation
|
||||
*/
|
||||
export class EvaluatorBlock extends Block {
|
||||
constructor(options: EvaluatorOptions) {
|
||||
super('evaluator', options)
|
||||
this.metadata.id = 'evaluator'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the evaluation criteria
|
||||
*/
|
||||
setCriteria(criteria: Criterion[]): this {
|
||||
this.data.criteria = criteria
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an evaluation criterion
|
||||
*/
|
||||
addCriterion(criterion: Criterion): this {
|
||||
if (!this.data.criteria) {
|
||||
this.data.criteria = []
|
||||
}
|
||||
this.data.criteria.push(criterion)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a criterion by ID
|
||||
*/
|
||||
removeCriterion(criterionId: string): this {
|
||||
if (this.data.criteria) {
|
||||
this.data.criteria = this.data.criteria.filter(
|
||||
(c: Criterion) => c.id !== criterionId
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the model to use for evaluation
|
||||
*/
|
||||
setModel(model: string): this {
|
||||
this.data.model = model
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the system prompt for evaluation
|
||||
*/
|
||||
setSystemPrompt(systemPrompt: string): this {
|
||||
this.data.systemPrompt = systemPrompt
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Block } from './base'
|
||||
|
||||
export interface FunctionOptions {
|
||||
code: string
|
||||
timeout?: number
|
||||
environment?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Function block for custom JavaScript code execution
|
||||
*/
|
||||
export class FunctionBlock extends Block {
|
||||
constructor(options: FunctionOptions) {
|
||||
super('function', options)
|
||||
this.metadata.id = 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the JavaScript code to execute
|
||||
*/
|
||||
setCode(code: string): this {
|
||||
this.data.code = code
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution timeout in milliseconds
|
||||
*/
|
||||
setTimeout(timeout: number): this {
|
||||
this.data.timeout = timeout
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set environment variables for function execution
|
||||
*/
|
||||
setEnvironment(env: Record<string, string>): this {
|
||||
this.data.environment = env
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './base'
|
||||
export * from './agent'
|
||||
export * from './function'
|
||||
export * from './condition'
|
||||
export * from './router'
|
||||
export * from './api'
|
||||
export * from './evaluator'
|
||||
export * from './starter'
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Block } from './base'
|
||||
import { availableBlocks, toolToBlockMap } from '../generated/registry'
|
||||
|
||||
/**
|
||||
* Registry for all available block types
|
||||
*/
|
||||
export class BlockRegistry {
|
||||
private static blocks: Map<string, new (...args: any[]) => Block> = new Map()
|
||||
private static blockOptions: Map<string, any> = new Map()
|
||||
|
||||
/**
|
||||
* Register a block type
|
||||
*/
|
||||
static register<T extends Block>(
|
||||
id: string,
|
||||
blockClass: new (...args: any[]) => T,
|
||||
options?: {
|
||||
category?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
defaultData?: any
|
||||
schema?: any
|
||||
}
|
||||
): void {
|
||||
this.blocks.set(id, blockClass)
|
||||
if (options) {
|
||||
this.blockOptions.set(id, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new block instance by ID
|
||||
*/
|
||||
static create(id: string, data?: any): Block | null {
|
||||
const BlockClass = this.blocks.get(id)
|
||||
if (!BlockClass) {
|
||||
return null
|
||||
}
|
||||
return new BlockClass(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block type is registered
|
||||
*/
|
||||
static has(id: string): boolean {
|
||||
return this.blocks.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered block types
|
||||
*/
|
||||
static getAll(): string[] {
|
||||
return Array.from(this.blocks.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block options for a specific block type
|
||||
*/
|
||||
static getOptions(id: string): any {
|
||||
return this.blockOptions.get(id) || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is available in the main application
|
||||
*/
|
||||
static isAvailableInMainApp(id: string): boolean {
|
||||
return availableBlocks.includes(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block type for a tool
|
||||
*/
|
||||
static getBlockTypeForTool(toolId: string): string | undefined {
|
||||
return toolToBlockMap[toolId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available block types from the main application
|
||||
*/
|
||||
static getAllAvailableBlocks(): string[] {
|
||||
return [...availableBlocks]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for registering a block class with the registry
|
||||
*/
|
||||
export function registerBlock(
|
||||
id: string,
|
||||
options?: {
|
||||
category?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
defaultData?: any
|
||||
schema?: any
|
||||
}
|
||||
): ClassDecorator {
|
||||
return (target: any) => {
|
||||
BlockRegistry.register(id, target, options)
|
||||
return target
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for block imports from the main application
|
||||
*/
|
||||
export interface BlockImport {
|
||||
id: string
|
||||
blockClass: new (...args: any[]) => Block
|
||||
options?: {
|
||||
category?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
defaultData?: any
|
||||
schema?: any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import blocks from the main application
|
||||
*/
|
||||
export function importBlocks(blocks: BlockImport[]): void {
|
||||
blocks.forEach(block => {
|
||||
BlockRegistry.register(block.id, block.blockClass, block.options)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Block } from './base'
|
||||
|
||||
export interface Route {
|
||||
id: string
|
||||
targetId: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface RouterOptions {
|
||||
routes: Route[]
|
||||
defaultRouteId?: string
|
||||
routingExpression?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Router block for dynamic path selection in workflows
|
||||
*/
|
||||
export class RouterBlock extends Block {
|
||||
constructor(options: RouterOptions) {
|
||||
super('router', options)
|
||||
this.metadata.id = 'router'
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a route
|
||||
*/
|
||||
addRoute(route: Route): this {
|
||||
if (!this.data.routes) {
|
||||
this.data.routes = []
|
||||
}
|
||||
this.data.routes.push(route)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a route by ID
|
||||
*/
|
||||
removeRoute(routeId: string): this {
|
||||
if (this.data.routes) {
|
||||
this.data.routes = this.data.routes.filter(
|
||||
(r: Route) => r.id !== routeId
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all routes at once
|
||||
*/
|
||||
setRoutes(routes: Route[]): this {
|
||||
this.data.routes = routes
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default route ID
|
||||
*/
|
||||
setDefaultRoute(routeId: string): this {
|
||||
this.data.defaultRouteId = routeId
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the routing expression
|
||||
*/
|
||||
setRoutingExpression(expression: string): this {
|
||||
this.data.routingExpression = expression
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Block } from './base'
|
||||
|
||||
export interface StarterOptions {
|
||||
input?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Starter block for workflow entry point
|
||||
*/
|
||||
export class StarterBlock extends Block {
|
||||
constructor(options: StarterOptions = {}) {
|
||||
super('starter', options)
|
||||
this.metadata.id = 'starter'
|
||||
this.setName('Start')
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default input for this workflow
|
||||
*/
|
||||
setInput(input: Record<string, any>): this {
|
||||
this.data.input = input
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
import { Workflow, Block, Connection } from '../types'
|
||||
|
||||
/**
|
||||
* Options for code generation
|
||||
*/
|
||||
export interface CodeGenerationOptions {
|
||||
/**
|
||||
* Include comments in generated code
|
||||
*/
|
||||
includeComments?: boolean
|
||||
|
||||
/**
|
||||
* Format the code with proper indentation
|
||||
*/
|
||||
prettyPrint?: boolean
|
||||
|
||||
/**
|
||||
* Add a timestamp and generator information
|
||||
*/
|
||||
addGenerationInfo?: boolean
|
||||
|
||||
/**
|
||||
* Export workflow as a module
|
||||
*/
|
||||
exportAsModule?: boolean
|
||||
|
||||
/**
|
||||
* Name of the main function
|
||||
*/
|
||||
mainFunctionName?: string
|
||||
|
||||
/**
|
||||
* ESNext or CommonJS module format
|
||||
*/
|
||||
moduleFormat?: 'esm' | 'commonjs'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates TypeScript code from a workflow
|
||||
*/
|
||||
export class WorkflowCodeGenerator {
|
||||
private workflow: Workflow
|
||||
private options: Required<CodeGenerationOptions>
|
||||
|
||||
constructor(workflow: Workflow, options: CodeGenerationOptions = {}) {
|
||||
this.workflow = workflow
|
||||
this.options = {
|
||||
includeComments: options.includeComments ?? true,
|
||||
prettyPrint: options.prettyPrint ?? true,
|
||||
addGenerationInfo: options.addGenerationInfo ?? true,
|
||||
exportAsModule: options.exportAsModule ?? true,
|
||||
mainFunctionName: options.mainFunctionName ?? this.sanitizeName(workflow.name || 'workflow'),
|
||||
moduleFormat: options.moduleFormat ?? 'esm',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TypeScript code representing the workflow
|
||||
*/
|
||||
generate(): string {
|
||||
const parts: string[] = []
|
||||
|
||||
// Add generation info if requested
|
||||
if (this.options.addGenerationInfo) {
|
||||
parts.push(this.generateHeader())
|
||||
}
|
||||
|
||||
// Add imports
|
||||
parts.push(this.generateImports())
|
||||
|
||||
// Add block declarations
|
||||
parts.push(this.generateBlockDeclarations())
|
||||
|
||||
// Add main function
|
||||
parts.push(this.generateMainFunction())
|
||||
|
||||
// Join all parts
|
||||
return parts.join('\n\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate header comment with generation info
|
||||
*/
|
||||
private generateHeader(): string {
|
||||
const date = new Date().toISOString()
|
||||
return [
|
||||
'/**',
|
||||
' * This code was generated from a Sim Studio workflow',
|
||||
` * Workflow: ${this.workflow.name || 'Unnamed workflow'}`,
|
||||
` * Generated: ${date}`,
|
||||
' */',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate imports section
|
||||
*/
|
||||
private generateImports(): string {
|
||||
if (this.options.moduleFormat === 'esm') {
|
||||
return [
|
||||
"import { SimStudio } from '@sim-studio/sdk'",
|
||||
this.getBlockImports(),
|
||||
].join('\n')
|
||||
} else {
|
||||
return [
|
||||
"const { SimStudio } = require('@sim-studio/sdk')",
|
||||
this.getBlockImports('commonjs'),
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate imports for block types used in the workflow
|
||||
*/
|
||||
private getBlockImports(format: 'esm' | 'commonjs' = 'esm'): string {
|
||||
const blockTypes = new Set<string>()
|
||||
|
||||
for (const block of this.workflow.blocks) {
|
||||
const blockType = this.getBlockClassName(block)
|
||||
if (blockType && blockType !== 'StarterBlock') {
|
||||
blockTypes.add(blockType)
|
||||
}
|
||||
}
|
||||
|
||||
if (blockTypes.size === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const blockTypesList = Array.from(blockTypes).sort()
|
||||
|
||||
if (format === 'esm') {
|
||||
return `import { ${blockTypesList.join(', ')} } from '@sim-studio/sdk'`
|
||||
} else {
|
||||
return `const { ${blockTypesList.join(', ')} } = require('@sim-studio/sdk')`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate declarations for all blocks
|
||||
*/
|
||||
private generateBlockDeclarations(): string {
|
||||
return this.workflow.blocks
|
||||
.filter(block => block.enabled !== false && block.metadata?.id !== 'starter')
|
||||
.map(block => this.generateBlockDeclaration(block))
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the main function that creates and executes the workflow
|
||||
*/
|
||||
private generateMainFunction(): string {
|
||||
const lines: string[] = []
|
||||
|
||||
// Function signature
|
||||
if (this.options.exportAsModule) {
|
||||
lines.push(`export async function ${this.options.mainFunctionName}(input = {}) {`)
|
||||
} else {
|
||||
lines.push(`async function ${this.options.mainFunctionName}(input = {}) {`)
|
||||
}
|
||||
|
||||
// Initialize SDK
|
||||
lines.push(' // Initialize the SDK')
|
||||
lines.push(" const simStudio = new SimStudio()")
|
||||
lines.push('')
|
||||
|
||||
// Create workflow
|
||||
lines.push(' // Create a new workflow')
|
||||
lines.push(` const workflowBuilder = simStudio.createWorkflow('${this.workflow.name || 'Generated Workflow'}')`)
|
||||
lines.push('')
|
||||
|
||||
// Add blocks
|
||||
lines.push(' // Add blocks to the workflow')
|
||||
for (const block of this.workflow.blocks) {
|
||||
if (block.metadata?.id !== 'starter' && block.enabled !== false) {
|
||||
const varName = this.getBlockVariableName(block)
|
||||
lines.push(` workflowBuilder.addBlock(${varName})`)
|
||||
}
|
||||
}
|
||||
lines.push('')
|
||||
|
||||
// Get starter block
|
||||
lines.push(' // Get the starter block')
|
||||
lines.push(' const starterBlock = workflowBuilder.getStarterBlock()')
|
||||
lines.push('')
|
||||
|
||||
// Connect blocks
|
||||
lines.push(' // Connect blocks')
|
||||
const connections = this.generateConnectionsCode()
|
||||
connections.forEach(conn => lines.push(` ${conn}`))
|
||||
lines.push('')
|
||||
|
||||
// Create loops if any
|
||||
if (this.workflow.loops && Object.keys(this.workflow.loops).length > 0) {
|
||||
lines.push(' // Create loops')
|
||||
for (const [loopId, loop] of Object.entries(this.workflow.loops)) {
|
||||
const nodesArray = JSON.stringify(loop.nodes)
|
||||
lines.push(` workflowBuilder.createLoop(${nodesArray}, ${loop.maxIterations}${loop.iterationVariable ? `, '${loop.iterationVariable}'` : ''})`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// Save workflow
|
||||
lines.push(' // Save and execute the workflow')
|
||||
lines.push(' const workflow = await workflowBuilder.build()')
|
||||
lines.push(' return workflow')
|
||||
|
||||
// Close function
|
||||
lines.push('}')
|
||||
|
||||
// Add example if not a module
|
||||
if (!this.options.exportAsModule) {
|
||||
lines.push('')
|
||||
lines.push(`// Execute the workflow with sample input`)
|
||||
lines.push(`${this.options.mainFunctionName}({`)
|
||||
lines.push(` // Add your input here`)
|
||||
lines.push(`}).then(console.log).catch(console.error)`)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single block declaration
|
||||
*/
|
||||
private generateBlockDeclaration(block: Block): string {
|
||||
const varName = this.getBlockVariableName(block)
|
||||
const className = this.getBlockClassName(block)
|
||||
const blockData = JSON.stringify(block.data, null, 2)
|
||||
.replace(/"([^"]+)":/g, '$1:') // Convert "key": to key:
|
||||
.replace(/"/g, "'") // Convert double quotes to single quotes
|
||||
|
||||
let declaration = `const ${varName} = new ${className}(${blockData})`
|
||||
|
||||
// Add name and description if available
|
||||
if (block.metadata?.name) {
|
||||
declaration += `.setName('${block.metadata.name.replace(/'/g, "\\'")}')`
|
||||
}
|
||||
|
||||
if (block.metadata?.description) {
|
||||
declaration += `.setDescription('${block.metadata.description.replace(/'/g, "\\'")}')`
|
||||
}
|
||||
|
||||
return declaration + ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code for all connections
|
||||
*/
|
||||
private generateConnectionsCode(): string[] {
|
||||
return this.workflow.connections.map(conn => {
|
||||
let sourceId = conn.source
|
||||
let targetId = conn.target
|
||||
|
||||
// Try to use variable names when possible
|
||||
const sourceBlock = this.workflow.blocks.find(b => b.id === conn.source)
|
||||
const targetBlock = this.workflow.blocks.find(b => b.id === conn.target)
|
||||
|
||||
if (sourceBlock?.metadata?.id === 'starter') {
|
||||
sourceId = 'starterBlock.id'
|
||||
} else if (sourceBlock) {
|
||||
sourceId = `${this.getBlockVariableName(sourceBlock)}.id`
|
||||
}
|
||||
|
||||
if (targetBlock) {
|
||||
targetId = `${this.getBlockVariableName(targetBlock)}.id`
|
||||
}
|
||||
|
||||
let options = ''
|
||||
if (conn.sourceHandle || conn.targetHandle) {
|
||||
const optionsObj: Record<string, string> = {}
|
||||
if (conn.sourceHandle) optionsObj.sourceHandle = `'${conn.sourceHandle}'`
|
||||
if (conn.targetHandle) optionsObj.targetHandle = `'${conn.targetHandle}'`
|
||||
|
||||
options = `, { ${Object.entries(optionsObj).map(([k, v]) => `${k}: ${v}`).join(', ')} }`
|
||||
}
|
||||
|
||||
return `workflowBuilder.connect(${sourceId}, ${targetId}${options})`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a variable name for a block
|
||||
*/
|
||||
private getBlockVariableName(block: Block): string {
|
||||
if (block.metadata?.id === 'starter') {
|
||||
return 'starterBlock'
|
||||
}
|
||||
|
||||
// Use the block name if available
|
||||
if (block.metadata?.name) {
|
||||
return this.sanitizeName(block.metadata.name, true)
|
||||
}
|
||||
|
||||
// Fall back to block type + ID
|
||||
return `${block.type}Block_${block.id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name for a block
|
||||
*/
|
||||
private getBlockClassName(block: Block): string | null {
|
||||
const blockTypeId = block.metadata?.id || block.type
|
||||
|
||||
// Map block types to class names
|
||||
const blockTypeToClass: Record<string, string> = {
|
||||
'starter': 'StarterBlock',
|
||||
'agent': 'AgentBlock',
|
||||
'function': 'FunctionBlock',
|
||||
'condition': 'ConditionBlock',
|
||||
'router': 'RouterBlock',
|
||||
'api': 'ApiBlock',
|
||||
'evaluator': 'EvaluatorBlock',
|
||||
'openai': 'OpenAIBlock',
|
||||
'tavily': 'TavilyBlock',
|
||||
'slack': 'SlackBlock',
|
||||
'jina': 'JinaBlock',
|
||||
'serper': 'SerperBlock',
|
||||
'pinecone': 'PineconeBlock',
|
||||
'github': 'GitHubBlock',
|
||||
'gmail': 'GmailBlock',
|
||||
'notion': 'NotionBlock',
|
||||
'sheets': 'SheetsBlock',
|
||||
'drive': 'DriveBlock',
|
||||
'docs': 'DocsBlock',
|
||||
'x': 'XBlock'
|
||||
}
|
||||
|
||||
return blockTypeToClass[blockTypeId] || `${this.capitalize(blockTypeId)}Block`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sanitized name that can be used as a variable name
|
||||
*/
|
||||
private sanitizeName(name: string, camelCase: boolean = false): string {
|
||||
// Replace non-alphanumeric characters with underscores
|
||||
let sanitized = name.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
|
||||
// Ensure it starts with a letter
|
||||
if (!/^[a-zA-Z]/.test(sanitized)) {
|
||||
sanitized = 'block_' + sanitized
|
||||
}
|
||||
|
||||
// Convert to camelCase if requested
|
||||
if (camelCase) {
|
||||
sanitized = sanitized.replace(/_([a-zA-Z])/g, (_, char) => char.toUpperCase())
|
||||
sanitized = sanitized.charAt(0).toLowerCase() + sanitized.slice(1)
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
*/
|
||||
private capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to generate TypeScript code from a workflow
|
||||
*/
|
||||
export function generateCode(workflow: Workflow, options: CodeGenerationOptions = {}): string {
|
||||
const generator = new WorkflowCodeGenerator(workflow, options)
|
||||
return generator.generate()
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { SimStudioConfig, Workflow, ExecutionResult, DeploymentOptions, DeploymentResult, ScheduleOptions, ScheduleResult } from './types'
|
||||
import { WorkflowBuilder } from './workflows/builder'
|
||||
import { BlockRegistry, BlockImport } from './blocks/registry'
|
||||
import { Tool, ToolRegistry } from './tools'
|
||||
|
||||
/**
|
||||
* Main SDK class for Sim Studio
|
||||
*/
|
||||
export class SimStudio {
|
||||
private client: AxiosInstance
|
||||
private config: Required<SimStudioConfig>
|
||||
|
||||
/**
|
||||
* Create a new Sim Studio SDK instance
|
||||
*/
|
||||
constructor(config: SimStudioConfig = {}) {
|
||||
this.config = {
|
||||
apiKey: config.apiKey ?? process.env.SIM_STUDIO_API_KEY ?? '',
|
||||
baseUrl: config.baseUrl ?? process.env.SIM_STUDIO_API_URL ?? 'https://api.simstudio.dev',
|
||||
timeout: config.timeout ?? 30000,
|
||||
}
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.config.baseUrl,
|
||||
timeout: this.config.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new workflow builder
|
||||
*/
|
||||
createWorkflow(name: string, description?: string): WorkflowBuilder {
|
||||
return new WorkflowBuilder(name, description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a workflow by ID
|
||||
*/
|
||||
async getWorkflow(id: string): Promise<Workflow> {
|
||||
const response = await this.client.get(`/workflows/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* List all workflows
|
||||
*/
|
||||
async listWorkflows(params: { limit?: number; offset?: number } = {}): Promise<Workflow[]> {
|
||||
const response = await this.client.get('/workflows', { params })
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a workflow
|
||||
*/
|
||||
async saveWorkflow(workflow: Workflow): Promise<Workflow> {
|
||||
if (workflow.id) {
|
||||
const response = await this.client.put(`/workflows/${workflow.id}`, workflow)
|
||||
return response.data
|
||||
} else {
|
||||
const response = await this.client.post('/workflows', workflow)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a workflow
|
||||
*/
|
||||
async deleteWorkflow(id: string): Promise<void> {
|
||||
await this.client.delete(`/workflows/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a workflow
|
||||
*/
|
||||
async executeWorkflow(id: string, input?: Record<string, any>): Promise<ExecutionResult> {
|
||||
const response = await this.client.post(`/workflows/${id}/execute`, { input })
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a workflow as an API endpoint
|
||||
*/
|
||||
async deployWorkflow(id: string, options: DeploymentOptions = {}): Promise<DeploymentResult> {
|
||||
const response = await this.client.post(`/workflows/${id}/deploy`, options)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment details
|
||||
*/
|
||||
async getDeployment(workflowId: string): Promise<DeploymentResult> {
|
||||
const response = await this.client.get(`/workflows/${workflowId}/deployment`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a workflow deployment
|
||||
*/
|
||||
async undeployWorkflow(workflowId: string): Promise<void> {
|
||||
await this.client.delete(`/workflows/${workflowId}/deployment`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a workflow
|
||||
*/
|
||||
async scheduleWorkflow(workflowId: string, options: ScheduleOptions): Promise<ScheduleResult> {
|
||||
const response = await this.client.post(`/workflows/${workflowId}/schedule`, options)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a workflow schedule
|
||||
*/
|
||||
async getSchedule(scheduleId: string): Promise<ScheduleResult> {
|
||||
const response = await this.client.get(`/schedules/${scheduleId}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* List schedules for a workflow
|
||||
*/
|
||||
async listSchedules(workflowId: string): Promise<ScheduleResult[]> {
|
||||
const response = await this.client.get(`/workflows/${workflowId}/schedules`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a schedule
|
||||
*/
|
||||
async deleteSchedule(scheduleId: string): Promise<void> {
|
||||
await this.client.delete(`/schedules/${scheduleId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register blocks from the main application
|
||||
*/
|
||||
static registerBlocks(blocks: BlockImport[]): void {
|
||||
blocks.forEach(block => {
|
||||
BlockRegistry.register(block.id, block.blockClass, block.options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register tools from the main application
|
||||
*/
|
||||
static registerTools(tools: Tool[]): void {
|
||||
tools.forEach(tool => {
|
||||
ToolRegistry.register(tool)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered blocks
|
||||
*/
|
||||
static getRegisteredBlocks(): string[] {
|
||||
return BlockRegistry.getAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools
|
||||
*/
|
||||
static getRegisteredTools(): Tool[] {
|
||||
return ToolRegistry.getAll()
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Type declarations for the generated registry
|
||||
*/
|
||||
|
||||
/**
|
||||
* List of all available tools in the main application
|
||||
*/
|
||||
export declare const availableTools: string[]
|
||||
|
||||
/**
|
||||
* List of all available blocks in the main application
|
||||
*/
|
||||
export declare const availableBlocks: string[]
|
||||
|
||||
/**
|
||||
* Map of tool IDs to their block types
|
||||
*/
|
||||
export declare const toolToBlockMap: Record<string, string>
|
||||
|
||||
/**
|
||||
* Required parameters for each tool
|
||||
*/
|
||||
export declare const toolRequiredParameters: Record<string, string[]>
|
||||
|
||||
/**
|
||||
* Required parameters for each block
|
||||
*/
|
||||
export declare const blockRequiredParameters: Record<string, string[]>
|
||||
|
||||
/**
|
||||
* Check if a tool is available
|
||||
*/
|
||||
export declare function isToolAvailable(toolId: string): boolean
|
||||
|
||||
/**
|
||||
* Check if a block is available
|
||||
*/
|
||||
export declare function isBlockAvailable(blockId: string): boolean
|
||||
|
||||
/**
|
||||
* Get the block type for a tool
|
||||
*/
|
||||
export declare function getBlockTypeForTool(toolId: string): string | undefined
|
||||
|
||||
/**
|
||||
* Get required parameters for a tool
|
||||
*/
|
||||
export declare function getToolRequiredParameters(toolId: string): string[]
|
||||
|
||||
/**
|
||||
* Get required parameters for a block
|
||||
*/
|
||||
export declare function getBlockRequiredParameters(blockId: string): string[]
|
||||
@@ -0,0 +1 @@
|
||||
export * from './registry';
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* This file is auto-generated. Do not edit directly.
|
||||
* Generated at 2025-03-18T23:55:40.504Z
|
||||
*/
|
||||
|
||||
/**
|
||||
* List of all available tools in the main application
|
||||
*/
|
||||
export const availableTools = [
|
||||
"openai_embeddings",
|
||||
"http_request",
|
||||
"hubspot_contacts",
|
||||
"salesforce_opportunities",
|
||||
"function_execute",
|
||||
"webcontainer_execute",
|
||||
"vision_tool",
|
||||
"firecrawl_scrape",
|
||||
"jina_readurl",
|
||||
"slack_message",
|
||||
"github_repoinfo",
|
||||
"serper_search",
|
||||
"tavily_search",
|
||||
"tavily_extract",
|
||||
"supabase_query",
|
||||
"supabase_insert",
|
||||
"supabase_update",
|
||||
"youtube_search",
|
||||
"notion_read",
|
||||
"notion_write",
|
||||
"gmail_send",
|
||||
"gmail_read",
|
||||
"gmail_search",
|
||||
"whatsapp_send_message",
|
||||
"x_write",
|
||||
"x_read",
|
||||
"x_search",
|
||||
"x_user",
|
||||
"pinecone_fetch",
|
||||
"pinecone_generate_embeddings",
|
||||
"pinecone_search_text",
|
||||
"pinecone_search_vector",
|
||||
"pinecone_upsert_text",
|
||||
"github_pr",
|
||||
"github_comment",
|
||||
"exa_search",
|
||||
"exa_get_contents",
|
||||
"exa_find_similar_links",
|
||||
"exa_answer",
|
||||
"reddit_hot_posts",
|
||||
"google_drive_download",
|
||||
"google_drive_list",
|
||||
"google_drive_upload",
|
||||
"google_docs_read",
|
||||
"google_docs_write",
|
||||
"google_docs_create",
|
||||
"google_sheets_read",
|
||||
"google_sheets_write",
|
||||
"google_sheets_update",
|
||||
"guesty_reservation",
|
||||
"guesty_guest"
|
||||
]
|
||||
|
||||
/**
|
||||
* List of all available blocks in the main application
|
||||
*/
|
||||
export const availableBlocks = [
|
||||
"agent",
|
||||
"api",
|
||||
"condition",
|
||||
"evaluator",
|
||||
"exa",
|
||||
"firecrawl",
|
||||
"function",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_docs",
|
||||
"google_drive",
|
||||
"google_sheets",
|
||||
"jina",
|
||||
"notion",
|
||||
"openai",
|
||||
"pinecone",
|
||||
"reddit",
|
||||
"router",
|
||||
"serper",
|
||||
"slack",
|
||||
"starter",
|
||||
"supabase",
|
||||
"tavily",
|
||||
"translate",
|
||||
"vision",
|
||||
"whatsapp",
|
||||
"x",
|
||||
"youtube"
|
||||
]
|
||||
|
||||
/**
|
||||
* Map of tool IDs to their block types
|
||||
*/
|
||||
export const toolToBlockMap: Record<string, string> = {
|
||||
'openai_embeddings': 'OpenaiEmbeddingsBlock',
|
||||
'http_request': 'HttpRequestBlock',
|
||||
'hubspot_contacts': 'HubspotContactsBlock',
|
||||
'salesforce_opportunities': 'SalesforceOpportunitiesBlock',
|
||||
'function_execute': 'FunctionExecuteBlock',
|
||||
'webcontainer_execute': 'WebcontainerExecuteBlock',
|
||||
'vision_tool': 'VisionToolBlock',
|
||||
'firecrawl_scrape': 'FirecrawlScrapeBlock',
|
||||
'jina_readurl': 'JinaReadurlBlock',
|
||||
'slack_message': 'SlackMessageBlock',
|
||||
'github_repoinfo': 'GithubRepoinfoBlock',
|
||||
'serper_search': 'SerperSearchBlock',
|
||||
'tavily_search': 'TavilySearchBlock',
|
||||
'tavily_extract': 'TavilyExtractBlock',
|
||||
'supabase_query': 'SupabaseQueryBlock',
|
||||
'supabase_insert': 'SupabaseInsertBlock',
|
||||
'supabase_update': 'SupabaseUpdateBlock',
|
||||
'youtube_search': 'YoutubeSearchBlock',
|
||||
'notion_read': 'NotionReadBlock',
|
||||
'notion_write': 'NotionWriteBlock',
|
||||
'gmail_send': 'GmailSendBlock',
|
||||
'gmail_read': 'GmailReadBlock',
|
||||
'gmail_search': 'GmailSearchBlock',
|
||||
'whatsapp_send_message': 'WhatsappSendMessageBlock',
|
||||
'x_write': 'XWriteBlock',
|
||||
'x_read': 'XReadBlock',
|
||||
'x_search': 'XSearchBlock',
|
||||
'x_user': 'XUserBlock',
|
||||
'pinecone_fetch': 'PineconeFetchBlock',
|
||||
'pinecone_generate_embeddings': 'PineconeGenerateEmbeddingsBlock',
|
||||
'pinecone_search_text': 'PineconeSearchTextBlock',
|
||||
'pinecone_search_vector': 'PineconeSearchVectorBlock',
|
||||
'pinecone_upsert_text': 'PineconeUpsertTextBlock',
|
||||
'github_pr': 'GithubPrBlock',
|
||||
'github_comment': 'GithubCommentBlock',
|
||||
'exa_search': 'ExaSearchBlock',
|
||||
'exa_get_contents': 'ExaGetContentsBlock',
|
||||
'exa_find_similar_links': 'ExaFindSimilarLinksBlock',
|
||||
'exa_answer': 'ExaAnswerBlock',
|
||||
'reddit_hot_posts': 'RedditHotPostsBlock',
|
||||
'google_drive_download': 'GoogleDriveDownloadBlock',
|
||||
'google_drive_list': 'GoogleDriveListBlock',
|
||||
'google_drive_upload': 'GoogleDriveUploadBlock',
|
||||
'google_docs_read': 'GoogleDocsReadBlock',
|
||||
'google_docs_write': 'GoogleDocsWriteBlock',
|
||||
'google_docs_create': 'GoogleDocsCreateBlock',
|
||||
'google_sheets_read': 'GoogleSheetsReadBlock',
|
||||
'google_sheets_write': 'GoogleSheetsWriteBlock',
|
||||
'google_sheets_update': 'GoogleSheetsUpdateBlock',
|
||||
'guesty_reservation': 'GuestyReservationBlock',
|
||||
'guesty_guest': 'GuestyGuestBlock'
|
||||
}
|
||||
|
||||
/**
|
||||
* Required parameters for each tool
|
||||
*/
|
||||
export const toolRequiredParameters: Record<string, string[]> = {
|
||||
'openai_embeddings': [],
|
||||
'http_request': [],
|
||||
'hubspot_contacts': ["apiKey"],
|
||||
'salesforce_opportunities': ["apiKey"],
|
||||
'function_execute': [],
|
||||
'webcontainer_execute': [],
|
||||
'vision_tool': ["apiKey"],
|
||||
'firecrawl_scrape': ["apiKey"],
|
||||
'jina_readurl': ["apiKey"],
|
||||
'slack_message': ["apiKey"],
|
||||
'github_repoinfo': ["apiKey"],
|
||||
'serper_search': ["apiKey"],
|
||||
'tavily_search': ["apiKey"],
|
||||
'tavily_extract': ["apiKey"],
|
||||
'supabase_query': [],
|
||||
'supabase_insert': [],
|
||||
'supabase_update': [],
|
||||
'youtube_search': ["apiKey"],
|
||||
'notion_read': ["pageId","apiKey"],
|
||||
'notion_write': ["pageId","apiKey"],
|
||||
'gmail_send': [],
|
||||
'gmail_read': [],
|
||||
'gmail_search': [],
|
||||
'whatsapp_send_message': [],
|
||||
'x_write': ["apiKey"],
|
||||
'x_read': ["apiKey","tweetId"],
|
||||
'x_search': ["apiKey"],
|
||||
'x_user': ["apiKey"],
|
||||
'pinecone_fetch': ["apiKey","indexHost","ids"],
|
||||
'pinecone_generate_embeddings': ["apiKey"],
|
||||
'pinecone_search_text': ["apiKey","indexHost"],
|
||||
'pinecone_search_vector': ["apiKey","indexHost"],
|
||||
'pinecone_upsert_text': ["apiKey","indexHost"],
|
||||
'github_pr': ["apiKey"],
|
||||
'github_comment': ["apiKey"],
|
||||
'exa_search': ["apiKey"],
|
||||
'exa_get_contents': ["apiKey"],
|
||||
'exa_find_similar_links': ["apiKey"],
|
||||
'exa_answer': ["apiKey"],
|
||||
'reddit_hot_posts': [],
|
||||
'google_drive_download': [],
|
||||
'google_drive_list': [],
|
||||
'google_drive_upload': [],
|
||||
'google_docs_read': [],
|
||||
'google_docs_write': [],
|
||||
'google_docs_create': [],
|
||||
'google_sheets_read': [],
|
||||
'google_sheets_write': [],
|
||||
'google_sheets_update': [],
|
||||
'guesty_reservation': ["apiKey"],
|
||||
'guesty_guest': ["apiKey"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Required parameters for each block
|
||||
*/
|
||||
export const blockRequiredParameters: Record<string, string[]> = {
|
||||
'agent': ["model","apiKey"],
|
||||
'api': ["url","method"],
|
||||
'condition': [],
|
||||
'evaluator': ["metrics","model","apiKey","content"],
|
||||
'exa': ["operation","apiKey"],
|
||||
'firecrawl': ["apiKey","url"],
|
||||
'function': [],
|
||||
'github': ["operation","owner","repo","apiKey"],
|
||||
'gmail': ["operation","credential"],
|
||||
'google_docs': ["operation","credential"],
|
||||
'google_drive': ["operation","credential"],
|
||||
'google_sheets': ["operation","credential"],
|
||||
'jina': ["url","apiKey"],
|
||||
'notion': ["pageId","operation","apiKey"],
|
||||
'openai': ["input","apiKey"],
|
||||
'pinecone': ["operation","apiKey"],
|
||||
'reddit': ["subreddit"],
|
||||
'router': ["prompt","model","apiKey"],
|
||||
'serper': ["query","apiKey"],
|
||||
'slack': ["apiKey","channel","text"],
|
||||
'starter': [],
|
||||
'supabase': ["operation","credential","projectId","table"],
|
||||
'tavily': ["operation","apiKey"],
|
||||
'translate': ["context","targetLanguage","apiKey","systemPrompt"],
|
||||
'vision': ["apiKey","imageUrl"],
|
||||
'whatsapp': ["phoneNumber","message","phoneNumberId","accessToken"],
|
||||
'x': ["operation","apiKey"],
|
||||
'youtube': ["apiKey","query"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is available
|
||||
*/
|
||||
export function isToolAvailable(toolId: string): boolean {
|
||||
return availableTools.includes(toolId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is available
|
||||
*/
|
||||
export function isBlockAvailable(blockId: string): boolean {
|
||||
return availableBlocks.includes(blockId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block type for a tool
|
||||
*/
|
||||
export function getBlockTypeForTool(toolId: string): string | undefined {
|
||||
return toolToBlockMap[toolId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required parameters for a tool
|
||||
*/
|
||||
export function getToolRequiredParameters(toolId: string): string[] {
|
||||
return toolRequiredParameters[toolId] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required parameters for a block
|
||||
*/
|
||||
export function getBlockRequiredParameters(blockId: string): string[] {
|
||||
return blockRequiredParameters[blockId] || []
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export { SimStudio } from './core'
|
||||
export { WorkflowBuilder } from './workflows/builder'
|
||||
export { Block as BlockBase } from './blocks/base'
|
||||
export { AgentBlock } from './blocks/agent'
|
||||
export { FunctionBlock } from './blocks/function'
|
||||
export { ConditionBlock } from './blocks/condition'
|
||||
export { RouterBlock } from './blocks/router'
|
||||
export { ApiBlock } from './blocks/api'
|
||||
export { EvaluatorBlock } from './blocks/evaluator'
|
||||
export { StarterBlock } from './blocks/starter'
|
||||
export { WorkflowCodeGenerator } from './code-generator'
|
||||
|
||||
export * from './utils'
|
||||
export * from './types'
|
||||
export * from './blocks/registry'
|
||||
export * from './tools'
|
||||
@@ -0,0 +1,555 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as ts from 'typescript'
|
||||
import * as dotenv from 'dotenv'
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config()
|
||||
|
||||
console.log('SIM_APP_PATH:', process.env.SIM_APP_PATH)
|
||||
|
||||
/**
|
||||
* Script to synchronize tools and blocks from the main app to the SDK
|
||||
* This script should be run as part of the build process
|
||||
*/
|
||||
|
||||
// Paths to look for the main app files
|
||||
const POSSIBLE_MAIN_APP_PATHS = [
|
||||
// Path from environment variable (should be the most reliable)
|
||||
process.env.SIM_APP_PATH ? path.join(process.env.SIM_APP_PATH, 'app/tools') : '',
|
||||
process.env.SIM_APP_PATH ? path.join(process.env.SIM_APP_PATH, 'app/blocks') : '',
|
||||
|
||||
// Direct relative paths from the script's location
|
||||
'../../sim/app/tools',
|
||||
'../../sim/app/blocks',
|
||||
|
||||
// From project root
|
||||
'../sim/app/tools',
|
||||
'../sim/app/blocks',
|
||||
|
||||
// From monorepo root
|
||||
'../../sim/app/tools',
|
||||
'../../sim/app/blocks',
|
||||
|
||||
// Path based on common monorepo structure
|
||||
'../../../sim/app/tools',
|
||||
'../../../sim/app/blocks',
|
||||
].filter(Boolean)
|
||||
|
||||
const OUTPUT_REGISTRY_PATH = '../generated/registry.ts'
|
||||
|
||||
export async function syncFromMainApp() {
|
||||
try {
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.resolve(__dirname, '../generated')
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Find the directories
|
||||
const toolsDir = findDir('tools')
|
||||
const blocksDir = findDir('blocks')
|
||||
|
||||
console.log("Tools directory:", toolsDir)
|
||||
console.log("Blocks directory:", blocksDir)
|
||||
|
||||
// Extract tools and blocks
|
||||
const tools = toolsDir ? extractToolsFromDir(toolsDir) : new Map<string, string[]>()
|
||||
const blocks = blocksDir ? extractBlocksFromDir(blocksDir) : new Map<string, string[]>()
|
||||
|
||||
// Get the registry data
|
||||
const toolRegistry = toolsDir ? extractToolsRegistry(path.join(toolsDir, 'index.ts')) : []
|
||||
const blockRegistry = blocksDir ? extractBlocksRegistry(path.join(blocksDir, 'index.ts')) : []
|
||||
|
||||
// Combine the information
|
||||
const combinedTools = mergeToolsInfo(toolRegistry, tools)
|
||||
const combinedBlocks = mergeBlocksInfo(blockRegistry, blocks)
|
||||
|
||||
// Generate registry file
|
||||
generateRegistryFile(combinedTools, combinedBlocks)
|
||||
|
||||
console.log('Successfully synchronized tools and blocks from main app')
|
||||
if (!toolsDir) console.log('Warning: Tools directory not found')
|
||||
if (!blocksDir) console.log('Warning: Blocks directory not found')
|
||||
} catch (error) {
|
||||
console.error('Error synchronizing from main app:', error)
|
||||
// Generate empty registry if failed
|
||||
generateRegistryFile([], [])
|
||||
}
|
||||
}
|
||||
|
||||
function findDir(type: 'tools' | 'blocks'): string | null {
|
||||
const possiblePaths = POSSIBLE_MAIN_APP_PATHS.filter(p => p.includes(`/${type}`))
|
||||
|
||||
for (const possiblePath of possiblePaths) {
|
||||
const fullPath = path.resolve(__dirname, possiblePath)
|
||||
if (fs.existsSync(fullPath)) {
|
||||
console.log(`Found ${type} directory at: ${fullPath}`)
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`${type} directory not found in any of the possible locations`)
|
||||
return null
|
||||
}
|
||||
|
||||
function extractToolsRegistry(filePath: string): string[] {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.warn(`Registry file not found at ${filePath}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'index.ts',
|
||||
fileContent,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
// Find the tools registry object
|
||||
const tools: string[] = []
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
node.name.getText() === 'tools' &&
|
||||
node.initializer &&
|
||||
ts.isObjectLiteralExpression(node.initializer)
|
||||
) {
|
||||
// Extract tool IDs from the object properties
|
||||
for (const property of node.initializer.properties) {
|
||||
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
|
||||
tools.push(property.name.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
console.log(`Extracted ${tools.length} tools from registry`)
|
||||
return tools
|
||||
} catch (error) {
|
||||
console.error('Error extracting tools from registry:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function extractBlocksRegistry(filePath: string): string[] {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.warn(`Registry file not found at ${filePath}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'index.ts',
|
||||
fileContent,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
// Find the blocks registry object
|
||||
const blocks: string[] = []
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (
|
||||
ts.isVariableDeclaration(node) &&
|
||||
node.name.getText() === 'blocks' &&
|
||||
node.initializer &&
|
||||
ts.isObjectLiteralExpression(node.initializer)
|
||||
) {
|
||||
// Extract block IDs from the object properties
|
||||
for (const property of node.initializer.properties) {
|
||||
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
|
||||
blocks.push(property.name.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
console.log(`Extracted ${blocks.length} blocks from registry`)
|
||||
return blocks
|
||||
} catch (error) {
|
||||
console.error('Error extracting blocks from registry:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolsFromDir(dirPath: string): Map<string, string[]> {
|
||||
const toolParams = new Map<string, string[]>()
|
||||
|
||||
// Check subdirectories first
|
||||
const subdirs = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name)
|
||||
|
||||
console.log(`Found ${subdirs.length} subdirectories in tools directory`)
|
||||
|
||||
// Process each subdirectory
|
||||
for (const subdir of subdirs) {
|
||||
if (subdir === 'node_modules' || subdir === '__test-utils__') continue
|
||||
|
||||
const subdirPath = path.join(dirPath, subdir)
|
||||
|
||||
// Look for all .ts files (but not .test.ts)
|
||||
const files = fs.readdirSync(subdirPath)
|
||||
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts'))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(subdirPath, file)
|
||||
const extractedTools = extractToolDefinitionFromFile(filePath)
|
||||
|
||||
// Merge the results
|
||||
for (const [toolId, params] of extractedTools.entries()) {
|
||||
toolParams.set(toolId, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also process root .ts files
|
||||
const rootFiles = fs.readdirSync(dirPath)
|
||||
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts') && file !== 'index.ts')
|
||||
|
||||
for (const file of rootFiles) {
|
||||
const filePath = path.join(dirPath, file)
|
||||
const extractedTools = extractToolDefinitionFromFile(filePath)
|
||||
|
||||
// Merge the results
|
||||
for (const [toolId, params] of extractedTools.entries()) {
|
||||
toolParams.set(toolId, params)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found tool definitions with required parameters for: ${Array.from(toolParams.keys()).join(', ')}`)
|
||||
return toolParams
|
||||
}
|
||||
|
||||
function extractBlocksFromDir(dirPath: string): Map<string, string[]> {
|
||||
const blockParams = new Map<string, string[]>()
|
||||
|
||||
// Check for blocks subdirectory specifically
|
||||
const blocksDir = path.join(dirPath, 'blocks')
|
||||
if (fs.existsSync(blocksDir) && fs.statSync(blocksDir).isDirectory()) {
|
||||
console.log('Found blocks subdirectory')
|
||||
|
||||
// Look for all .ts files (but not .test.ts)
|
||||
const files = fs.readdirSync(blocksDir)
|
||||
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts'))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(blocksDir, file)
|
||||
const extractedBlocks = extractBlockDefinitionFromFile(filePath)
|
||||
|
||||
// Merge the results
|
||||
for (const [blockId, params] of extractedBlocks.entries()) {
|
||||
blockParams.set(blockId, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also process root .ts files
|
||||
const rootFiles = fs.readdirSync(dirPath)
|
||||
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts') && file !== 'index.ts')
|
||||
|
||||
for (const file of rootFiles) {
|
||||
const filePath = path.join(dirPath, file)
|
||||
const extractedBlocks = extractBlockDefinitionFromFile(filePath)
|
||||
|
||||
// Merge the results
|
||||
for (const [blockId, params] of extractedBlocks.entries()) {
|
||||
blockParams.set(blockId, params)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found block definitions with required parameters for: ${Array.from(blockParams.keys()).join(', ')}`)
|
||||
return blockParams
|
||||
}
|
||||
|
||||
function extractToolDefinitionFromFile(filePath: string): Map<string, string[]> {
|
||||
const toolParams = new Map<string, string[]>()
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(
|
||||
path.basename(filePath),
|
||||
fileContent,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
// Look for tool definitions (objects with id property)
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
for (const declaration of node.declarationList.declarations) {
|
||||
if (declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
|
||||
let toolId = ''
|
||||
|
||||
// Find the tool ID
|
||||
for (const prop of declaration.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
ts.isIdentifier(prop.name) &&
|
||||
prop.name.text === 'id' &&
|
||||
ts.isStringLiteral(prop.initializer)
|
||||
) {
|
||||
toolId = prop.initializer.text
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!toolId) continue
|
||||
|
||||
// Find required parameters
|
||||
const requiredParams: string[] = []
|
||||
|
||||
for (const prop of declaration.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
ts.isIdentifier(prop.name) &&
|
||||
(prop.name.text === 'params' || prop.name.text === 'parameters') &&
|
||||
ts.isObjectLiteralExpression(prop.initializer)
|
||||
) {
|
||||
// Examine each parameter
|
||||
for (const param of prop.initializer.properties) {
|
||||
if (ts.isPropertyAssignment(param) && ts.isIdentifier(param.name)) {
|
||||
const paramName = param.name.text
|
||||
|
||||
if (ts.isObjectLiteralExpression(param.initializer)) {
|
||||
// Look for requiredForToolCall: true
|
||||
for (const paramProp of param.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(paramProp) &&
|
||||
ts.isIdentifier(paramProp.name) &&
|
||||
paramProp.name.text === 'requiredForToolCall' &&
|
||||
(
|
||||
(paramProp.initializer.kind === ts.SyntaxKind.TrueKeyword) ||
|
||||
(ts.isStringLiteral(paramProp.initializer) && paramProp.initializer.text === 'true')
|
||||
)
|
||||
) {
|
||||
requiredParams.push(paramName)
|
||||
console.log(`Found requiredForToolCall parameter '${paramName}' for tool '${toolId}' in ${filePath}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredParams.length > 0) {
|
||||
toolParams.set(toolId, [...new Set(requiredParams)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return toolParams
|
||||
} catch (error) {
|
||||
console.error(`Error extracting tool definitions from ${filePath}:`, error)
|
||||
return toolParams
|
||||
}
|
||||
}
|
||||
|
||||
function extractBlockDefinitionFromFile(filePath: string): Map<string, string[]> {
|
||||
const blockParams = new Map<string, string[]>()
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(
|
||||
path.basename(filePath),
|
||||
fileContent,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
// Look for block definitions (objects with type property)
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
for (const declaration of node.declarationList.declarations) {
|
||||
if (declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
|
||||
let blockId = ''
|
||||
|
||||
// Find the block type
|
||||
for (const prop of declaration.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
ts.isIdentifier(prop.name) &&
|
||||
prop.name.text === 'type' &&
|
||||
ts.isStringLiteral(prop.initializer)
|
||||
) {
|
||||
blockId = prop.initializer.text
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockId) continue
|
||||
|
||||
// Find required parameters
|
||||
const requiredParams: string[] = []
|
||||
|
||||
for (const prop of declaration.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
ts.isIdentifier(prop.name) &&
|
||||
prop.name.text === 'inputs' &&
|
||||
ts.isObjectLiteralExpression(prop.initializer)
|
||||
) {
|
||||
// Examine each parameter
|
||||
for (const param of prop.initializer.properties) {
|
||||
if (ts.isPropertyAssignment(param) && ts.isIdentifier(param.name)) {
|
||||
const paramName = param.name.text
|
||||
|
||||
if (ts.isObjectLiteralExpression(param.initializer)) {
|
||||
// Look for required: true
|
||||
for (const paramProp of param.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(paramProp) &&
|
||||
ts.isIdentifier(paramProp.name) &&
|
||||
paramProp.name.text === 'required' &&
|
||||
(
|
||||
(paramProp.initializer.kind === ts.SyntaxKind.TrueKeyword) ||
|
||||
(ts.isStringLiteral(paramProp.initializer) && paramProp.initializer.text === 'true')
|
||||
)
|
||||
) {
|
||||
requiredParams.push(paramName)
|
||||
console.log(`Found required parameter '${paramName}' for block '${blockId}' in ${filePath}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredParams.length > 0) {
|
||||
blockParams.set(blockId, [...new Set(requiredParams)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return blockParams
|
||||
} catch (error) {
|
||||
console.error(`Error extracting block definitions from ${filePath}:`, error)
|
||||
return blockParams
|
||||
}
|
||||
}
|
||||
|
||||
function mergeToolsInfo(registry: string[], toolParams: Map<string, string[]>): { id: string, requiredParams: string[] }[] {
|
||||
return registry.map(id => ({
|
||||
id,
|
||||
requiredParams: toolParams.get(id) || []
|
||||
}))
|
||||
}
|
||||
|
||||
function mergeBlocksInfo(registry: string[], blockParams: Map<string, string[]>): { id: string, requiredParams: string[] }[] {
|
||||
return registry.map(id => ({
|
||||
id,
|
||||
requiredParams: blockParams.get(id) || []
|
||||
}))
|
||||
}
|
||||
|
||||
function generateRegistryFile(
|
||||
tools: { id: string, requiredParams: string[] }[],
|
||||
blocks: { id: string, requiredParams: string[] }[]
|
||||
) {
|
||||
const outputPath = path.resolve(__dirname, OUTPUT_REGISTRY_PATH)
|
||||
|
||||
const content = `/**
|
||||
* This file is auto-generated. Do not edit directly.
|
||||
* Generated at ${new Date().toISOString()}
|
||||
*/
|
||||
|
||||
/**
|
||||
* List of all available tools in the main application
|
||||
*/
|
||||
export const availableTools = ${JSON.stringify(tools.map(t => t.id), null, 2)}
|
||||
|
||||
/**
|
||||
* List of all available blocks in the main application
|
||||
*/
|
||||
export const availableBlocks = ${JSON.stringify(blocks.map(b => b.id), null, 2)}
|
||||
|
||||
/**
|
||||
* Map of tool IDs to their block types
|
||||
*/
|
||||
export const toolToBlockMap: Record<string, string> = {
|
||||
${tools.map(tool => {
|
||||
// Convert tool_name to ToolNameBlock format
|
||||
const blockName = tool.id.split('_')
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('') + 'Block'
|
||||
return ` '${tool.id}': '${blockName}'`
|
||||
}).join(',\n')}
|
||||
}
|
||||
|
||||
/**
|
||||
* Required parameters for each tool
|
||||
*/
|
||||
export const toolRequiredParameters: Record<string, string[]> = {
|
||||
${tools.map(tool => ` '${tool.id}': ${JSON.stringify(tool.requiredParams)}`).join(',\n')}
|
||||
}
|
||||
|
||||
/**
|
||||
* Required parameters for each block
|
||||
*/
|
||||
export const blockRequiredParameters: Record<string, string[]> = {
|
||||
${blocks.map(block => ` '${block.id}': ${JSON.stringify(block.requiredParams)}`).join(',\n')}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is available
|
||||
*/
|
||||
export function isToolAvailable(toolId: string): boolean {
|
||||
return availableTools.includes(toolId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block is available
|
||||
*/
|
||||
export function isBlockAvailable(blockId: string): boolean {
|
||||
return availableBlocks.includes(blockId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block type for a tool
|
||||
*/
|
||||
export function getBlockTypeForTool(toolId: string): string | undefined {
|
||||
return toolToBlockMap[toolId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required parameters for a tool
|
||||
*/
|
||||
export function getToolRequiredParameters(toolId: string): string[] {
|
||||
return toolRequiredParameters[toolId] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required parameters for a block
|
||||
*/
|
||||
export function getBlockRequiredParameters(blockId: string): string[] {
|
||||
return blockRequiredParameters[blockId] || []
|
||||
}
|
||||
`
|
||||
|
||||
fs.writeFileSync(outputPath, content)
|
||||
console.log(`Generated registry file at ${outputPath}`)
|
||||
}
|
||||
|
||||
// Run the sync if this file is executed directly
|
||||
if (require.main === module) {
|
||||
syncFromMainApp().catch(console.error)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Tool registry for Sim Studio SDK
|
||||
*/
|
||||
import { availableTools } from '../generated/index'
|
||||
|
||||
export interface Tool {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
schema: any
|
||||
parameters?: any
|
||||
requiredParameters?: string[] // Parameters that must be provided for the tool to function
|
||||
execute(params: any): Promise<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for all available tool types
|
||||
*/
|
||||
export class ToolRegistry {
|
||||
private static tools: Map<string, Tool> = new Map()
|
||||
|
||||
/**
|
||||
* Register a tool
|
||||
*/
|
||||
static register(tool: Tool): void {
|
||||
this.tools.set(tool.id, tool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tool by ID
|
||||
*/
|
||||
static get(id: string): Tool | undefined {
|
||||
return this.tools.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is registered
|
||||
*/
|
||||
static has(id: string): boolean {
|
||||
return this.tools.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools
|
||||
*/
|
||||
static getAll(): Tool[] {
|
||||
return Array.from(this.tools.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Import tools from the main application
|
||||
*/
|
||||
static importTools(tools: Tool[]): void {
|
||||
tools.forEach(tool => {
|
||||
this.register(tool)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is available in the main application
|
||||
*/
|
||||
static isAvailableInMainApp(id: string): boolean {
|
||||
return availableTools.includes(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available tool IDs from the main application
|
||||
*/
|
||||
static getAllAvailableTools(): string[] {
|
||||
return [...availableTools]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool definition
|
||||
*/
|
||||
export function createTool(tool: Tool): Tool {
|
||||
return tool
|
||||
}
|
||||
|
||||
export * from './registry'
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Tool } from './index'
|
||||
|
||||
/**
|
||||
* Interface for tool imports from the main application
|
||||
*/
|
||||
export interface ToolImport {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
schema: any
|
||||
execute: (params: any) => Promise<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dynamic adapter for importing tools from the main application
|
||||
* This serves as a bridge between the main app's tools and our SDK
|
||||
*/
|
||||
export function createToolAdapter(tool: ToolImport): Tool {
|
||||
return {
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
schema: tool.schema,
|
||||
execute: tool.execute
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool factory for a specific tool type
|
||||
*/
|
||||
export function createToolFactory<T extends Record<string, any>>(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
schema: any,
|
||||
executeFunction: (params: T) => Promise<any>
|
||||
): (params?: Partial<T>) => Tool {
|
||||
return (params: Partial<T> = {}) => ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
schema,
|
||||
execute: (inputParams: any) => executeFunction({ ...params, ...inputParams })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Core types for the Sim Studio SDK
|
||||
*/
|
||||
|
||||
// API Configuration
|
||||
export interface SimStudioConfig {
|
||||
apiKey?: string
|
||||
baseUrl?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
// Workflow definition types
|
||||
export interface Workflow {
|
||||
id?: string
|
||||
name: string
|
||||
description?: string
|
||||
blocks: Block[]
|
||||
connections: Connection[]
|
||||
loops?: Record<string, Loop>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
id: string
|
||||
type: BlockType
|
||||
data: BlockData
|
||||
position?: Position
|
||||
enabled?: boolean
|
||||
metadata?: {
|
||||
name?: string
|
||||
description?: string
|
||||
id?: string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
// Core block types
|
||||
export type CoreBlockType =
|
||||
| 'starter'
|
||||
| 'agent'
|
||||
| 'function'
|
||||
| 'condition'
|
||||
| 'router'
|
||||
| 'api'
|
||||
| 'evaluator'
|
||||
| 'generic'
|
||||
|
||||
// Custom block types can be any string
|
||||
export type BlockType = CoreBlockType | string
|
||||
|
||||
export interface BlockData {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
id?: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
}
|
||||
|
||||
export interface Loop {
|
||||
nodes: string[]
|
||||
maxIterations: number
|
||||
iterationVariable?: string
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
// Execution result types
|
||||
export interface ExecutionResult {
|
||||
success: boolean
|
||||
output: any
|
||||
error?: string
|
||||
logs?: ExecutionLog[]
|
||||
metadata?: {
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExecutionLog {
|
||||
blockId: string
|
||||
blockName?: string
|
||||
blockType?: string
|
||||
startedAt: string
|
||||
endedAt: string
|
||||
durationMs: number
|
||||
success: boolean
|
||||
output?: any
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Deployment types
|
||||
export interface DeploymentOptions {
|
||||
isPublic?: boolean
|
||||
authentication?: 'none' | 'api_key' | 'oauth'
|
||||
rateLimit?: number
|
||||
}
|
||||
|
||||
export interface DeploymentResult {
|
||||
id: string
|
||||
url: string
|
||||
status: 'active' | 'inactive'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// Schedule types
|
||||
export interface ScheduleOptions {
|
||||
cron: string
|
||||
timezone?: string
|
||||
input?: Record<string, any>
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface ScheduleResult {
|
||||
id: string
|
||||
workflowId: string
|
||||
cron: string
|
||||
nextRunAt: string
|
||||
status: 'active' | 'inactive'
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Utility functions for Sim Studio SDK
|
||||
*/
|
||||
|
||||
/**
|
||||
* Safely parse a JSON string
|
||||
*/
|
||||
export function safeJsonParse<T>(jsonString: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(jsonString) as T
|
||||
} catch (error) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a template string with variable interpolation
|
||||
*/
|
||||
export function template(str: string, variables: Record<string, any>): string {
|
||||
return str.replace(/\{\{([^}]+)\}\}/g, (_, path) => {
|
||||
const trimmedPath = path.trim()
|
||||
return getValueByPath(variables, trimmedPath)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a nested value from an object using dot notation
|
||||
*/
|
||||
export function getValueByPath(obj: any, path: string): any {
|
||||
const parts = path.split('.')
|
||||
let current = obj
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique ID
|
||||
*/
|
||||
export function generateId(prefix = ''): string {
|
||||
return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone an object
|
||||
*/
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(deepClone) as unknown as T
|
||||
}
|
||||
|
||||
const clone: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(obj as Record<string, any>)) {
|
||||
clone[key] = deepClone(value)
|
||||
}
|
||||
|
||||
return clone as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two objects are deeply equal
|
||||
*/
|
||||
export function deepEquals(a: any, b: any): boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const keysA = Object.keys(a)
|
||||
const keysB = Object.keys(b)
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return keysA.every(key => keysB.includes(key) && deepEquals(a[key], b[key]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date string in ISO format
|
||||
*/
|
||||
export function formatDate(date: Date = new Date()): string {
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert seconds to a human-readable duration
|
||||
*/
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) {
|
||||
return `${Math.round(seconds)}s`
|
||||
}
|
||||
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = Math.round(seconds % 60)
|
||||
return `${minutes}m ${remainingSeconds}s`
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const remainingMinutes = Math.floor((seconds % 3600) / 60)
|
||||
return `${hours}h ${remainingMinutes}m`
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Workflow, Connection, Loop, Block as BlockType } from '../types'
|
||||
import { Block } from '../blocks/base'
|
||||
import { StarterBlock } from '../blocks/starter'
|
||||
|
||||
/**
|
||||
* Builder class for creating workflows programmatically
|
||||
*/
|
||||
export class WorkflowBuilder {
|
||||
private workflow: Workflow
|
||||
private blockMap: Map<string, Block>
|
||||
|
||||
/**
|
||||
* Create a new workflow builder
|
||||
*/
|
||||
constructor(name: string, description?: string) {
|
||||
this.workflow = {
|
||||
name,
|
||||
description,
|
||||
blocks: [],
|
||||
connections: [],
|
||||
loops: {},
|
||||
}
|
||||
this.blockMap = new Map()
|
||||
|
||||
// Add a starter block by default
|
||||
const starterBlock = new StarterBlock()
|
||||
this.addBlock(starterBlock)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a block to the workflow
|
||||
*/
|
||||
addBlock(block: Block): this {
|
||||
this.blockMap.set(block.id, block)
|
||||
this.workflow.blocks.push(block.toJSON())
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect two blocks
|
||||
*/
|
||||
connect(sourceId: string, targetId: string, options: {
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
} = {}): this {
|
||||
const connection: Connection = {
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
...options,
|
||||
}
|
||||
this.workflow.connections.push(connection)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a loop with specific blocks
|
||||
*/
|
||||
createLoop(nodes: string[], maxIterations: number, iterationVariable?: string): this {
|
||||
const loopId = `loop_${Date.now()}`
|
||||
const loop: Loop = {
|
||||
nodes,
|
||||
maxIterations,
|
||||
...(iterationVariable ? { iterationVariable } : {})
|
||||
}
|
||||
this.workflow.loops = {
|
||||
...this.workflow.loops,
|
||||
[loopId]: loop
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Position a block at specific coordinates
|
||||
*/
|
||||
positionBlock(blockId: string, x: number, y: number): this {
|
||||
const block = this.workflow.blocks.find(b => b.id === blockId)
|
||||
if (block) {
|
||||
block.position = { x, y }
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workflow definition
|
||||
*/
|
||||
build(): Workflow {
|
||||
return this.workflow
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a block by name
|
||||
*/
|
||||
findBlockByName(name: string): BlockType | undefined {
|
||||
return this.workflow.blocks.find(block => block.metadata?.name === name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the starter block
|
||||
*/
|
||||
getStarterBlock(): BlockType {
|
||||
const starterBlock = this.workflow.blocks.find(block => block.metadata?.id === 'starter')
|
||||
if (!starterBlock) {
|
||||
throw new Error('Starter block not found in workflow')
|
||||
}
|
||||
return starterBlock
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { WorkflowBuilder } from './builder'
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"examples/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { apiKey } from '@/app/db/schema'
|
||||
import { db } from '@/db'
|
||||
|
||||
const logger = createLogger('ApiKeyRoute')
|
||||
|
||||
// DELETE /api/user/api-keys/[id] - Delete an API key
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Deleting API key: ${id}`)
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
const keyId = id
|
||||
|
||||
if (!keyId) {
|
||||
return NextResponse.json({ error: 'API key ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Delete the API key, ensuring it belongs to the current user
|
||||
const result = await db
|
||||
.delete(apiKey)
|
||||
.where(and(eq(apiKey.id, keyId), eq(apiKey.userId, userId)))
|
||||
.returning({ id: apiKey.id })
|
||||
|
||||
if (!result.length) {
|
||||
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete API key', { error })
|
||||
return NextResponse.json({ error: 'Failed to delete API key' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { apiKey } from '@/app/db/schema'
|
||||
import { db } from '@/db'
|
||||
|
||||
const logger = createLogger('ApiKeysRoute')
|
||||
|
||||
// GET /api/user/api-keys - Get all API keys for the current user
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Fetch all API keys for this user
|
||||
const keys = await db
|
||||
.select({
|
||||
id: apiKey.id,
|
||||
name: apiKey.name,
|
||||
key: apiKey.key,
|
||||
createdAt: apiKey.createdAt,
|
||||
lastUsed: apiKey.lastUsed,
|
||||
expiresAt: apiKey.expiresAt,
|
||||
})
|
||||
.from(apiKey)
|
||||
.where(eq(apiKey.userId, userId))
|
||||
.orderBy(apiKey.createdAt)
|
||||
|
||||
const maskedKeys = keys.map((key) => ({
|
||||
...key,
|
||||
key: key.key,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ keys: maskedKeys })
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch API keys', { error })
|
||||
return NextResponse.json({ error: 'Failed to fetch API keys' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/user/api-keys - Create a new API key
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the request
|
||||
const { name } = body
|
||||
if (!name || typeof name !== 'string') {
|
||||
return NextResponse.json({ error: 'Invalid request. Name is required.' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Generate an API key - we'll use a prefix to identify this as an API key (sim_)
|
||||
const keyValue = `sim_${nanoid(32)}`
|
||||
|
||||
// Insert the new API key
|
||||
const [newKey] = await db
|
||||
.insert(apiKey)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
userId,
|
||||
name,
|
||||
key: keyValue,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning({
|
||||
id: apiKey.id,
|
||||
name: apiKey.name,
|
||||
key: apiKey.key,
|
||||
createdAt: apiKey.createdAt,
|
||||
})
|
||||
|
||||
return NextResponse.json({ key: newKey })
|
||||
} catch (error) {
|
||||
logger.error('Failed to create API key', { error })
|
||||
return NextResponse.json({ error: 'Failed to create API key' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConditionalIcon } from '@/components/icons'
|
||||
import { CodeExecutionOutput } from '@/tools/function/execute'
|
||||
import { BlockConfig } from '../types'
|
||||
|
||||
interface ConditionBlockOutput {
|
||||
@@ -35,9 +34,7 @@ export const ConditionBlock: BlockConfig<ConditionBlockOutput> = {
|
||||
tools: {
|
||||
access: ['function_execute'],
|
||||
},
|
||||
inputs: {
|
||||
code: { type: 'string', required: true },
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "api_key" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"last_used" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"expires_at" timestamp,
|
||||
CONSTRAINT "api_key_key_unique" UNIQUE("key")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "api_key" ADD CONSTRAINT "api_key_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,897 @@
|
||||
{
|
||||
"id": "f71b2569-b14c-40aa-aae2-a65fd1c1978e",
|
||||
"prevId": "41e64bff-e76e-4af6-aebd-1d2dd3db72f9",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.api_key": {
|
||||
"name": "api_key",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_used": {
|
||||
"name": "last_used",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"api_key_user_id_user_id_fk": {
|
||||
"name": "api_key_user_id_user_id_fk",
|
||||
"tableFrom": "api_key",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"api_key_key_unique": {
|
||||
"name": "api_key_key_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["key"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.environment": {
|
||||
"name": "environment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"variables": {
|
||||
"name": "variables",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"environment_user_id_user_id_fk": {
|
||||
"name": "environment_user_id_user_id_fk",
|
||||
"tableFrom": "environment",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"environment_user_id_unique": {
|
||||
"name": "environment_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["user_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["token"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.settings": {
|
||||
"name": "settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"general": {
|
||||
"name": "general",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"settings_user_id_user_id_fk": {
|
||||
"name": "settings_user_id_user_id_fk",
|
||||
"tableFrom": "settings",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"settings_user_id_unique": {
|
||||
"name": "settings_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["user_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.waitlist": {
|
||||
"name": "waitlist",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"waitlist_email_unique": {
|
||||
"name": "waitlist_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.webhook": {
|
||||
"name": "webhook",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"provider_config": {
|
||||
"name": "provider_config",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"path_idx": {
|
||||
"name": "path_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "path",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"webhook_workflow_id_workflow_id_fk": {
|
||||
"name": "webhook_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "webhook",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow": {
|
||||
"name": "workflow",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'#3972F6'"
|
||||
},
|
||||
"last_synced": {
|
||||
"name": "last_synced",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_deployed": {
|
||||
"name": "is_deployed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"deployed_at": {
|
||||
"name": "deployed_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"api_key": {
|
||||
"name": "api_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"collaborators": {
|
||||
"name": "collaborators",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'[]'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_user_id_user_id_fk": {
|
||||
"name": "workflow_user_id_user_id_fk",
|
||||
"tableFrom": "workflow",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow_logs": {
|
||||
"name": "workflow_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"execution_id": {
|
||||
"name": "execution_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"level": {
|
||||
"name": "level",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trigger": {
|
||||
"name": "trigger",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_logs_workflow_id_workflow_id_fk": {
|
||||
"name": "workflow_logs_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "workflow_logs",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow_schedule": {
|
||||
"name": "workflow_schedule",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_run_at": {
|
||||
"name": "next_run_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_ran_at": {
|
||||
"name": "last_ran_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trigger_type": {
|
||||
"name": "trigger_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_schedule_workflow_id_workflow_id_fk": {
|
||||
"name": "workflow_schedule_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "workflow_schedule",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"workflow_schedule_workflow_id_unique": {
|
||||
"name": "workflow_schedule_workflow_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["workflow_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,13 @@
|
||||
"when": 1742197359430,
|
||||
"tag": "0016_cultured_butterfly",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1742342896119,
|
||||
"tag": "0017_curious_ink",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -145,3 +145,16 @@ export const webhook = pgTable(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const apiKey = pgTable('api_key', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
key: text('key').notNull().unique(),
|
||||
lastUsed: timestamp('last_used'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
expiresAt: timestamp('expires_at'),
|
||||
})
|
||||
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertCircle, Check, Copy, KeySquare, Plus, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const logger = createLogger('ApiKeys')
|
||||
|
||||
interface ApiKeysProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
interface ApiKey {
|
||||
id: string
|
||||
name: string
|
||||
key: string
|
||||
lastUsed?: string
|
||||
createdAt: string
|
||||
expiresAt?: string
|
||||
}
|
||||
|
||||
export function ApiKeys({ onOpenChange }: ApiKeysProps) {
|
||||
const { data: session } = useSession()
|
||||
const userId = session?.user?.id
|
||||
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [newKeyName, setNewKeyName] = useState('')
|
||||
const [newKey, setNewKey] = useState<ApiKey | null>(null)
|
||||
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
|
||||
const [deleteKey, setDeleteKey] = useState<ApiKey | null>(null)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [copySuccess, setCopySuccess] = useState(false)
|
||||
|
||||
// Fetch API keys
|
||||
const fetchApiKeys = async () => {
|
||||
if (!userId) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/user/api-keys')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setApiKeys(data.keys || [])
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching API keys:', { error })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new API key
|
||||
const handleCreateKey = async () => {
|
||||
if (!userId || !newKeyName.trim()) return
|
||||
|
||||
setIsCreating(true)
|
||||
try {
|
||||
const response = await fetch('/api/user/api-keys', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newKeyName.trim(),
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Show the new key dialog with the API key (only shown once)
|
||||
setNewKey(data.key)
|
||||
setShowNewKeyDialog(true)
|
||||
// Reset form
|
||||
setNewKeyName('')
|
||||
// Refresh the keys list
|
||||
fetchApiKeys()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error creating API key:', { error })
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete an API key
|
||||
const handleDeleteKey = async () => {
|
||||
if (!userId || !deleteKey) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/user/api-keys/${deleteKey.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh the keys list
|
||||
fetchApiKeys()
|
||||
// Close the dialog
|
||||
setShowDeleteDialog(false)
|
||||
setDeleteKey(null)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error deleting API key:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
// Copy API key to clipboard
|
||||
const copyToClipboard = (key: string) => {
|
||||
navigator.clipboard.writeText(key)
|
||||
setCopySuccess(true)
|
||||
setTimeout(() => setCopySuccess(false), 2000)
|
||||
}
|
||||
|
||||
// Load API keys on mount
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
fetchApiKeys()
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Never'
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">API Keys</h2>
|
||||
<Button
|
||||
onClick={() => setIsCreating(true)}
|
||||
disabled={isLoading}
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
API keys allow you to authenticate with the Sim SDK. Keep your API keys secure. They have
|
||||
access to your account and workflows.
|
||||
</p>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3 mt-6">
|
||||
<KeySkeleton />
|
||||
<KeySkeleton />
|
||||
</div>
|
||||
) : apiKeys.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-8 mt-6">
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
||||
<KeySquare className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-medium">No API keys yet</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground max-w-sm">
|
||||
You don't have any API keys yet. Create one to get started with the Sim SDK.
|
||||
</p>
|
||||
<Button
|
||||
variant="default"
|
||||
className="mt-4"
|
||||
onClick={() => setIsCreating(true)}
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1.5" /> Create API Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 mt-6">
|
||||
{apiKeys.map((key) => (
|
||||
<Card key={key.id} className="p-4 hover:shadow-sm transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-base">{key.name}</h3>
|
||||
<div className="flex items-center space-x-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Created: {formatDate(key.createdAt)} • Last used: {formatDate(key.lastUsed)}
|
||||
</p>
|
||||
<div className="text-xs px-1.5 py-0.5 bg-muted/50 rounded font-mono">
|
||||
•••••{key.key.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDeleteKey(key)
|
||||
setShowDeleteDialog(true)
|
||||
}}
|
||||
className="text-destructive hover:bg-destructive/10 h-8 w-8"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">Delete key</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create API Key Dialog */}
|
||||
<Dialog open={isCreating} onOpenChange={setIsCreating}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create new API key</DialogTitle>
|
||||
<DialogDescription>
|
||||
Name your API key to help you identify it later. This key will have access to your
|
||||
account and workflows.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="keyName">API Key Name</Label>
|
||||
<Input
|
||||
id="keyName"
|
||||
placeholder="e.g., Development, Production, etc."
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
className="focus-visible:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="sm:justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsCreating(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateKey} disabled={!newKeyName.trim()}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* New API Key Dialog */}
|
||||
<Dialog
|
||||
open={showNewKeyDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowNewKeyDialog(open)
|
||||
if (!open) setNewKey(null)
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Your API key has been created</DialogTitle>
|
||||
<DialogDescription>
|
||||
This is the only time you will see your API key. Copy it now and store it securely.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{newKey && (
|
||||
<div className="space-y-4 py-3">
|
||||
<div className="space-y-2">
|
||||
<Label>API Key</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
readOnly
|
||||
value={newKey.key}
|
||||
className="font-mono text-sm pr-10 bg-muted/50 border-slate-300"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
|
||||
onClick={() => copyToClipboard(newKey.key)}
|
||||
>
|
||||
{copySuccess ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy to clipboard</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
For security, we don't store the complete key. You won't be able to view
|
||||
it again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="sm:justify-end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowNewKeyDialog(false)
|
||||
setNewKey(null)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent className="sm:max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete API Key</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{deleteKey && (
|
||||
<>
|
||||
Are you sure you want to delete the API key{' '}
|
||||
<span className="font-semibold">{deleteKey.name}</span>? This action cannot be
|
||||
undone and any integrations using this key will no longer work.
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="sm:justify-end gap-2">
|
||||
<AlertDialogCancel onClick={() => setDeleteKey(null)}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteKey}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function KeySkeleton() {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-5 w-32 mb-2" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-8 rounded-md" />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
+9
-2
@@ -1,9 +1,11 @@
|
||||
import { Key, KeyRound, Settings, UserCircle } from 'lucide-react'
|
||||
import { Key, KeyRound, KeySquare, Settings, UserCircle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
activeSection: string
|
||||
onSectionChange: (section: 'general' | 'environment' | 'account' | 'credentials') => void
|
||||
onSectionChange: (
|
||||
section: 'general' | 'environment' | 'account' | 'credentials' | 'apikeys'
|
||||
) => void
|
||||
}
|
||||
|
||||
const navigationItems = [
|
||||
@@ -27,6 +29,11 @@ const navigationItems = [
|
||||
label: 'Credentials',
|
||||
icon: Key,
|
||||
},
|
||||
{
|
||||
id: 'apikeys',
|
||||
label: 'API Keys',
|
||||
icon: KeySquare,
|
||||
},
|
||||
] as const
|
||||
|
||||
export function SettingsNavigation({ activeSection, onSectionChange }: SettingsNavigationProps) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Account } from './components/account/account'
|
||||
import { ApiKeys } from './components/api-keys/api-keys'
|
||||
import { Credentials } from './components/credentials/credentials'
|
||||
import { EnvironmentVariables } from './components/environment/environment'
|
||||
import { General } from './components/general/general'
|
||||
@@ -16,7 +17,7 @@ interface SettingsModalProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
type SettingsSection = 'general' | 'environment' | 'account' | 'credentials'
|
||||
type SettingsSection = 'general' | 'environment' | 'account' | 'credentials' | 'apikeys'
|
||||
|
||||
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
|
||||
@@ -75,6 +76,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
<div className={cn('h-full', activeSection === 'credentials' ? 'block' : 'hidden')}>
|
||||
<Credentials onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
<div className={cn('h-full', activeSection === 'apikeys' ? 'block' : 'hidden')}>
|
||||
<ApiKeys onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user