feat: implement skills system for AI agents (#16865)
## Summary This PR introduces a Skills system for AI agents, inspired by the [Agent Skills specification](https://agentskills.io/specification). ## Changes ### Backend - **SkillEntity**: New database entity with migration for storing skills - **V2 Sync Mechanism**: Implemented FlatSkill, builders, validators, and action handlers following the v2 flat entity pattern - **Standard Skills**: Pre-defined skills (workflow-building, data-manipulation, dashboard-building, metadata-building, research, code-interpreter, xlsx, pdf, docx, pptx) - **GraphQL API**: CRUD operations for skills with proper guards and permissions - **Workspace Cache**: Integrated skills into the workspace cache system ### Frontend - **Skills Table**: Searchable table in AI settings showing all skills - **Skill Form**: Create/edit page with Label (primary), Description, and Content (markdown editor) - **API Name**: Following existing patterns, name is derived from label with advanced settings toggle for custom API names - **Standard vs Custom**: Standard skills are read-only, custom skills can be edited/deleted ## Key Design Decisions - Skills are stored in the database (Salesforce-like approach) rather than files - Name is derived from Label by default (isLabelSyncedWithName pattern) - Skills reference functions/files via @ mentions in markdown content rather than explicit relations - Standard skills are synced from code, custom skills are created via UI ## Screenshots Skills table and form UI follow existing settings patterns. ## Testing - [x] Lint passes - [x] Typecheck passes - [ ] CI tests
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
export type SkillDefinition = {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
content: string;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { SkillsService } from './skills.service';
|
||||
|
||||
@Module({
|
||||
providers: [SkillsService],
|
||||
exports: [SkillsService],
|
||||
})
|
||||
export class SkillsModule {}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
import { CODE_INTERPRETER_SKILL } from 'src/engine/core-modules/skills/skills/code-interpreter.skill';
|
||||
import { DASHBOARD_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/dashboard-building.skill';
|
||||
import { DATA_MANIPULATION_SKILL } from 'src/engine/core-modules/skills/skills/data-manipulation.skill';
|
||||
import { DOCX_SKILL } from 'src/engine/core-modules/skills/skills/docx.skill';
|
||||
import { METADATA_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/metadata-building.skill';
|
||||
import { PDF_SKILL } from 'src/engine/core-modules/skills/skills/pdf.skill';
|
||||
import { PPTX_SKILL } from 'src/engine/core-modules/skills/skills/pptx.skill';
|
||||
import { RESEARCH_SKILL } from 'src/engine/core-modules/skills/skills/research.skill';
|
||||
import { WORKFLOW_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/workflow-building.skill';
|
||||
import { XLSX_SKILL } from 'src/engine/core-modules/skills/skills/xlsx.skill';
|
||||
|
||||
const SKILL_DEFINITIONS: SkillDefinition[] = [
|
||||
WORKFLOW_BUILDING_SKILL,
|
||||
DATA_MANIPULATION_SKILL,
|
||||
DASHBOARD_BUILDING_SKILL,
|
||||
METADATA_BUILDING_SKILL,
|
||||
RESEARCH_SKILL,
|
||||
CODE_INTERPRETER_SKILL,
|
||||
XLSX_SKILL,
|
||||
PDF_SKILL,
|
||||
DOCX_SKILL,
|
||||
PPTX_SKILL,
|
||||
];
|
||||
|
||||
export type Skill = {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SkillsService {
|
||||
getAllSkills(): Skill[] {
|
||||
return SKILL_DEFINITIONS.map((skill) => ({
|
||||
name: skill.name,
|
||||
label: skill.label,
|
||||
description: skill.description,
|
||||
content: skill.content,
|
||||
}));
|
||||
}
|
||||
|
||||
getSkillByName(name: string): Skill | undefined {
|
||||
const skillDef = SKILL_DEFINITIONS.find((skill) => skill.name === name);
|
||||
|
||||
if (!skillDef) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: skillDef.name,
|
||||
label: skillDef.label,
|
||||
description: skillDef.description,
|
||||
content: skillDef.content,
|
||||
};
|
||||
}
|
||||
|
||||
getSkillsByNames(names: string[]): Skill[] {
|
||||
return names
|
||||
.map((name) => this.getSkillByName(name))
|
||||
.filter((skill): skill is Skill => skill !== undefined);
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const CODE_INTERPRETER_SKILL: SkillDefinition = {
|
||||
name: 'code-interpreter',
|
||||
label: 'Code Interpreter',
|
||||
description:
|
||||
'Python code execution for data analysis, complex multi-step operations, and efficient bulk processing via MCP bridge',
|
||||
content: `# Code Interpreter Skill
|
||||
|
||||
You have access to the \`code_interpreter\` tool to execute Python code in a sandboxed environment.
|
||||
|
||||
## How to Use
|
||||
Call the \`code_interpreter\` tool with your Python code. The tool will execute the code and return stdout, stderr, and any generated files.
|
||||
|
||||
## Capabilities
|
||||
- Analyze CSV, Excel, and JSON data files
|
||||
- Create charts and visualizations (matplotlib, seaborn)
|
||||
- Generate reports (PDF, PPTX, Excel)
|
||||
- Perform calculations and data transformations
|
||||
|
||||
## Pre-installed Libraries
|
||||
pandas, numpy, matplotlib, seaborn, scikit-learn, openpyxl, python-pptx
|
||||
|
||||
## Input Files
|
||||
- User-uploaded files are available at \`/home/user/{filename}\`
|
||||
- Always check the file exists before processing
|
||||
|
||||
## Output Files
|
||||
- Charts: Save to \`/home/user/output/\` directory - these are automatically returned as downloadable URLs
|
||||
- For matplotlib: \`plt.savefig('/home/user/output/chart.png')\`
|
||||
- Generated files: Save to \`/home/user/output/{filename}\`
|
||||
|
||||
## Example: Create a Bar Chart
|
||||
\`\`\`python
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
|
||||
# Data
|
||||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
|
||||
sales = [100, 150, 200, 175, 250, 300]
|
||||
|
||||
# Create chart
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.bar(months, sales, color='skyblue')
|
||||
plt.title('Monthly Sales')
|
||||
plt.xlabel('Month')
|
||||
plt.ylabel('Sales')
|
||||
plt.tight_layout()
|
||||
|
||||
# Save to output directory
|
||||
os.makedirs('/home/user/output', exist_ok=True)
|
||||
plt.savefig('/home/user/output/sales_chart.png')
|
||||
print('Chart saved!')
|
||||
\`\`\`
|
||||
|
||||
## Example: Analyze CSV
|
||||
\`\`\`python
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
|
||||
# Load data
|
||||
df = pd.read_csv('/home/user/data.csv')
|
||||
print(f"Loaded {len(df)} rows")
|
||||
|
||||
# Create visualization
|
||||
plt.figure(figsize=(10, 6))
|
||||
df.groupby('category')['value'].mean().plot(kind='bar')
|
||||
plt.title('Average Value by Category')
|
||||
plt.tight_layout()
|
||||
|
||||
os.makedirs('/home/user/output', exist_ok=True)
|
||||
plt.savefig('/home/user/output/analysis.png')
|
||||
print('Analysis complete!')
|
||||
\`\`\`
|
||||
|
||||
## Calling Twenty Tools from Python (MCP Bridge)
|
||||
|
||||
A \`twenty\` helper is automatically available in your code. Use it to call any Twenty tool directly from Python:
|
||||
|
||||
\`\`\`python
|
||||
# Find records
|
||||
people = twenty.call_tool('find_person_records', {'limit': 10})
|
||||
print(f"Found {len(people['edges'])} people")
|
||||
|
||||
# Create a record
|
||||
result = twenty.call_tool('create_company_record', {
|
||||
'data': {'name': 'Acme Corp', 'domainName': {'primaryLinkUrl': 'acme.com'}}
|
||||
})
|
||||
print(f"Created company: {result['id']}")
|
||||
|
||||
# Update a record
|
||||
twenty.call_tool('update_person_record', {
|
||||
'id': 'person-uuid',
|
||||
'data': {'jobTitle': 'CEO'}
|
||||
})
|
||||
|
||||
# List available tools
|
||||
tools = twenty.list_tools()
|
||||
for tool in tools:
|
||||
print(f"- {tool['name']}: {tool['description']}")
|
||||
\`\`\`
|
||||
|
||||
This allows you to orchestrate complex multi-step operations in a single code execution, which is more efficient than multiple tool calls.`,
|
||||
};
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const DASHBOARD_BUILDING_SKILL: SkillDefinition = {
|
||||
name: 'dashboard-building',
|
||||
label: 'Dashboard Building',
|
||||
description: 'Creating and managing dashboards with widgets and layouts',
|
||||
content: `# Dashboard Building Skill
|
||||
|
||||
You help users create and manage dashboards with widgets.
|
||||
|
||||
## CRITICAL: Creating GRAPH Widgets
|
||||
|
||||
Before creating any GRAPH widget, you MUST:
|
||||
1. Use list_object_metadata_items to get the objectMetadataId (e.g., for "opportunity", "company")
|
||||
2. From the response, get the field IDs you need (aggregateFieldMetadataId, primaryAxisGroupByFieldMetadataId)
|
||||
|
||||
GRAPH widgets require real UUIDs from the workspace metadata, NOT made-up values.
|
||||
|
||||
## Widget Configuration
|
||||
|
||||
### GRAPH - AGGREGATE (KPI numbers)
|
||||
Shows a single aggregated value (count, sum, average).
|
||||
Required:
|
||||
- objectMetadataId: UUID of the object (e.g., opportunity)
|
||||
- configuration.graphType: "AGGREGATE"
|
||||
- configuration.aggregateFieldMetadataId: UUID of field to aggregate
|
||||
- configuration.aggregateOperation: "COUNT", "SUM", "AVG", "MIN", "MAX"
|
||||
|
||||
### GRAPH - BAR/LINE Charts
|
||||
Shows data grouped by a dimension.
|
||||
Required:
|
||||
- objectMetadataId: UUID of the object
|
||||
- configuration.graphType: "VERTICAL_BAR", "HORIZONTAL_BAR", or "LINE"
|
||||
- configuration.aggregateFieldMetadataId: field to aggregate
|
||||
- configuration.aggregateOperation: aggregation type
|
||||
- configuration.primaryAxisGroupByFieldMetadataId: field to group by (x-axis)
|
||||
|
||||
### GRAPH - PIE Charts
|
||||
Shows data distribution as slices.
|
||||
Required:
|
||||
- objectMetadataId: UUID of the object
|
||||
- configuration.graphType: "PIE"
|
||||
- configuration.aggregateFieldMetadataId: field to aggregate
|
||||
- configuration.aggregateOperation: aggregation type
|
||||
- configuration.groupByFieldMetadataId: field to slice by
|
||||
|
||||
### IFRAME
|
||||
Embeds external content:
|
||||
- configuration.url: "https://..."
|
||||
|
||||
### STANDALONE_RICH_TEXT
|
||||
Text content widget:
|
||||
- configuration.body: "Your text here"
|
||||
|
||||
## Grid System
|
||||
|
||||
- 12 columns (0-11)
|
||||
- KPI widgets: rowSpan 2-4, columnSpan 3-4
|
||||
- Charts: rowSpan 6-8, columnSpan 6-12
|
||||
- Common layouts:
|
||||
- 4 KPIs in a row: each { columnSpan: 3 }
|
||||
- 2 charts side by side: each { columnSpan: 6 }
|
||||
- Full width chart: { column: 0, columnSpan: 12 }
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Ask user what data they want to visualize
|
||||
2. Load list_object_metadata_items to discover available objects and fields
|
||||
3. Create dashboard with appropriate widgets using real field IDs
|
||||
4. Use get_dashboard to verify creation
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Place KPIs at the top (row 0)
|
||||
- Group related charts together
|
||||
- Use consistent heights within rows
|
||||
- Start simple, add complexity as needed`,
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const DATA_MANIPULATION_SKILL: SkillDefinition = {
|
||||
name: 'data-manipulation',
|
||||
label: 'Data Manipulation',
|
||||
description:
|
||||
'Searching, filtering, creating, and updating records across all objects',
|
||||
content: `# Data Manipulation Skill
|
||||
|
||||
You explore and manage data across companies, people, opportunities, tasks, notes, and custom objects.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Search, filter, sort, create, update records
|
||||
- Manage relationships between records
|
||||
- Bulk operations and data analysis
|
||||
|
||||
## Constraints
|
||||
|
||||
- READ and WRITE access to all objects
|
||||
- CANNOT delete records or access workflow objects
|
||||
- CANNOT modify workspace settings
|
||||
|
||||
## Multi-step Approach
|
||||
|
||||
- Chain queries to solve complex requests (e.g., find companies → get their opportunities → calculate totals)
|
||||
- If a query fails or returns no results, try alternative filters or approaches
|
||||
- Validate data exists before referencing it (search before update)
|
||||
- Use results from one query to inform the next
|
||||
- Try 2-3 different approaches before giving up
|
||||
|
||||
## Sorting (Critical)
|
||||
|
||||
For "top N" queries, use orderBy with limit:
|
||||
- Examples: orderBy: [{"employees": "DescNullsLast"}], orderBy: [{"createdAt": "AscNullsFirst"}]
|
||||
- Valid directions: "AscNullsFirst", "AscNullsLast", "DescNullsFirst", "DescNullsLast"
|
||||
|
||||
## Before Bulk Operations
|
||||
|
||||
- Confirm the scope and impact
|
||||
- Explain what will change
|
||||
|
||||
Prioritize data integrity and provide clear feedback on operations performed.`,
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const DOCX_SKILL: SkillDefinition = {
|
||||
name: 'docx',
|
||||
label: 'Word Documents',
|
||||
description:
|
||||
'Word document creation, editing, template processing, and OOXML manipulation',
|
||||
content: `# Word Document Processing Skill
|
||||
|
||||
**IMPORTANT**: Save all output files to \`/home/user/output/\` for them to be downloadable.
|
||||
|
||||
## Pre-installed Scripts (OOXML Editing)
|
||||
|
||||
- \`python /home/user/scripts/docx/unpack.py <docx_file> <output_dir>\` - Unpack .docx to XML files for direct editing
|
||||
- \`python /home/user/scripts/docx/pack.py <input_dir> <docx_file>\` - Repack XML files into .docx
|
||||
- \`python /home/user/scripts/docx/validate.py <docx_file>\` - Validate document structure
|
||||
|
||||
### Validation Scripts
|
||||
- \`/home/user/scripts/docx/validation/docx.py\` - DOCX validation module
|
||||
- \`/home/user/scripts/docx/validation/redlining.py\` - Track changes/redline validation
|
||||
|
||||
## High-Level API (python-docx)
|
||||
|
||||
### Reading Documents
|
||||
|
||||
\`\`\`python
|
||||
from docx import Document
|
||||
|
||||
doc = Document('document.docx')
|
||||
|
||||
# Read paragraphs
|
||||
for para in doc.paragraphs:
|
||||
print(para.text)
|
||||
|
||||
# Read tables
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
print(cell.text)
|
||||
\`\`\`
|
||||
|
||||
### Creating Documents
|
||||
|
||||
\`\`\`python
|
||||
from docx import Document
|
||||
from docx.shared import Inches, Pt
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
doc = Document()
|
||||
|
||||
# Add heading
|
||||
doc.add_heading('Document Title', 0)
|
||||
|
||||
# Add paragraph with formatting
|
||||
para = doc.add_paragraph('Normal text. ')
|
||||
run = para.add_run('Bold text.')
|
||||
run.bold = True
|
||||
|
||||
# Add table
|
||||
table = doc.add_table(rows=2, cols=2)
|
||||
table.cell(0, 0).text = 'Header 1'
|
||||
table.cell(0, 1).text = 'Header 2'
|
||||
|
||||
# Add image
|
||||
doc.add_picture('image.png', width=Inches(4))
|
||||
|
||||
doc.save('/home/user/output/output.docx')
|
||||
\`\`\`
|
||||
|
||||
## Low-Level OOXML Editing
|
||||
|
||||
For complex edits (tracked changes, custom XML), use the unpack/edit/pack workflow:
|
||||
|
||||
### Step 1: Unpack
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/docx/unpack.py document.docx ./unpacked/
|
||||
\`\`\`
|
||||
|
||||
### Step 2: Edit XML directly
|
||||
\`\`\`python
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
tree = ET.parse('./unpacked/word/document.xml')
|
||||
root = tree.getroot()
|
||||
|
||||
# Edit XML...
|
||||
# Namespaces: w = http://schemas.openxmlformats.org/wordprocessingml/2006/main
|
||||
|
||||
tree.write('./unpacked/word/document.xml', xml_declaration=True, encoding='UTF-8')
|
||||
\`\`\`
|
||||
|
||||
### Step 3: Validate & Repack
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/docx/validate.py ./unpacked/
|
||||
python /home/user/scripts/docx/pack.py ./unpacked/ /home/user/output/output.docx
|
||||
\`\`\`
|
||||
|
||||
## Template Processing
|
||||
|
||||
### Find and Replace
|
||||
\`\`\`python
|
||||
from docx import Document
|
||||
|
||||
doc = Document('template.docx')
|
||||
|
||||
for para in doc.paragraphs:
|
||||
if '{{name}}' in para.text:
|
||||
para.text = para.text.replace('{{name}}', 'John Doe')
|
||||
|
||||
doc.save('/home/user/output/filled.docx')
|
||||
\`\`\`
|
||||
|
||||
### Preserve Formatting During Replace
|
||||
\`\`\`python
|
||||
def replace_in_paragraph(para, old_text, new_text):
|
||||
"""Replace text while preserving formatting"""
|
||||
for run in para.runs:
|
||||
if old_text in run.text:
|
||||
run.text = run.text.replace(old_text, new_text)
|
||||
|
||||
for para in doc.paragraphs:
|
||||
replace_in_paragraph(para, '{{name}}', 'John Doe')
|
||||
\`\`\`
|
||||
|
||||
## Working with Styles
|
||||
|
||||
\`\`\`python
|
||||
from docx.shared import Pt, RGBColor
|
||||
|
||||
# Set font
|
||||
run.font.name = 'Arial'
|
||||
run.font.size = Pt(12)
|
||||
run.font.color.rgb = RGBColor(0, 0, 0)
|
||||
|
||||
# Paragraph formatting
|
||||
para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
para.paragraph_format.space_before = Pt(12)
|
||||
para.paragraph_format.space_after = Pt(12)
|
||||
\`\`\`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Tool | Example |
|
||||
|------|------|---------|
|
||||
| Read document | python-docx | \`Document('file.docx')\` |
|
||||
| Create document | python-docx | \`Document()\` |
|
||||
| Add heading | python-docx | \`doc.add_heading('Title', 0)\` |
|
||||
| Add table | python-docx | \`doc.add_table(rows=2, cols=2)\` |
|
||||
| Unpack for editing | script | \`python unpack.py doc.docx ./out/\` |
|
||||
| Repack | script | \`python pack.py ./out/ doc.docx\` |
|
||||
| Validate | script | \`python validate.py doc.docx\` |`,
|
||||
};
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const METADATA_BUILDING_SKILL: SkillDefinition = {
|
||||
name: 'metadata-building',
|
||||
label: 'Metadata Building',
|
||||
description:
|
||||
'Managing the data model: creating objects, fields, and relations',
|
||||
content: `# Metadata Building Skill
|
||||
|
||||
You help users manage their workspace data model by creating, updating, and organizing custom objects and fields.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Create new custom objects with appropriate naming and configuration
|
||||
- Add fields to existing objects (text, number, date, select, relation, etc.)
|
||||
- Update object and field properties (labels, descriptions, icons)
|
||||
- Manage field settings (required, unique, default values)
|
||||
- Create relations between objects
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Objects**: Represent entities in the data model (e.g., Company, Person, Opportunity)
|
||||
- **Fields**: Properties of objects with specific types (TEXT, NUMBER, DATE_TIME, SELECT, RELATION, etc.)
|
||||
- **Relations**: Links between objects (one-to-many, many-to-one)
|
||||
- **Labels vs Names**: Labels are for display, names are internal identifiers (camelCase)
|
||||
|
||||
## Field Types Available
|
||||
|
||||
- **TEXT**: Simple text fields
|
||||
- **NUMBER**: Numeric values (integers or decimals)
|
||||
- **BOOLEAN**: True/false values
|
||||
- **DATE_TIME**: Date and time values
|
||||
- **DATE**: Date only values
|
||||
- **SELECT**: Single choice from options
|
||||
- **MULTI_SELECT**: Multiple choices from options
|
||||
- **LINK**: URL fields
|
||||
- **LINKS**: Multiple URL fields
|
||||
- **EMAIL**: Email address fields
|
||||
- **EMAILS**: Multiple email fields
|
||||
- **PHONE**: Phone number fields
|
||||
- **PHONES**: Multiple phone fields
|
||||
- **CURRENCY**: Monetary values
|
||||
- **RATING**: Star ratings
|
||||
- **RELATION**: Links to other objects
|
||||
- **RICH_TEXT**: Formatted text content
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use clear, descriptive names for objects and fields
|
||||
- Follow naming conventions: singular for object names, camelCase for field names
|
||||
- Add helpful descriptions to objects and fields
|
||||
- Choose appropriate field types for the data being stored
|
||||
- Consider relationships between objects when designing the data model
|
||||
|
||||
## Approach
|
||||
|
||||
- Ask clarifying questions to understand the user's data modeling needs
|
||||
- Suggest best practices for naming and organization
|
||||
- Explain the impact of changes to the data model
|
||||
- Verify object and field existence before making updates
|
||||
- Provide clear feedback on operations performed
|
||||
|
||||
Prioritize data model integrity and user understanding.`,
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const PDF_SKILL: SkillDefinition = {
|
||||
name: 'pdf',
|
||||
label: 'PDF Processing',
|
||||
description:
|
||||
'PDF form filling, field extraction, table parsing, and validation',
|
||||
content: `# PDF Processing Skill
|
||||
|
||||
**IMPORTANT**: Save all output files to \`/home/user/output/\` for them to be downloadable.
|
||||
|
||||
## Pre-installed Scripts
|
||||
|
||||
### Field Extraction
|
||||
- \`python /home/user/scripts/pdf/extract_form_field_info.py <pdf_file>\` - Extract all fillable field names and types (JSON output)
|
||||
- \`python /home/user/scripts/pdf/check_fillable_fields.py <pdf_file>\` - Check if PDF has fillable fields
|
||||
|
||||
### Form Filling
|
||||
- \`python /home/user/scripts/pdf/fill_fillable_fields.py <pdf_file> <json_data> <output_file>\` - Fill PDF form fields
|
||||
- \`python /home/user/scripts/pdf/fill_pdf_form_with_annotations.py <pdf_file> <json_data> <output_file>\` - Fill with annotation support
|
||||
|
||||
### Validation
|
||||
- \`python /home/user/scripts/pdf/create_validation_image.py <pdf_file>\` - Create validation image of filled PDF
|
||||
- \`python /home/user/scripts/pdf/check_bounding_boxes.py <pdf_file>\` - Check field boundaries
|
||||
- \`python /home/user/scripts/pdf/convert_pdf_to_images.py <pdf_file>\` - Convert PDF pages to images
|
||||
|
||||
## Reading PDFs
|
||||
|
||||
\`\`\`python
|
||||
import fitz # PyMuPDF
|
||||
|
||||
# Open PDF
|
||||
doc = fitz.open('document.pdf')
|
||||
|
||||
# Extract text from all pages
|
||||
for page in doc:
|
||||
text = page.get_text()
|
||||
print(text)
|
||||
|
||||
# Extract text from specific page
|
||||
page = doc[0] # First page
|
||||
text = page.get_text()
|
||||
\`\`\`
|
||||
|
||||
## Extracting Tables
|
||||
|
||||
\`\`\`python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open('document.pdf') as pdf:
|
||||
for page in pdf.pages:
|
||||
tables = page.extract_tables()
|
||||
for table in tables:
|
||||
for row in table:
|
||||
print(row)
|
||||
\`\`\`
|
||||
|
||||
## Filling PDF Forms
|
||||
|
||||
### Step 1: Extract field information
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pdf/extract_form_field_info.py form.pdf > fields.json
|
||||
\`\`\`
|
||||
|
||||
### Step 2: Create fill data JSON
|
||||
\`\`\`json
|
||||
{
|
||||
"field_name_1": "value1",
|
||||
"field_name_2": "value2",
|
||||
"checkbox_field": true
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Step 3: Fill the form
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pdf/fill_fillable_fields.py form.pdf fill_data.json /home/user/output/output.pdf
|
||||
\`\`\`
|
||||
|
||||
### Step 4: Validate the output
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pdf/create_validation_image.py /home/user/output/output.pdf
|
||||
\`\`\`
|
||||
|
||||
## Creating PDFs
|
||||
|
||||
\`\`\`python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas('/home/user/output/output.pdf', pagesize=letter)
|
||||
c.drawString(100, 750, 'Hello World!')
|
||||
c.save()
|
||||
\`\`\`
|
||||
|
||||
## Merging PDFs
|
||||
|
||||
\`\`\`python
|
||||
from PyPDF2 import PdfMerger
|
||||
|
||||
merger = PdfMerger()
|
||||
merger.append('file1.pdf')
|
||||
merger.append('file2.pdf')
|
||||
merger.write('/home/user/output/merged.pdf')
|
||||
merger.close()
|
||||
\`\`\`
|
||||
|
||||
## Splitting PDFs
|
||||
|
||||
\`\`\`python
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
|
||||
reader = PdfReader('document.pdf')
|
||||
|
||||
# Extract specific pages
|
||||
writer = PdfWriter()
|
||||
writer.add_page(reader.pages[0]) # First page
|
||||
writer.write('/home/user/output/page1.pdf')
|
||||
\`\`\`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Tool | Command/Example |
|
||||
|------|------|-----------------|
|
||||
| Extract text | PyMuPDF | \`page.get_text()\` |
|
||||
| Extract tables | pdfplumber | \`page.extract_tables()\` |
|
||||
| List form fields | script | \`python extract_form_field_info.py form.pdf\` |
|
||||
| Fill form | script | \`python fill_fillable_fields.py form.pdf data.json out.pdf\` |
|
||||
| Validate fill | script | \`python create_validation_image.py filled.pdf\` |
|
||||
| Create PDF | reportlab | \`canvas.Canvas('out.pdf')\` |
|
||||
| Merge PDFs | PyPDF2 | \`PdfMerger()\` |`,
|
||||
};
|
||||
@@ -1,170 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const PPTX_SKILL: SkillDefinition = {
|
||||
name: 'pptx',
|
||||
label: 'PowerPoint',
|
||||
description:
|
||||
'PowerPoint creation, editing, templates, thumbnails, and slide manipulation',
|
||||
content: `# PowerPoint Processing Skill
|
||||
|
||||
**IMPORTANT**: Save all output files to \`/home/user/output/\` for them to be downloadable.
|
||||
|
||||
## Pre-installed Scripts
|
||||
|
||||
- \`python /home/user/scripts/pptx/thumbnail.py <pptx_file> [output_dir]\` - Generate slide thumbnails
|
||||
- \`python /home/user/scripts/pptx/rearrange.py <pptx_file> <slide_order_json> <output_file>\` - Reorder slides
|
||||
- \`python /home/user/scripts/pptx/inventory.py <pptx_file>\` - List all slides and their content
|
||||
- \`python /home/user/scripts/pptx/replace.py <pptx_file> <replacements_json> <output_file>\` - Find/replace text
|
||||
|
||||
## Reading Presentations
|
||||
|
||||
\`\`\`python
|
||||
from pptx import Presentation
|
||||
|
||||
prs = Presentation('presentation.pptx')
|
||||
|
||||
# Iterate through slides
|
||||
for slide in prs.slides:
|
||||
for shape in slide.shapes:
|
||||
if shape.has_text_frame:
|
||||
print(shape.text)
|
||||
\`\`\`
|
||||
|
||||
## Creating Presentations
|
||||
|
||||
\`\`\`python
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt
|
||||
|
||||
prs = Presentation()
|
||||
|
||||
# Add title slide
|
||||
slide_layout = prs.slide_layouts[0] # Title layout
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
title = slide.shapes.title
|
||||
subtitle = slide.placeholders[1]
|
||||
|
||||
title.text = "Presentation Title"
|
||||
subtitle.text = "Subtitle goes here"
|
||||
|
||||
# Add content slide
|
||||
slide_layout = prs.slide_layouts[1] # Title and content
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
title = slide.shapes.title
|
||||
body = slide.placeholders[1]
|
||||
|
||||
title.text = "Slide Title"
|
||||
tf = body.text_frame
|
||||
tf.text = "First bullet"
|
||||
p = tf.add_paragraph()
|
||||
p.text = "Second bullet"
|
||||
p.level = 1
|
||||
|
||||
prs.save('/home/user/output/output.pptx')
|
||||
\`\`\`
|
||||
|
||||
## Adding Images
|
||||
|
||||
\`\`\`python
|
||||
from pptx.util import Inches
|
||||
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
||||
slide.shapes.add_picture(
|
||||
'image.png',
|
||||
left=Inches(1),
|
||||
top=Inches(1),
|
||||
width=Inches(5)
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
## Adding Tables
|
||||
|
||||
\`\`\`python
|
||||
from pptx.util import Inches
|
||||
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
table = slide.shapes.add_table(
|
||||
rows=3, cols=3,
|
||||
left=Inches(1), top=Inches(1),
|
||||
width=Inches(8), height=Inches(2)
|
||||
).table
|
||||
|
||||
# Set cell values
|
||||
table.cell(0, 0).text = "Header 1"
|
||||
table.cell(0, 1).text = "Header 2"
|
||||
table.cell(1, 0).text = "Data 1"
|
||||
\`\`\`
|
||||
|
||||
## Adding Charts
|
||||
|
||||
\`\`\`python
|
||||
from pptx.chart.data import CategoryChartData
|
||||
from pptx.enum.chart import XL_CHART_TYPE
|
||||
from pptx.util import Inches
|
||||
|
||||
chart_data = CategoryChartData()
|
||||
chart_data.categories = ['East', 'West', 'Midwest']
|
||||
chart_data.add_series('Series 1', (19.2, 21.4, 16.7))
|
||||
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
chart = slide.shapes.add_chart(
|
||||
XL_CHART_TYPE.COLUMN_CLUSTERED,
|
||||
Inches(1), Inches(1), Inches(8), Inches(5),
|
||||
chart_data
|
||||
).chart
|
||||
\`\`\`
|
||||
|
||||
## Using Scripts
|
||||
|
||||
### Generate Thumbnails
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pptx/thumbnail.py presentation.pptx ./thumbnails/
|
||||
# Creates: thumbnails/slide_1.png, slide_2.png, etc.
|
||||
\`\`\`
|
||||
|
||||
### Get Slide Inventory
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pptx/inventory.py presentation.pptx
|
||||
# Returns JSON with all slide content and shapes
|
||||
\`\`\`
|
||||
|
||||
### Reorder Slides
|
||||
\`\`\`bash
|
||||
# Order: [3, 1, 2] means slide 3 becomes first, slide 1 second, etc.
|
||||
python /home/user/scripts/pptx/rearrange.py input.pptx '[3, 1, 2]' output.pptx
|
||||
\`\`\`
|
||||
|
||||
### Find and Replace Text
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pptx/replace.py input.pptx '{"{{company}}": "Acme Corp", "{{date}}": "2024"}' output.pptx
|
||||
\`\`\`
|
||||
|
||||
## Template Processing Workflow
|
||||
|
||||
1. **Generate thumbnails** to understand slide structure:
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pptx/thumbnail.py template.pptx ./preview/
|
||||
\`\`\`
|
||||
|
||||
2. **Get inventory** to find placeholder text:
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pptx/inventory.py template.pptx
|
||||
\`\`\`
|
||||
|
||||
3. **Replace placeholders**:
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/pptx/replace.py template.pptx '{"{{title}}": "Q4 Report"}' output.pptx
|
||||
\`\`\`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Tool | Example |
|
||||
|------|------|---------|
|
||||
| Read presentation | python-pptx | \`Presentation('file.pptx')\` |
|
||||
| Create presentation | python-pptx | \`Presentation()\` |
|
||||
| Add slide | python-pptx | \`prs.slides.add_slide(layout)\` |
|
||||
| Generate thumbnails | script | \`python thumbnail.py pres.pptx ./out/\` |
|
||||
| Get slide inventory | script | \`python inventory.py pres.pptx\` |
|
||||
| Reorder slides | script | \`python rearrange.py pres.pptx '[2,1,3]' out.pptx\` |
|
||||
| Find/replace | script | \`python replace.py pres.pptx '{...}' out.pptx\` |`,
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const RESEARCH_SKILL: SkillDefinition = {
|
||||
name: 'research',
|
||||
label: 'Research',
|
||||
description: 'Finding information and gathering facts from the web',
|
||||
content: `# Research Skill
|
||||
|
||||
You find information and gather facts from the web.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Search for current information and facts
|
||||
- Research companies, people, technologies, trends
|
||||
- Gather competitive intelligence and market data
|
||||
- Find contact details and verify information
|
||||
|
||||
## Research Strategy
|
||||
|
||||
- Try multiple search queries from different angles
|
||||
- If initial searches fail, use alternative search terms
|
||||
- Cross-reference information when possible
|
||||
- Cite sources and provide context
|
||||
|
||||
## Present Findings
|
||||
|
||||
- Be thorough but concise
|
||||
- Organize information logically
|
||||
- Distinguish facts from speculation
|
||||
- Note if information might be outdated
|
||||
- Include relevant sources
|
||||
|
||||
Be persistent in finding accurate information.`,
|
||||
};
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const WORKFLOW_BUILDING_SKILL: SkillDefinition = {
|
||||
name: 'workflow-building',
|
||||
label: 'Workflow Building',
|
||||
description:
|
||||
'Creating and managing automation workflows with triggers and steps',
|
||||
content: `# Workflow Building Skill
|
||||
|
||||
You help users create and manage automation workflows.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Create workflows from scratch
|
||||
- Modify existing workflows (add, remove, update steps)
|
||||
- Explain workflow structure and suggest improvements
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Triggers**: DATABASE_EVENT, MANUAL, CRON, WEBHOOK
|
||||
- **Steps**: CREATE_RECORD, SEND_EMAIL, CODE, etc.
|
||||
- **Data flow**: Use {{stepId.fieldName}} to reference previous step outputs
|
||||
- **Relationships**: Use nested objects like {"company": {"id": "{{reference}}"}}
|
||||
|
||||
## CRON Trigger Settings Schema
|
||||
|
||||
For CRON triggers, settings.type must be one of these exact values:
|
||||
|
||||
1. **DAYS** - Daily schedule
|
||||
- Requires: schedule: { day: number (1+), hour: number (0-23), minute: number (0-59) }
|
||||
- Example: { type: "DAYS", schedule: { day: 1, hour: 9, minute: 0 }, outputSchema: {} }
|
||||
|
||||
2. **HOURS** - Hourly schedule (USE THIS FOR "EVERY HOUR")
|
||||
- Requires: schedule: { hour: number (1+), minute: number (0-59) }
|
||||
- Example: { type: "HOURS", schedule: { hour: 1, minute: 0 }, outputSchema: {} }
|
||||
- This runs every X hours at Y minutes past the hour
|
||||
|
||||
3. **MINUTES** - Minute-based schedule
|
||||
- Requires: schedule: { minute: number (1+) }
|
||||
- Example: { type: "MINUTES", schedule: { minute: 15 }, outputSchema: {} }
|
||||
|
||||
4. **CUSTOM** - Custom cron pattern
|
||||
- Requires: pattern: string (cron expression)
|
||||
- Example: { type: "CUSTOM", pattern: "0 * * * *", outputSchema: {} }
|
||||
|
||||
## Critical Notes
|
||||
|
||||
Always rely on tool schema definitions:
|
||||
- The workflow creation tool provides comprehensive schemas with examples
|
||||
- Follow schema definitions exactly for field names, types, and structures
|
||||
- Schema includes validation rules and common patterns
|
||||
|
||||
## Approach
|
||||
|
||||
- Ask clarifying questions to understand user needs
|
||||
- Suggest appropriate actions for the use case
|
||||
- Explain each step and why it's needed
|
||||
- For modifications, understand current structure first
|
||||
- Ensure workflow logic remains coherent
|
||||
|
||||
Prioritize user understanding and workflow effectiveness.`,
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
|
||||
|
||||
export const XLSX_SKILL: SkillDefinition = {
|
||||
name: 'xlsx',
|
||||
label: 'Excel & Spreadsheets',
|
||||
description:
|
||||
'Excel/spreadsheet creation, editing, and analysis with formulas, formatting, and visualization',
|
||||
content: `# Excel Processing Skill
|
||||
|
||||
**IMPORTANT**: Save all output files to \`/home/user/output/\` for them to be downloadable.
|
||||
|
||||
## Pre-installed Scripts
|
||||
|
||||
- \`python /home/user/scripts/xlsx/recalc.py <excel_file> [timeout]\` - Recalculate formulas using LibreOffice
|
||||
|
||||
## Requirements
|
||||
|
||||
### Zero Formula Errors
|
||||
Every Excel model MUST be delivered with ZERO formula errors (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?)
|
||||
|
||||
### Use Formulas, Not Hardcoded Values
|
||||
**Always use Excel formulas instead of calculating values in Python and hardcoding them.**
|
||||
|
||||
\`\`\`python
|
||||
# ❌ WRONG - Hardcoding
|
||||
total = df['Sales'].sum()
|
||||
sheet['B10'] = total
|
||||
|
||||
# ✅ CORRECT - Using formulas
|
||||
sheet['B10'] = '=SUM(B2:B9)'
|
||||
\`\`\`
|
||||
|
||||
## Reading and Analyzing Data
|
||||
|
||||
\`\`\`python
|
||||
import pandas as pd
|
||||
|
||||
# Read Excel
|
||||
df = pd.read_excel('file.xlsx')
|
||||
all_sheets = pd.read_excel('file.xlsx', sheet_name=None) # All sheets as dict
|
||||
|
||||
# Analyze
|
||||
df.head()
|
||||
df.info()
|
||||
df.describe()
|
||||
\`\`\`
|
||||
|
||||
## Creating New Excel Files
|
||||
|
||||
\`\`\`python
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
wb = Workbook()
|
||||
sheet = wb.active
|
||||
|
||||
# Add data
|
||||
sheet['A1'] = 'Hello'
|
||||
sheet.append(['Row', 'of', 'data'])
|
||||
|
||||
# Add formula
|
||||
sheet['B2'] = '=SUM(A1:A10)'
|
||||
|
||||
# Formatting
|
||||
sheet['A1'].font = Font(bold=True)
|
||||
sheet['A1'].fill = PatternFill('solid', start_color='FFFF00')
|
||||
sheet['A1'].alignment = Alignment(horizontal='center')
|
||||
|
||||
# Column width
|
||||
sheet.column_dimensions['A'].width = 20
|
||||
|
||||
wb.save('/home/user/output/output.xlsx')
|
||||
\`\`\`
|
||||
|
||||
## Editing Existing Files
|
||||
|
||||
\`\`\`python
|
||||
from openpyxl import load_workbook
|
||||
|
||||
wb = load_workbook('existing.xlsx')
|
||||
sheet = wb.active
|
||||
|
||||
# Modify cells
|
||||
sheet['A1'] = 'New Value'
|
||||
sheet.insert_rows(2)
|
||||
|
||||
wb.save('/home/user/output/modified.xlsx')
|
||||
\`\`\`
|
||||
|
||||
## Recalculating Formulas (MANDATORY)
|
||||
|
||||
After creating/editing files with formulas, run:
|
||||
\`\`\`bash
|
||||
python /home/user/scripts/xlsx/recalc.py /home/user/output/output.xlsx
|
||||
\`\`\`
|
||||
|
||||
The script returns JSON with error details:
|
||||
\`\`\`json
|
||||
{
|
||||
"status": "success",
|
||||
"total_errors": 0,
|
||||
"total_formulas": 42,
|
||||
"error_summary": {}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
If errors found, fix them and recalculate again.
|
||||
|
||||
## Financial Model Color Coding
|
||||
|
||||
- **Blue text**: Hardcoded inputs
|
||||
- **Black text**: Formulas and calculations
|
||||
- **Green text**: Links from other worksheets
|
||||
- **Yellow background**: Key assumptions needing attention
|
||||
|
||||
## Number Formatting
|
||||
|
||||
- Years: Format as text ("2024" not "2,024")
|
||||
- Currency: Use $#,##0 format
|
||||
- Percentages: 0.0% format
|
||||
- Negatives: Use parentheses (123) not minus -123
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Tool | Example |
|
||||
|------|------|---------|
|
||||
| Read Excel | pandas | \`pd.read_excel('file.xlsx')\` |
|
||||
| Create Excel | openpyxl | \`Workbook()\` |
|
||||
| Add formula | openpyxl | \`sheet['B2'] = '=SUM(A1:A10)'\` |
|
||||
| Recalculate | script | \`python /home/user/scripts/xlsx/recalc.py file.xlsx\` |`,
|
||||
};
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type Skill } from 'src/engine/core-modules/skills/skills.service';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
|
||||
export const LOAD_SKILL_TOOL_NAME = 'load_skill';
|
||||
|
||||
@@ -25,7 +25,7 @@ export type LoadSkillResult = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type LoadSkillFunction = (names: string[]) => Skill[];
|
||||
export type LoadSkillFunction = (names: string[]) => Promise<FlatSkill[]>;
|
||||
|
||||
export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
|
||||
description:
|
||||
@@ -36,7 +36,7 @@ export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
|
||||
}): Promise<LoadSkillResult> => {
|
||||
const { skillNames } = parameters.input;
|
||||
|
||||
const skills = loadSkills(skillNames);
|
||||
const skills = await loadSkills(skillNames);
|
||||
|
||||
if (skills.length === 0) {
|
||||
return {
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
// Configuration: $0.00001 = 1 credit
|
||||
export const DOLLAR_TO_CREDIT_MULTIPLIER = 1_000_000; // 1 / 0.000001 = 1 000 000 credits per dollar
|
||||
// Configuration: $0.000_001 = 1 credit
|
||||
export const DOLLAR_TO_CREDIT_MULTIPLIER = 1_000_000; // 1 / 0.000_001 = 1_000_000 credits per dollar
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/metadata-modules/ai/ai-b
|
||||
|
||||
// Converts cost in cents to cost in credits
|
||||
// Formula: credits = (cents / 100) * DOLLAR_TO_CREDIT_MULTIPLIER
|
||||
// Where DOLLAR_TO_CREDIT_MULTIPLIER = 1000000 (so $0.00001 = 1 credit)
|
||||
// Example: 1 cent = (1 / 100) * 1000000 = 10000 credits
|
||||
// Where DOLLAR_TO_CREDIT_MULTIPLIER = 1_000_000 (so $0.000_001 = 1 credit)
|
||||
// Example: 1 cent = (1 / 100) * 1_000_000 = 10_000 credits
|
||||
export const convertCentsToBillingCredits = (cents: number): number =>
|
||||
(cents / 100) * DOLLAR_TO_CREDIT_MULTIPLIER;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { SkillsModule } from 'src/engine/core-modules/skills/skills.module';
|
||||
import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@@ -44,7 +44,7 @@ import { ChatExecutionService } from './services/chat-execution.service';
|
||||
FileUploadModule,
|
||||
FileModule,
|
||||
PermissionsModule,
|
||||
SkillsModule,
|
||||
SkillModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceDomainsModule,
|
||||
|
||||
+6
@@ -10,6 +10,12 @@ Tool usage strategy:
|
||||
- Don't give up after first failure - be persistent
|
||||
- Validate assumptions before making changes
|
||||
|
||||
Database vs HTTP tools:
|
||||
- Use database tools (find_*, create_*, update_*, delete_*) for ALL Twenty CRM data operations
|
||||
- NEVER guess or construct API URLs - always use the appropriate database tool
|
||||
- The \`http_request\` tool is ONLY for external third-party APIs (not for Twenty's own data)
|
||||
- If you need to look up a record, load and use the corresponding find_one_* or find_many_* tool
|
||||
|
||||
Error recovery:
|
||||
- Analyze error messages to understand what went wrong
|
||||
- Adjust parameters or try different tools
|
||||
|
||||
+13
-10
@@ -17,7 +17,6 @@ import { getAppPath } from 'twenty-shared/utils';
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { SkillsService } from 'src/engine/core-modules/skills/skills.service';
|
||||
import {
|
||||
type ToolIndexEntry,
|
||||
ToolRegistryService,
|
||||
@@ -46,6 +45,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
|
||||
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
|
||||
|
||||
export type ChatExecutionOptions = {
|
||||
workspace: WorkspaceEntity;
|
||||
@@ -61,7 +62,7 @@ export type ChatExecutionResult = {
|
||||
modelConfig: AIModelConfig;
|
||||
};
|
||||
|
||||
const COMMON_PRELOAD_TOOLS = ['http_request', 'search_help_center'];
|
||||
const COMMON_PRELOAD_TOOLS = ['search_help_center'];
|
||||
|
||||
@Injectable()
|
||||
export class ChatExecutionService {
|
||||
@@ -69,7 +70,7 @@ export class ChatExecutionService {
|
||||
|
||||
constructor(
|
||||
private readonly toolRegistry: ToolRegistryService,
|
||||
private readonly skillsService: SkillsService,
|
||||
private readonly skillService: SkillService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly agentActorContextService: AgentActorContextService,
|
||||
@@ -108,7 +109,9 @@ export class ChatExecutionService {
|
||||
{ userId, userWorkspaceId },
|
||||
);
|
||||
|
||||
const skillCatalog = this.skillsService.getAllSkills();
|
||||
const skillCatalog = await this.skillService.findAllFlatSkills(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Built tool catalog with ${toolCatalog.length} tools, ${skillCatalog.length} skills available`,
|
||||
@@ -150,7 +153,7 @@ export class ChatExecutionService {
|
||||
},
|
||||
),
|
||||
[LOAD_SKILL_TOOL_NAME]: createLoadSkillTool((skillNames) =>
|
||||
this.skillsService.getSkillsByNames(skillNames),
|
||||
this.skillService.findFlatSkillsByNames(skillNames, workspace.id),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -283,7 +286,7 @@ export class ChatExecutionService {
|
||||
|
||||
private buildSystemPrompt(
|
||||
toolCatalog: ToolIndexEntry[],
|
||||
skillCatalog: Array<{ name: string; label: string; description: string }>,
|
||||
skillCatalog: FlatSkill[],
|
||||
preloadedTools: string[],
|
||||
contextString?: string,
|
||||
storedFiles?: Array<{ filename: string; storagePath: string; url: string }>,
|
||||
@@ -333,15 +336,15 @@ ${filesJson}
|
||||
In your Python code, access files at \`/home/user/{filename}\`.`;
|
||||
}
|
||||
|
||||
private buildSkillCatalogSection(
|
||||
skillCatalog: Array<{ name: string; label: string; description: string }>,
|
||||
): string {
|
||||
private buildSkillCatalogSection(skillCatalog: FlatSkill[]): string {
|
||||
if (skillCatalog.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const skillsList = skillCatalog
|
||||
.map((skill) => `- \`${skill.name}\`: ${skill.description}`)
|
||||
.map(
|
||||
(skill) => `- \`${skill.name}\`: ${skill.description ?? skill.label}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return `
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
// Configuration: $0.00001 = 1 credit
|
||||
export const DOLLAR_TO_CREDIT_MULTIPLIER = 1000000; // 1 / 0.000001 = 1000000 credits per dollar
|
||||
// Configuration: $0.00_001 = 1 credit
|
||||
export const DOLLAR_TO_CREDIT_MULTIPLIER = 1_000_000; // 1 / 0.00_0001 = 1_000_000 credits per dollar
|
||||
|
||||
+5
@@ -13,6 +13,7 @@ import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-module
|
||||
import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant';
|
||||
import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate-group/constants/flat-row-level-permission-predicate-group-editable-properties.constant';
|
||||
import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/flat-row-level-permission-predicate-editable-properties.constant';
|
||||
import { FLAT_SKILL_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-skill/constants/flat-skill-editable-properties.constant';
|
||||
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
|
||||
import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter-group/constants/flat-view-filter-group-editable-properties.constant';
|
||||
import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant';
|
||||
@@ -135,6 +136,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
skill: {
|
||||
propertiesToCompare: [...FLAT_SKILL_EDITABLE_PROPERTIES],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
rowLevelPermissionPredicate: {
|
||||
propertiesToCompare: [
|
||||
...FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES,
|
||||
|
||||
+1
@@ -113,6 +113,7 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
|
||||
},
|
||||
},
|
||||
agent: {},
|
||||
skill: {},
|
||||
pageLayout: {},
|
||||
pageLayoutWidget: {
|
||||
pageLayoutTabId: {
|
||||
|
||||
+4
@@ -8,6 +8,10 @@ export const ALL_METADATA_RELATION_PROPERTIES = {
|
||||
workspace: true,
|
||||
application: true,
|
||||
},
|
||||
skill: {
|
||||
workspace: true,
|
||||
application: true,
|
||||
},
|
||||
fieldMetadata: {
|
||||
relationTargetFieldMetadata: true,
|
||||
relationTargetObjectMetadata: true,
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
agent: {
|
||||
role: true,
|
||||
},
|
||||
skill: {},
|
||||
pageLayout: {},
|
||||
pageLayoutTab: {
|
||||
pageLayout: true,
|
||||
|
||||
+16
@@ -13,6 +13,7 @@ import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group.type';
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
@@ -33,6 +34,7 @@ import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-m
|
||||
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
|
||||
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
@@ -113,6 +115,11 @@ import {
|
||||
type DeleteServerlessFunctionAction,
|
||||
type UpdateServerlessFunctionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/types/workspace-migration-serverless-function-action-v2.type';
|
||||
import {
|
||||
type CreateSkillAction,
|
||||
type DeleteSkillAction,
|
||||
type UpdateSkillAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
|
||||
import {
|
||||
type CreateViewFieldAction,
|
||||
type DeleteViewFieldAction,
|
||||
@@ -295,6 +302,15 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
flatEntity: FlatAgent;
|
||||
entity: AgentEntity;
|
||||
};
|
||||
skill: {
|
||||
actions: {
|
||||
created: CreateSkillAction;
|
||||
updated: UpdateSkillAction;
|
||||
deleted: DeleteSkillAction;
|
||||
};
|
||||
flatEntity: FlatSkill;
|
||||
entity: SkillEntity;
|
||||
};
|
||||
pageLayout: {
|
||||
actions: {
|
||||
created: CreatePageLayoutAction;
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
|
||||
export const FLAT_SKILL_EDITABLE_PROPERTIES = [
|
||||
'name',
|
||||
'label',
|
||||
'icon',
|
||||
'description',
|
||||
'content',
|
||||
'isActive',
|
||||
] as const satisfies (keyof FlatSkill)[];
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { WorkspaceFlatSkillMapCacheService } from 'src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service';
|
||||
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([SkillEntity]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [WorkspaceFlatSkillMapCacheService],
|
||||
exports: [WorkspaceFlatSkillMapCacheService],
|
||||
})
|
||||
export class FlatSkillModule {}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { type FlatSkillMaps } from 'src/engine/metadata-modules/flat-skill/types/flat-skill-maps.type';
|
||||
import { transformSkillEntityToFlatSkill } from 'src/engine/metadata-modules/flat-skill/utils/transform-skill-entity-to-flat-skill.util';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('flatSkillMaps')
|
||||
export class WorkspaceFlatSkillMapCacheService extends WorkspaceCacheProvider<FlatSkillMaps> {
|
||||
constructor(
|
||||
@InjectRepository(SkillEntity)
|
||||
private readonly skillRepository: Repository<SkillEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(workspaceId: string): Promise<FlatSkillMaps> {
|
||||
const skills = await this.skillRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const flatSkillMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const skillEntity of skills) {
|
||||
const flatSkill = transformSkillEntityToFlatSkill(skillEntity);
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatSkill,
|
||||
flatEntityMapsToMutate: flatSkillMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatSkillMaps;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
|
||||
export type FlatSkillMaps = FlatEntityMaps<FlatSkill>;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
|
||||
export type FlatSkill = FlatEntityFrom<SkillEntity>;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { type CreateSkillInput } from 'src/engine/metadata-modules/skill/dtos/create-skill.input';
|
||||
|
||||
export const fromCreateSkillInputToFlatSkillToCreate = ({
|
||||
createSkillInput,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
createSkillInput: CreateSkillInput;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}): FlatSkill => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const { name, label, icon, description } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
createSkillInput,
|
||||
['name', 'label', 'icon', 'description'],
|
||||
);
|
||||
|
||||
// Content is markdown - only trim, don't collapse whitespace (preserve newlines)
|
||||
const content = createSkillInput.content.trim();
|
||||
|
||||
const id = v4();
|
||||
|
||||
return {
|
||||
id,
|
||||
standardId: null,
|
||||
name,
|
||||
label,
|
||||
icon: icon ?? null,
|
||||
description: description ?? null,
|
||||
content,
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
workspaceId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
universalIdentifier: id,
|
||||
applicationId,
|
||||
};
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import {
|
||||
SkillException,
|
||||
SkillExceptionCode,
|
||||
} from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
|
||||
export const fromDeleteSkillInputToFlatSkillOrThrow = ({
|
||||
flatSkillMaps,
|
||||
skillId,
|
||||
}: {
|
||||
flatSkillMaps: FlatEntityMaps<FlatSkill>;
|
||||
skillId: string;
|
||||
}): FlatSkill => {
|
||||
const existingFlatSkill = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: skillId,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatSkill)) {
|
||||
throw new SkillException(
|
||||
'Skill not found',
|
||||
SkillExceptionCode.SKILL_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!existingFlatSkill.isCustom) {
|
||||
throw new SkillException(
|
||||
'Cannot delete standard skill',
|
||||
SkillExceptionCode.SKILL_IS_STANDARD,
|
||||
);
|
||||
}
|
||||
|
||||
return existingFlatSkill;
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { type SkillDTO } from 'src/engine/metadata-modules/skill/dtos/skill.dto';
|
||||
|
||||
export const fromFlatSkillToSkillDto = (flatSkill: FlatSkill): SkillDTO => ({
|
||||
id: flatSkill.id,
|
||||
standardId: flatSkill.standardId,
|
||||
name: flatSkill.name,
|
||||
label: flatSkill.label,
|
||||
icon: flatSkill.icon ?? undefined,
|
||||
description: flatSkill.description ?? undefined,
|
||||
content: flatSkill.content,
|
||||
isCustom: flatSkill.isCustom,
|
||||
isActive: flatSkill.isActive,
|
||||
workspaceId: flatSkill.workspaceId,
|
||||
applicationId: flatSkill.applicationId ?? undefined,
|
||||
createdAt: new Date(flatSkill.createdAt),
|
||||
updatedAt: new Date(flatSkill.updatedAt),
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FLAT_SKILL_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-skill/constants/flat-skill-editable-properties.constant';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { type UpdateSkillInput } from 'src/engine/metadata-modules/skill/dtos/update-skill.input';
|
||||
import {
|
||||
SkillException,
|
||||
SkillExceptionCode,
|
||||
} from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export const fromUpdateSkillInputToFlatSkillToUpdateOrThrow = ({
|
||||
flatSkillMaps,
|
||||
updateSkillInput,
|
||||
}: {
|
||||
flatSkillMaps: FlatEntityMaps<FlatSkill>;
|
||||
updateSkillInput: UpdateSkillInput;
|
||||
}): FlatSkill => {
|
||||
const existingFlatSkill = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: updateSkillInput.id,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatSkill)) {
|
||||
throw new SkillException(
|
||||
'Skill not found',
|
||||
SkillExceptionCode.SKILL_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!existingFlatSkill.isCustom) {
|
||||
throw new SkillException(
|
||||
'Cannot update standard skill',
|
||||
SkillExceptionCode.SKILL_IS_STANDARD,
|
||||
);
|
||||
}
|
||||
|
||||
const { id: _id, ...updates } = updateSkillInput;
|
||||
|
||||
return {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingFlatSkill,
|
||||
properties: [...FLAT_SKILL_EDITABLE_PROPERTIES],
|
||||
update: updates,
|
||||
}),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
|
||||
export const transformSkillEntityToFlatSkill = (
|
||||
skillEntity: SkillEntity,
|
||||
): FlatSkill => {
|
||||
return {
|
||||
createdAt: skillEntity.createdAt.toISOString(),
|
||||
updatedAt: skillEntity.updatedAt.toISOString(),
|
||||
id: skillEntity.id,
|
||||
standardId: skillEntity.standardId,
|
||||
name: skillEntity.name,
|
||||
label: skillEntity.label,
|
||||
icon: skillEntity.icon,
|
||||
description: skillEntity.description,
|
||||
content: skillEntity.content,
|
||||
workspaceId: skillEntity.workspaceId,
|
||||
isCustom: skillEntity.isCustom,
|
||||
isActive: skillEntity.isActive,
|
||||
universalIdentifier: skillEntity.standardId || skillEntity.id,
|
||||
applicationId: skillEntity.applicationId,
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/ro
|
||||
import { SearchFieldMetadataModule } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.module';
|
||||
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
|
||||
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
|
||||
import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
@@ -27,6 +28,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
||||
SearchFieldMetadataModule,
|
||||
ServerlessFunctionModule,
|
||||
ServerlessFunctionLayerModule,
|
||||
SkillModule,
|
||||
AiAgentModule,
|
||||
AiAgentMonitorModule,
|
||||
AiChatModule,
|
||||
@@ -47,6 +49,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
||||
ObjectMetadataModule,
|
||||
SearchFieldMetadataModule,
|
||||
ServerlessFunctionModule,
|
||||
SkillModule,
|
||||
AiAgentModule,
|
||||
AiChatModule,
|
||||
ViewModule,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateSkillInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('Skill')
|
||||
export class SkillDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
standardId?: string | null;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
content: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isCustom: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isActive: boolean;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
applicationId?: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateSkillInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
content?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface';
|
||||
|
||||
@Entity('skill')
|
||||
@Index('IDX_SKILL_ID_IS_ACTIVE', ['id', 'isActive'])
|
||||
@Index('IDX_SKILL_NAME_WORKSPACE_ID_UNIQUE', ['name', 'workspaceId'], {
|
||||
unique: true,
|
||||
where: '"isActive" = true',
|
||||
})
|
||||
export class SkillEntity
|
||||
extends SyncableEntity
|
||||
implements Required<SkillEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
standardId: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
label: string;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
icon: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
content: string;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { skillGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/skill/utils/skill-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class SkillGraphqlApiExceptionInterceptor implements NestInterceptor {
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next.handle().pipe(catchError(skillGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum SkillExceptionCode {
|
||||
SKILL_NOT_FOUND = 'SKILL_NOT_FOUND',
|
||||
SKILL_ALREADY_EXISTS = 'SKILL_ALREADY_EXISTS',
|
||||
SKILL_IS_STANDARD = 'SKILL_IS_STANDARD',
|
||||
INVALID_SKILL_INPUT = 'INVALID_SKILL_INPUT',
|
||||
}
|
||||
|
||||
const getSkillExceptionUserFriendlyMessage = (code: SkillExceptionCode) => {
|
||||
switch (code) {
|
||||
case SkillExceptionCode.SKILL_NOT_FOUND:
|
||||
return msg`Skill not found.`;
|
||||
case SkillExceptionCode.SKILL_ALREADY_EXISTS:
|
||||
return msg`A skill with this name already exists.`;
|
||||
case SkillExceptionCode.SKILL_IS_STANDARD:
|
||||
return msg`Standard skills cannot be modified.`;
|
||||
case SkillExceptionCode.INVALID_SKILL_INPUT:
|
||||
return msg`Invalid skill input.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class SkillException extends CustomException<SkillExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: SkillExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ?? getSkillExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FlatSkillModule } from 'src/engine/metadata-modules/flat-skill/flat-skill.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { SkillGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/skill/interceptors/skill-graphql-api-exception.interceptor';
|
||||
import { SkillResolver } from 'src/engine/metadata-modules/skill/skill.resolver';
|
||||
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceMigrationV2Module,
|
||||
ApplicationModule,
|
||||
PermissionsModule,
|
||||
FlatSkillModule,
|
||||
],
|
||||
providers: [
|
||||
SkillService,
|
||||
SkillResolver,
|
||||
SkillGraphqlApiExceptionInterceptor,
|
||||
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [SkillService],
|
||||
})
|
||||
export class SkillModule {}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateSkillInput } from 'src/engine/metadata-modules/skill/dtos/create-skill.input';
|
||||
import { SkillDTO } from 'src/engine/metadata-modules/skill/dtos/skill.dto';
|
||||
import { UpdateSkillInput } from 'src/engine/metadata-modules/skill/dtos/update-skill.input';
|
||||
import { SkillGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/skill/interceptors/skill-graphql-api-exception.interceptor';
|
||||
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI))
|
||||
@UseInterceptors(
|
||||
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
|
||||
SkillGraphqlApiExceptionInterceptor,
|
||||
)
|
||||
@Resolver(() => SkillDTO)
|
||||
export class SkillResolver {
|
||||
constructor(private readonly skillService: SkillService) {}
|
||||
|
||||
@Query(() => [SkillDTO])
|
||||
async skills(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO[]> {
|
||||
return this.skillService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => SkillDTO, { nullable: true })
|
||||
async skill(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO | null> {
|
||||
return this.skillService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => SkillDTO)
|
||||
async createSkill(
|
||||
@Args('input') input: CreateSkillInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO> {
|
||||
return this.skillService.create(input, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => SkillDTO)
|
||||
async updateSkill(
|
||||
@Args('input') input: UpdateSkillInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO> {
|
||||
return this.skillService.update(input, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => SkillDTO)
|
||||
async deleteSkill(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO> {
|
||||
return this.skillService.delete(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => SkillDTO)
|
||||
async activateSkill(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO> {
|
||||
return this.skillService.activate(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => SkillDTO)
|
||||
async deactivateSkill(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<SkillDTO> {
|
||||
return this.skillService.deactivate(id, workspace.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { fromCreateSkillInputToFlatSkillToCreate } from 'src/engine/metadata-modules/flat-skill/utils/from-create-skill-input-to-flat-skill-to-create.util';
|
||||
import { fromDeleteSkillInputToFlatSkillOrThrow } from 'src/engine/metadata-modules/flat-skill/utils/from-delete-skill-input-to-flat-skill-or-throw.util';
|
||||
import { fromFlatSkillToSkillDto } from 'src/engine/metadata-modules/flat-skill/utils/from-flat-skill-to-skill-dto.util';
|
||||
import { fromUpdateSkillInputToFlatSkillToUpdateOrThrow } from 'src/engine/metadata-modules/flat-skill/utils/from-update-skill-input-to-flat-skill-to-update-or-throw.util';
|
||||
import { type CreateSkillInput } from 'src/engine/metadata-modules/skill/dtos/create-skill.input';
|
||||
import { type SkillDTO } from 'src/engine/metadata-modules/skill/dtos/skill.dto';
|
||||
import { type UpdateSkillInput } from 'src/engine/metadata-modules/skill/dtos/update-skill.input';
|
||||
import {
|
||||
SkillException,
|
||||
SkillExceptionCode,
|
||||
} from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class SkillService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<SkillDTO[]> {
|
||||
const { flatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatSkillMaps.byId)
|
||||
.filter(isDefined)
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map(fromFlatSkillToSkillDto);
|
||||
}
|
||||
|
||||
async findById(id: string, workspaceId: string): Promise<SkillDTO | null> {
|
||||
const { flatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatSkill = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatSkill)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromFlatSkillToSkillDto(flatSkill);
|
||||
}
|
||||
|
||||
async create(
|
||||
input: CreateSkillInput,
|
||||
workspaceId: string,
|
||||
): Promise<SkillDTO> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const flatSkillToCreate = fromCreateSkillInputToFlatSkillToCreate({
|
||||
createSkillInput: input,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
skill: {
|
||||
flatEntityToCreate: [flatSkillToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating skill',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatSkillMaps: recomputedFlatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatSkillToSkillDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatSkillToCreate.id,
|
||||
flatEntityMaps: recomputedFlatSkillMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async update(
|
||||
input: UpdateSkillInput,
|
||||
workspaceId: string,
|
||||
): Promise<SkillDTO> {
|
||||
const { flatSkillMaps: existingFlatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatSkillToUpdate = fromUpdateSkillInputToFlatSkillToUpdateOrThrow({
|
||||
flatSkillMaps: existingFlatSkillMaps,
|
||||
updateSkillInput: input,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
skill: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatSkillToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating skill',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatSkillMaps: recomputedFlatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatSkillToSkillDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: input.id,
|
||||
flatEntityMaps: recomputedFlatSkillMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<SkillDTO> {
|
||||
const { flatSkillMaps: existingFlatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatSkillToDelete = fromDeleteSkillInputToFlatSkillOrThrow({
|
||||
flatSkillMaps: existingFlatSkillMaps,
|
||||
skillId: id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
skill: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatSkillToDelete],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting skill',
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatSkillToSkillDto(flatSkillToDelete);
|
||||
}
|
||||
|
||||
async findAllFlatSkills(workspaceId: string): Promise<FlatSkill[]> {
|
||||
const { flatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatSkillMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((flatSkill) => flatSkill.isActive)
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
}
|
||||
|
||||
async findFlatSkillsByNames(
|
||||
names: string[],
|
||||
workspaceId: string,
|
||||
): Promise<FlatSkill[]> {
|
||||
if (names.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { flatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatSkillMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(flatSkill) => names.includes(flatSkill.name) && flatSkill.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
async activate(id: string, workspaceId: string): Promise<SkillDTO> {
|
||||
const { flatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingFlatSkill = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
const flatSkillToUpdate: FlatSkill = {
|
||||
...existingFlatSkill,
|
||||
isActive: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
skill: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatSkillToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while activating skill',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatSkillMaps: recomputedFlatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatSkillToSkillDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatSkillMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async deactivate(id: string, workspaceId: string): Promise<SkillDTO> {
|
||||
const { flatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingFlatSkill = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
const flatSkillToUpdate: FlatSkill = {
|
||||
...existingFlatSkill,
|
||||
isActive: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
skill: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatSkillToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deactivating skill',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatSkillMaps: recomputedFlatSkillMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatSkillMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatSkillToSkillDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatSkillMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdOrThrow(id: string, workspaceId: string): Promise<SkillDTO> {
|
||||
const skill = await this.findById(id, workspaceId);
|
||||
|
||||
if (!isDefined(skill)) {
|
||||
throw new SkillException(
|
||||
'Skill not found',
|
||||
SkillExceptionCode.SKILL_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
SkillException,
|
||||
SkillExceptionCode,
|
||||
} from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
|
||||
export const skillGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof SkillException) {
|
||||
switch (error.code) {
|
||||
case SkillExceptionCode.SKILL_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case SkillExceptionCode.INVALID_SKILL_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case SkillExceptionCode.SKILL_ALREADY_EXISTS:
|
||||
throw new ConflictError(error);
|
||||
case SkillExceptionCode.SKILL_IS_STANDARD:
|
||||
throw new ForbiddenError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
@@ -31,6 +31,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
|
||||
flatRoleTargetMaps: 'flat-maps:role-target',
|
||||
ORMEntityMetadatas: 'orm:entity-metadatas',
|
||||
flatAgentMaps: 'flat-maps:agent',
|
||||
flatSkillMaps: 'flat-maps:skill',
|
||||
flatRoleTargetByAgentIdMaps: 'flat-maps:flatRoleTargetByAgentId',
|
||||
flatPageLayoutMaps: 'flat-maps:page-layout',
|
||||
flatPageLayoutWidgetMaps: 'flat-maps:page-layout-widget',
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
export const STANDARD_SKILL = {
|
||||
'workflow-building': {
|
||||
universalIdentifier: '20202020-6155-838a-b64e-44a791fbdc13',
|
||||
},
|
||||
'data-manipulation': {
|
||||
universalIdentifier: '20202020-e225-f5c7-3d56-45feaa36f2e6',
|
||||
},
|
||||
'dashboard-building': {
|
||||
universalIdentifier: '20202020-398f-0d7a-82db-4f43bc7e7044',
|
||||
},
|
||||
'metadata-building': {
|
||||
universalIdentifier: '20202020-c66a-5fed-4a74-46e0b42a6332',
|
||||
},
|
||||
research: {
|
||||
universalIdentifier: '20202020-db75-4fca-6813-4c7db0f964a0',
|
||||
},
|
||||
'code-interpreter': {
|
||||
universalIdentifier: '20202020-5eb9-e775-cf4e-4f22be7be362',
|
||||
},
|
||||
xlsx: {
|
||||
universalIdentifier: '20202020-2c7f-5b77-dfa4-494b84752ab7',
|
||||
},
|
||||
pdf: {
|
||||
universalIdentifier: '20202020-c3d1-e0c9-2f93-45648b8bbd26',
|
||||
},
|
||||
docx: {
|
||||
universalIdentifier: '20202020-6f15-2432-0537-4e23a2efd1cb',
|
||||
},
|
||||
pptx: {
|
||||
universalIdentifier: '20202020-c81b-baf8-5255-4c34bd0eac9b',
|
||||
},
|
||||
} as const satisfies Record<
|
||||
string,
|
||||
{
|
||||
universalIdentifier: string;
|
||||
}
|
||||
>;
|
||||
+1
@@ -10,4 +10,5 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
|
||||
'view',
|
||||
'role',
|
||||
'agent',
|
||||
'skill',
|
||||
] as const satisfies AllMetadataName[];
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import type { STANDARD_SKILL } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant';
|
||||
|
||||
export type AllStandardSkillName = keyof typeof STANDARD_SKILL;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
|
||||
import { type CreateStandardSkillArgs } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-skill-flat-metadata.util';
|
||||
import { STANDARD_FLAT_SKILL_METADATA_BUILDERS_BY_SKILL_NAME } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util';
|
||||
|
||||
export const buildStandardFlatSkillMetadataMaps = (
|
||||
args: Omit<CreateStandardSkillArgs, 'context'>,
|
||||
): FlatEntityMaps<FlatSkill> => {
|
||||
const allSkillMetadatas: FlatSkill[] = Object.values(
|
||||
STANDARD_FLAT_SKILL_METADATA_BUILDERS_BY_SKILL_NAME,
|
||||
).map((builder) => builder(args));
|
||||
|
||||
let flatSkillMetadataMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const skillMetadata of allSkillMetadatas) {
|
||||
flatSkillMetadataMaps = addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity: skillMetadata,
|
||||
flatEntityMaps: flatSkillMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatSkillMetadataMaps;
|
||||
};
|
||||
+1044
File diff suppressed because it is too large
Load Diff
+46
@@ -0,0 +1,46 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { STANDARD_SKILL } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant';
|
||||
import { type AllStandardSkillName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-skill-name.type';
|
||||
import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type';
|
||||
|
||||
export type CreateStandardSkillContext = {
|
||||
skillName: AllStandardSkillName;
|
||||
name: string;
|
||||
label: string;
|
||||
icon: string | null;
|
||||
description: string | null;
|
||||
content: string;
|
||||
isCustom: boolean;
|
||||
};
|
||||
|
||||
export type CreateStandardSkillArgs = StandardBuilderArgs<'skill'> & {
|
||||
context: CreateStandardSkillContext;
|
||||
};
|
||||
|
||||
export const createStandardSkillFlatMetadata = ({
|
||||
context: { skillName, name, label, icon, description, content, isCustom },
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}: CreateStandardSkillArgs): FlatSkill => {
|
||||
const universalIdentifier = STANDARD_SKILL[skillName].universalIdentifier;
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
universalIdentifier,
|
||||
standardId: universalIdentifier,
|
||||
name,
|
||||
label,
|
||||
icon,
|
||||
description,
|
||||
content,
|
||||
isCustom,
|
||||
isActive: true,
|
||||
workspaceId,
|
||||
applicationId: twentyStandardApplicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
+10
@@ -1,6 +1,7 @@
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type TwentyStandardAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/types/twenty-standard-all-flat-entity-maps.type';
|
||||
import { buildStandardFlatAgentMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/agent-metadata/build-standard-flat-agent-metadata-maps.util';
|
||||
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
|
||||
import { buildStandardFlatFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/build-standard-flat-field-metadata-maps.util';
|
||||
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
|
||||
import { buildStandardFlatIndexMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/build-standard-flat-index-metadata-maps.util';
|
||||
@@ -122,6 +123,14 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
},
|
||||
});
|
||||
|
||||
const flatSkillMaps = buildStandardFlatSkillMetadataMaps({
|
||||
now,
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps: undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
flatViewFieldMaps,
|
||||
flatViewFilterMaps,
|
||||
@@ -132,5 +141,6 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatRoleMaps,
|
||||
flatAgentMaps,
|
||||
flatSkillMaps,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE
|
||||
return {
|
||||
summary: {
|
||||
invalidAgent: 0,
|
||||
invalidSkill: 0,
|
||||
invalidViewFilter: 0,
|
||||
invalidViewFilterGroup: 0,
|
||||
invalidObjectMetadata: 0,
|
||||
|
||||
+39
@@ -29,6 +29,7 @@ import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engin
|
||||
import { WorkspaceMigrationV2RowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/row-level-permission-predicate-group/workspace-migration-v2-row-level-permission-predicate-group-actions-builder.service';
|
||||
import { WorkspaceMigrationV2RowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/row-level-permission-predicate/workspace-migration-v2-row-level-permission-predicate-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
|
||||
import { WorkspaceMigrationV2SkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/workspace-migration-v2-skill-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/workspace-migration-v2-view-filter-group-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service';
|
||||
@@ -53,6 +54,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
private readonly workspaceMigrationV2FieldActionsBuilderService: WorkspaceMigrationV2FieldActionsBuilderService,
|
||||
private readonly workspaceMigrationV2RoleActionsBuilderService: WorkspaceMigrationV2RoleActionsBuilderService,
|
||||
private readonly workspaceMigrationV2AgentActionsBuilderService: WorkspaceMigrationV2AgentActionsBuilderService,
|
||||
private readonly workspaceMigrationV2SkillActionsBuilderService: WorkspaceMigrationV2SkillActionsBuilderService,
|
||||
private readonly workspaceMigrationV2PageLayoutActionsBuilderService: WorkspaceMigrationV2PageLayoutActionsBuilderService,
|
||||
private readonly workspaceMigrationV2PageLayoutWidgetActionsBuilderService: WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService,
|
||||
private readonly workspaceMigrationV2PageLayoutTabActionsBuilderService: WorkspaceMigrationV2PageLayoutTabActionsBuilderService,
|
||||
@@ -153,6 +155,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatRoleMaps,
|
||||
flatRoleTargetMaps,
|
||||
flatAgentMaps,
|
||||
flatSkillMaps,
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
@@ -781,6 +784,36 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatSkillMaps)) {
|
||||
const { from: fromFlatSkillMaps, to: toFlatSkillMaps } = flatSkillMaps;
|
||||
|
||||
const skillResult =
|
||||
await this.workspaceMigrationV2SkillActionsBuilderService.validateAndBuild(
|
||||
{
|
||||
additionalCacheDataMaps,
|
||||
from: fromFlatSkillMaps,
|
||||
to: toFlatSkillMaps,
|
||||
buildOptions,
|
||||
dependencyOptimisticFlatEntityMaps: undefined,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
|
||||
{
|
||||
allFlatEntityMaps: optimisticAllFlatEntityMaps,
|
||||
flatEntityMapsAndRelatedFlatEntityMaps:
|
||||
skillResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (skillResult.status === 'fail') {
|
||||
orchestratorFailureReport.skill.push(...skillResult.errors);
|
||||
} else {
|
||||
orchestratorActionsReport.skill = skillResult.actions;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatPageLayoutMaps)) {
|
||||
const { from: fromFlatPageLayoutMaps, to: toFlatPageLayoutMaps } =
|
||||
flatPageLayoutMaps;
|
||||
@@ -988,6 +1021,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
...aggregatedOrchestratorActionsReport.agent.updated,
|
||||
///
|
||||
|
||||
// Skills
|
||||
...aggregatedOrchestratorActionsReport.skill.deleted,
|
||||
...aggregatedOrchestratorActionsReport.skill.created,
|
||||
...aggregatedOrchestratorActionsReport.skill.updated,
|
||||
///
|
||||
|
||||
// Page layouts
|
||||
...aggregatedOrchestratorActionsReport.pageLayout.deleted,
|
||||
...aggregatedOrchestratorActionsReport.pageLayout.created,
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
|
||||
export type UpdateSkillAction = {
|
||||
type: 'update_skill';
|
||||
flatEntityId: string;
|
||||
flatEntityUpdates: FlatEntityPropertiesUpdates<'skill'>;
|
||||
};
|
||||
|
||||
export type CreateSkillAction = {
|
||||
type: 'create_skill';
|
||||
flatEntity: FlatSkill;
|
||||
};
|
||||
|
||||
export type DeleteSkillAction = {
|
||||
type: 'delete_skill';
|
||||
flatEntityId: string;
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
|
||||
import { UpdateSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
|
||||
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
|
||||
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
|
||||
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
|
||||
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-skill-validator.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationV2SkillActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
|
||||
typeof ALL_METADATA_NAME.skill
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatSkillValidatorService: FlatSkillValidatorService,
|
||||
) {
|
||||
super(ALL_METADATA_NAME.skill);
|
||||
}
|
||||
|
||||
protected validateFlatEntityCreation(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.skill>,
|
||||
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.skill, 'created'> {
|
||||
const validationResult =
|
||||
this.flatSkillValidatorService.validateFlatSkillCreation(args);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatSkillToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create_skill',
|
||||
flatEntity: flatSkillToValidate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityDeletion(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.skill>,
|
||||
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.skill, 'deleted'> {
|
||||
const validationResult =
|
||||
this.flatSkillValidatorService.validateFlatSkillDeletion(args);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatSkillToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete_skill',
|
||||
flatEntityId: flatSkillToValidate.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityUpdate(
|
||||
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.skill>,
|
||||
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.skill, 'updated'> {
|
||||
const validationResult =
|
||||
this.flatSkillValidatorService.validateFlatSkillUpdate(args);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityId, flatEntityUpdates } = args;
|
||||
|
||||
const updateSkillAction: UpdateSkillAction = {
|
||||
type: 'update_skill',
|
||||
flatEntityId,
|
||||
flatEntityUpdates,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: updateSkillAction,
|
||||
};
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
import { validateSkillNameUniqueness } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-skill-name-uniqueness.util';
|
||||
import {
|
||||
validateSkillContentIsDefined,
|
||||
validateSkillLabelIsDefined,
|
||||
validateSkillRequiredProperties,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-skill-required-properties.util';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@Injectable()
|
||||
export class FlatSkillValidatorService {
|
||||
public validateFlatSkillCreation({
|
||||
flatEntityToValidate: flatSkill,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatSkillMaps: optimisticFlatSkillMaps,
|
||||
},
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.skill
|
||||
>): FailedFlatEntityValidation<FlatSkill> {
|
||||
const validationResult: FailedFlatEntityValidation<FlatSkill> = {
|
||||
type: 'create_skill',
|
||||
errors: [],
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatSkill.id,
|
||||
name: flatSkill.name,
|
||||
},
|
||||
};
|
||||
|
||||
const existingSkills = Object.values(optimisticFlatSkillMaps.byId).filter(
|
||||
isDefined,
|
||||
);
|
||||
|
||||
validationResult.errors.push(
|
||||
...validateSkillRequiredProperties({ flatSkill }),
|
||||
);
|
||||
|
||||
validationResult.errors.push(
|
||||
...validateSkillNameUniqueness({
|
||||
name: flatSkill.name,
|
||||
existingFlatSkills: existingSkills,
|
||||
}),
|
||||
);
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatSkillDeletion({
|
||||
flatEntityToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatSkillMaps: optimisticFlatSkillMaps,
|
||||
},
|
||||
buildOptions,
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.skill
|
||||
>): FailedFlatEntityValidation<FlatSkill> {
|
||||
const validationResult: FailedFlatEntityValidation<FlatSkill> = {
|
||||
type: 'delete_skill',
|
||||
errors: [],
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityToValidate.id,
|
||||
name: flatEntityToValidate.name,
|
||||
},
|
||||
};
|
||||
|
||||
const existingSkill = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatEntityToValidate.id,
|
||||
flatEntityMaps: optimisticFlatSkillMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingSkill)) {
|
||||
validationResult.errors.push({
|
||||
code: SkillExceptionCode.SKILL_NOT_FOUND,
|
||||
message: t`Skill not found`,
|
||||
userFriendlyMessage: msg`Skill not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
if (!buildOptions.isSystemBuild && isStandardMetadata(existingSkill)) {
|
||||
validationResult.errors.push({
|
||||
code: SkillExceptionCode.SKILL_IS_STANDARD,
|
||||
message: t`Cannot delete standard skill`,
|
||||
userFriendlyMessage: msg`Cannot delete standard skill`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatSkillUpdate({
|
||||
flatEntityId,
|
||||
flatEntityUpdates,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatSkillMaps: optimisticFlatSkillMaps,
|
||||
},
|
||||
buildOptions,
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.skill
|
||||
>): FailedFlatEntityValidation<FlatSkill> {
|
||||
const validationResult: FailedFlatEntityValidation<FlatSkill> = {
|
||||
type: 'update_skill',
|
||||
errors: [],
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityId,
|
||||
},
|
||||
};
|
||||
|
||||
const fromFlatSkill = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId,
|
||||
flatEntityMaps: optimisticFlatSkillMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(fromFlatSkill)) {
|
||||
validationResult.errors.push({
|
||||
code: SkillExceptionCode.SKILL_NOT_FOUND,
|
||||
message: t`Skill not found`,
|
||||
userFriendlyMessage: msg`Skill not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
// Standard skills can only have isActive toggled, not other properties
|
||||
const isActiveUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'isActive',
|
||||
});
|
||||
|
||||
const hasNonIsActiveUpdates = flatEntityUpdates.some(
|
||||
(update) => update.property !== 'isActive',
|
||||
);
|
||||
|
||||
if (
|
||||
!buildOptions.isSystemBuild &&
|
||||
isStandardMetadata(fromFlatSkill) &&
|
||||
hasNonIsActiveUpdates
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: SkillExceptionCode.SKILL_IS_STANDARD,
|
||||
message: t`Cannot update standard skill properties (only activation/deactivation allowed)`,
|
||||
userFriendlyMessage: msg`Cannot update standard skill properties (only activation/deactivation allowed)`,
|
||||
});
|
||||
}
|
||||
|
||||
// If only isActive is being updated on a standard skill, allow it
|
||||
if (
|
||||
isStandardMetadata(fromFlatSkill) &&
|
||||
isDefined(isActiveUpdate) &&
|
||||
!hasNonIsActiveUpdates
|
||||
) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const optimisticFlatSkill: FlatSkill = {
|
||||
...fromFlatSkill,
|
||||
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
|
||||
updates: flatEntityUpdates,
|
||||
}),
|
||||
};
|
||||
|
||||
const labelUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'label',
|
||||
});
|
||||
|
||||
if (isDefined(labelUpdate)) {
|
||||
validationResult.errors.push(
|
||||
...validateSkillLabelIsDefined({ flatSkill: optimisticFlatSkill }),
|
||||
);
|
||||
}
|
||||
|
||||
const contentUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'content',
|
||||
});
|
||||
|
||||
if (isDefined(contentUpdate)) {
|
||||
validationResult.errors.push(
|
||||
...validateSkillContentIsDefined({ flatSkill: optimisticFlatSkill }),
|
||||
);
|
||||
}
|
||||
|
||||
const nameUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'name',
|
||||
});
|
||||
|
||||
if (isDefined(nameUpdate)) {
|
||||
const existingSkills = Object.values(optimisticFlatSkillMaps.byId)
|
||||
.filter(isDefined)
|
||||
.filter((skill) => skill.id !== flatEntityId);
|
||||
|
||||
validationResult.errors.push(
|
||||
...validateSkillNameUniqueness({
|
||||
name: nameUpdate.to,
|
||||
existingFlatSkills: existingSkills,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
|
||||
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
export const validateSkillNameUniqueness = ({
|
||||
name,
|
||||
existingFlatSkills,
|
||||
}: {
|
||||
name: string;
|
||||
existingFlatSkills: FlatSkill[];
|
||||
}): FlatEntityValidationError<SkillExceptionCode>[] => {
|
||||
const errors: FlatEntityValidationError<SkillExceptionCode>[] = [];
|
||||
|
||||
if (existingFlatSkills.some((skill) => skill.name === name)) {
|
||||
errors.push({
|
||||
code: SkillExceptionCode.SKILL_ALREADY_EXISTS,
|
||||
message: t`Skill with name "${name}" already exists`,
|
||||
userFriendlyMessage: msg`A skill with this name already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
|
||||
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
|
||||
export const validateSkillLabelIsDefined = ({
|
||||
flatSkill,
|
||||
}: {
|
||||
flatSkill: FlatSkill;
|
||||
}): FlatEntityValidationError<SkillExceptionCode>[] => {
|
||||
if (isNonEmptyString(flatSkill.label)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
code: SkillExceptionCode.INVALID_SKILL_INPUT,
|
||||
message: t`Label cannot be empty`,
|
||||
userFriendlyMessage: msg`Label cannot be empty`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const validateSkillContentIsDefined = ({
|
||||
flatSkill,
|
||||
}: {
|
||||
flatSkill: FlatSkill;
|
||||
}): FlatEntityValidationError<SkillExceptionCode>[] => {
|
||||
if (isNonEmptyString(flatSkill.content)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
code: SkillExceptionCode.INVALID_SKILL_INPUT,
|
||||
message: t`Content cannot be empty`,
|
||||
userFriendlyMessage: msg`Content cannot be empty`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const validateSkillRequiredProperties = ({
|
||||
flatSkill,
|
||||
}: {
|
||||
flatSkill: FlatSkill;
|
||||
}): FlatEntityValidationError<SkillExceptionCode>[] => [
|
||||
...validateSkillLabelIsDefined({ flatSkill }),
|
||||
...validateSkillContentIsDefined({ flatSkill }),
|
||||
];
|
||||
+3
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
|
||||
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-agent-validator.service';
|
||||
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-skill-validator.service';
|
||||
import { FlatCronTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-cron-trigger-validator.service';
|
||||
import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-database-event-trigger-validator.service';
|
||||
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service';
|
||||
@@ -42,6 +43,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatRoleValidatorService,
|
||||
FlatRoleTargetValidatorService,
|
||||
FlatAgentValidatorService,
|
||||
FlatSkillValidatorService,
|
||||
FlatPageLayoutValidatorService,
|
||||
FlatPageLayoutWidgetValidatorService,
|
||||
FlatPageLayoutTabValidatorService,
|
||||
@@ -65,6 +67,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatRoleValidatorService,
|
||||
FlatRoleTargetValidatorService,
|
||||
FlatAgentValidatorService,
|
||||
FlatSkillValidatorService,
|
||||
FlatPageLayoutValidatorService,
|
||||
FlatPageLayoutWidgetValidatorService,
|
||||
FlatPageLayoutTabValidatorService,
|
||||
|
||||
+3
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
|
||||
import { WorkspaceMigrationV2AgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/workspace-migration-v2-agent-actions-builder.service';
|
||||
import { WorkspaceMigrationV2SkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/workspace-migration-v2-skill-actions-builder.service';
|
||||
import { WorkspaceMigrationV2CronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/cron-trigger/workspace-migration-v2-cron-trigger-action-builder.service';
|
||||
import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/database-event-trigger/workspace-migration-v2-database-event-trigger-actions-builder.service';
|
||||
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
|
||||
@@ -43,6 +44,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationV2RoleActionsBuilderService,
|
||||
WorkspaceMigrationV2RoleTargetActionsBuilderService,
|
||||
WorkspaceMigrationV2AgentActionsBuilderService,
|
||||
WorkspaceMigrationV2SkillActionsBuilderService,
|
||||
WorkspaceMigrationV2PageLayoutActionsBuilderService,
|
||||
WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService,
|
||||
WorkspaceMigrationV2PageLayoutTabActionsBuilderService,
|
||||
@@ -65,6 +67,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationV2RoleActionsBuilderService,
|
||||
WorkspaceMigrationV2RoleTargetActionsBuilderService,
|
||||
WorkspaceMigrationV2AgentActionsBuilderService,
|
||||
WorkspaceMigrationV2SkillActionsBuilderService,
|
||||
WorkspaceMigrationV2PageLayoutActionsBuilderService,
|
||||
WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService,
|
||||
WorkspaceMigrationV2PageLayoutTabActionsBuilderService,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
|
||||
import { CreateSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateSkillActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'create_skill',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateSkillAction>): Partial<AllFlatEntityMaps> {
|
||||
const { flatSkillMaps } = allFlatEntityMaps;
|
||||
const { flatEntity } = action;
|
||||
|
||||
const updatedFlatSkillMaps = addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatSkillMaps: updatedFlatSkillMaps,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<CreateSkillAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { flatEntity } = action;
|
||||
|
||||
const skillRepository =
|
||||
queryRunner.manager.getRepository<SkillEntity>(SkillEntity);
|
||||
|
||||
await skillRepository.save({
|
||||
...flatEntity,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<CreateSkillAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
|
||||
import { DeleteSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteSkillActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'delete_skill',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteSkillAction>): Partial<AllFlatEntityMaps> {
|
||||
const { flatSkillMaps } = allFlatEntityMaps;
|
||||
const { flatEntityId } = action;
|
||||
|
||||
const updatedFlatSkillMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
|
||||
entityToDeleteId: flatEntityId,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatSkillMaps: updatedFlatSkillMaps,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<DeleteSkillAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { flatEntityId } = action;
|
||||
|
||||
const skillRepository =
|
||||
queryRunner.manager.getRepository<SkillEntity>(SkillEntity);
|
||||
|
||||
await skillRepository.delete({ id: flatEntityId, workspaceId });
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<DeleteSkillAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
|
||||
import { UpdateSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateSkillActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update_skill',
|
||||
) {
|
||||
optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateSkillAction>): Partial<AllFlatEntityMaps> {
|
||||
const { flatSkillMaps } = allFlatEntityMaps;
|
||||
const { flatEntityId, flatEntityUpdates } = action;
|
||||
|
||||
const existingSkill = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
const updatedSkill = {
|
||||
...existingSkill,
|
||||
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
|
||||
updates: flatEntityUpdates,
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedFlatSkillMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedSkill,
|
||||
flatEntityMaps: flatSkillMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatSkillMaps: updatedFlatSkillMaps,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UpdateSkillAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { flatEntityId, flatEntityUpdates } = action;
|
||||
|
||||
const skillRepository =
|
||||
queryRunner.manager.getRepository<SkillEntity>(SkillEntity);
|
||||
|
||||
await skillRepository.update(
|
||||
{ id: flatEntityId, workspaceId },
|
||||
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
|
||||
updates: flatEntityUpdates,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<UpdateSkillAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+7
@@ -4,6 +4,9 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
|
||||
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/create-agent-action-handler.service';
|
||||
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/delete-agent-action-handler.service';
|
||||
import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/update-agent-action-handler.service';
|
||||
import { CreateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/skill/services/create-skill-action-handler.service';
|
||||
import { DeleteSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/skill/services/delete-skill-action-handler.service';
|
||||
import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/skill/services/update-skill-action-handler.service';
|
||||
import { CreateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/create-cron-trigger-action-handler.service';
|
||||
import { DeleteCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/delete-cron-trigger-action-handler.service';
|
||||
import { UpdateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/update-cron-trigger-action-handler.service';
|
||||
@@ -123,6 +126,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
|
||||
UpdateAgentActionHandlerService,
|
||||
DeleteAgentActionHandlerService,
|
||||
|
||||
CreateSkillActionHandlerService,
|
||||
UpdateSkillActionHandlerService,
|
||||
DeleteSkillActionHandlerService,
|
||||
|
||||
CreatePageLayoutActionHandlerService,
|
||||
UpdatePageLayoutActionHandlerService,
|
||||
DeletePageLayoutActionHandlerService,
|
||||
|
||||
Reference in New Issue
Block a user