Compare commits

...

11 Commits

Author SHA1 Message Date
Waleed
da091dfe8a v0.4.17: input format + files support for webhooks, docs updates, dashboard improvements 2025-10-16 16:51:55 -07:00
Waleed
29c7827d6f fix(deployed-chat): include workspace id in deployed chat request (#1664) 2025-10-16 16:23:28 -07:00
Waleed
22f9d6e2df improvement(dashboard): improve dashboard performance, enrich charts, consolidate routes (#1662)
* improvement(dashboard): improve dashboard performance, enrich charts, consolidate routes

* cleanup
2025-10-16 15:53:06 -07:00
Waleed
0cb615428d feat(powershell): add gitattributes to prevent lint issues for powershell users (#1661) 2025-10-16 15:50:26 -07:00
Waleed
74576ec921 feat(i18n): update translations (#1660) 2025-10-16 15:31:10 -07:00
Waleed
67e681dd7c feat(webhooks): added optioanl input format to webhooks, added support for file uploads (#1654)
* feat(webhooks): added  optioanl input format to webhooks, added support for file uploads

* feat(webhooks): added input format component to generic webhook trigger, added file support

* consolidated execution files utils, extended presigned URL duration for async tasks
2025-10-16 12:16:12 -07:00
Waleed
4b05da31e0 feat(i18n): update translations (#1659) 2025-10-16 11:49:51 -07:00
Vikhyath Mondreti
82fa4e8bbb fix(input-format): allow value field to be cleared (#1658)
* fix(input-format): allow value field to be cleared

* don't let value field be detected as deployment change

* fix zep icon in docs

* exclude collapsed state
2025-10-16 11:11:27 -07:00
Waleed
4cd790b200 feat(i18n): update translations (#1653) 2025-10-15 23:21:26 -07:00
Waleed
b7e0b42d48 improvement(dashboard): cleanup execution dashboard UI, fix logs trace, and improve performance (#1651)
* improvement(dashboard): cleanup execution dashboard UI, fix logs trace, and improve perforamnce

* cleanup

* cleaned up

* ack PR comments
2025-10-15 23:00:07 -07:00
Waleed
c6ef5785c8 fix(i18n): fix SDK and guardrails translation corruption, restore i18n gh action for docs (#1652)
* fix(i18n): fix SDK and guardrails translation corruption

* re-enable i18n gh action
2025-10-15 22:52:48 -07:00
75 changed files with 6044 additions and 2571 deletions

34
.gitattributes vendored Normal file
View File

@@ -0,0 +1,34 @@
# Set default behavior to automatically normalize line endings
* text=auto eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.sh text eol=lf
*.bash text eol=lf
Dockerfile* text eol=lf
.dockerignore text eol=lf
.gitignore text eol=lf
.gitattributes text eol=lf
# Denote all files that are truly binary and should not be modified
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary

View File

@@ -1,13 +1,11 @@
name: 'Auto-translate Documentation'
# Temporarily disabled
on:
workflow_dispatch: # Allow manual triggers only
# push:
# branches: [ staging ]
# paths:
# - 'apps/docs/content/docs/en/**'
# - 'apps/docs/i18n.json'
push:
branches: [ staging ]
paths:
- 'apps/docs/content/docs/en/**'
- 'apps/docs/i18n.json'
permissions:
contents: write

View File

@@ -8,7 +8,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow.
Der Guardrails-Block validiert und schützt Ihre KI-Workflows, indem er Inhalte anhand mehrerer Validierungstypen überprüft. Stellen Sie die Datenqualität sicher, verhindern Sie Halluzinationen, erkennen Sie personenbezogene Daten und erzwingen Sie Formatanforderungen, bevor Inhalte durch Ihren Workflow fließen.
<div className="flex justify-center">
<Image
@@ -20,201 +20,201 @@ The Guardrails block validates and protects your AI workflows by checking conten
/>
</div>
## Overview
## Übersicht
The Guardrails block enables you to:
Mit dem Guardrails-Block können Sie:
<Steps>
<Step>
<strong>Validate JSON Structure</strong>: Ensure LLM outputs are valid JSON before parsing
<strong>JSON-Struktur validieren</strong>: Stellen Sie sicher, dass LLM-Ausgaben gültiges JSON sind, bevor sie geparst werden
</Step>
<Step>
<strong>Match Regex Patterns</strong>: Verify content matches specific formats (emails, phone numbers, URLs, etc.)
<strong>Regex-Muster abgleichen</strong>: Überprüfen Sie, ob Inhalte bestimmten Formaten entsprechen (E-Mails, Telefonnummern, URLs usw.)
</Step>
<Step>
<strong>Detect Hallucinations</strong>: Use RAG + LLM scoring to validate AI outputs against knowledge base content
<strong>Halluzinationen erkennen</strong>: Nutzen Sie RAG + LLM-Scoring, um KI-Ausgaben anhand von Wissensdatenbankinhalten zu validieren
</Step>
<Step>
<strong>Detect PII</strong>: Identify and optionally mask personally identifiable information across 40+ entity types
<strong>PII erkennen</strong>: Identifizieren und optional maskieren Sie personenbezogene Daten über mehr als 40 Entitätstypen hinweg
</Step>
</Steps>
## Validation Types
## Validierungstypen
### JSON Validation
### JSON-Validierung
Validates that content is properly formatted JSON. Perfect for ensuring structured LLM outputs can be safely parsed.
Überprüft, ob Inhalte korrekt formatiertes JSON sind. Perfekt, um sicherzustellen, dass strukturierte LLM-Ausgaben sicher geparst werden können.
**Use Cases:**
- Validate JSON responses from Agent blocks before parsing
- Ensure API payloads are properly formatted
- Check structured data integrity
**Anwendungsfälle:**
- Validieren von JSON-Antworten aus Agent-Blöcken vor dem Parsen
- Sicherstellen, dass API-Payloads korrekt formatiert sind
- Überprüfen der Integrität strukturierter Daten
**Output:**
- `passed`: `true` if valid JSON, `false` otherwise
- `error`: Error message if validation fails (e.g., "Invalid JSON: Unexpected token...")
- `passed`: `true` wenn gültiges JSON, sonst `false`
- `error`: Fehlermeldung bei fehlgeschlagener Validierung (z.B. "Invalid JSON: Unexpected token...")
### Regex Validation
### Regex-Validierung
Checks if content matches a specified regular expression pattern.
Überprüft, ob Inhalte einem bestimmten regulären Ausdrucksmuster entsprechen.
**Use Cases:**
- Validate email addresses
- Check phone number formats
- Verify URLs or custom identifiers
- Enforce specific text patterns
**Anwendungsfälle:**
- Validieren von E-Mail-Adressen
- Überprüfen von Telefonnummernformaten
- Verifizieren von URLs oder benutzerdefinierten Kennungen
- Durchsetzen spezifischer Textmuster
**Configuration:**
- **Regex Pattern**: The regular expression to match against (e.g., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` for emails)
**Konfiguration:**
- **Regex-Muster**: Der reguläre Ausdruck, der abgeglichen werden soll (z.B. `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` für E-Mails)
**Output:**
- `passed`: `true` if content matches pattern, `false` otherwise
- `error`: Error message if validation fails
- `passed`: `true` wenn der Inhalt dem Muster entspricht, `false` andernfalls
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
### Hallucination Detection
### Halluzinationserkennung
Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-generated content contradicts or isn't grounded in your knowledge base.
Verwendet Retrieval-Augmented Generation (RAG) mit LLM-Bewertung, um zu erkennen, wann KI-generierte Inhalte im Widerspruch zu Ihrer Wissensdatenbank stehen oder nicht darin begründet sind.
**How It Works:**
1. Queries your knowledge base for relevant context
2. Sends both the AI output and retrieved context to an LLM
3. LLM assigns a confidence score (0-10 scale)
- **0** = Full hallucination (completely ungrounded)
- **10** = Fully grounded (completely supported by knowledge base)
4. Validation passes if score ≥ threshold (default: 3)
**Funktionsweise:**
1. Durchsucht Ihre Wissensdatenbank nach relevantem Kontext
2. Sendet sowohl die KI-Ausgabe als auch den abgerufenen Kontext an ein LLM
3. LLM weist einen Konfidenzwert zu (Skala 0-10)
- **0** = Vollständige Halluzination (völlig unbegründet)
- **10** = Vollständig fundiert (komplett durch Wissensdatenbank gestützt)
4. Validierung besteht, wenn der Wert ≥ Schwellenwert (Standard: 3)
**Configuration:**
- **Knowledge Base**: Select from your existing knowledge bases
- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended)
- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama models)
- **Confidence Threshold**: Minimum score to pass (0-10, default: 3)
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10)
**Konfiguration:**
- **Wissensdatenbank**: Auswahl aus Ihren vorhandenen Wissensdatenbanken
- **Modell**: LLM für die Bewertung wählen (erfordert starkes Reasoning - GPT-4o, Claude 3.7 Sonnet empfohlen)
- **API-Schlüssel**: Authentifizierung für den ausgewählten LLM-Anbieter (automatisch ausgeblendet für gehostete/Ollama-Modelle)
- **Konfidenz-Schwellenwert**: Mindestwert zum Bestehen (0-10, Standard: 3)
- **Top K** (Erweitert): Anzahl der abzurufenden Wissensdatenbank-Chunks (Standard: 10)
**Output:**
- `passed`: `true` if confidence score ≥ threshold
- `score`: Confidence score (0-10)
- `reasoning`: LLM's explanation for the score
- `error`: Error message if validation fails
- `passed`: `true` wenn Konfidenzwert ≥ Schwellenwert
- `score`: Konfidenzwert (0-10)
- `reasoning`: Erklärung des LLM für den Wert
- `error`: Fehlermeldung bei fehlgeschlagener Validierung
**Use Cases:**
- Validate Agent responses against documentation
- Ensure customer support answers are factually accurate
- Verify generated content matches source material
- Quality control for RAG applications
**Anwendungsfälle:**
- Validierung von Agent-Antworten anhand der Dokumentation
- Sicherstellen, dass Kundenservice-Antworten sachlich korrekt sind
- Überprüfen, ob generierte Inhalte mit dem Quellmaterial übereinstimmen
- Qualitätskontrolle für RAG-Anwendungen
### PII Detection
### PII-Erkennung
Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages.
Erkennt personenbezogene Daten mit Microsoft Presidio. Unterstützt über 40 Entitätstypen in mehreren Ländern und Sprachen.
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**How It Works:**
1. Scans content for PII entities using pattern matching and NLP
2. Returns detected entities with locations and confidence scores
3. Optionally masks detected PII in the output
**Funktionsweise:**
1. Scannt Inhalte nach PII-Entitäten mittels Mustererkennung und NLP
2. Gibt erkannte Entitäten mit Positionen und Konfidenzwerten zurück
3. Maskiert optional erkannte PII in der Ausgabe
**Configuration:**
- **PII Types to Detect**: Select from grouped categories via modal selector
- **Common**: Person name, Email, Phone, Credit card, IP address, etc.
- **USA**: SSN, Driver's license, Passport, etc.
- **UK**: NHS number, National insurance number
- **Spain**: NIF, NIE, CIF
- **Italy**: Fiscal code, Driver's license, VAT code
- **Poland**: PESEL, NIP, REGON
- **Singapore**: NRIC/FIN, UEN
- **Australia**: ABN, ACN, TFN, Medicare
- **India**: Aadhaar, PAN, Passport, Voter number
- **Mode**:
- **Detect**: Only identify PII (default)
- **Mask**: Replace detected PII with masked values
- **Language**: Detection language (default: English)
**Konfiguration:**
- **Zu erkennende PII-Typen**: Auswahl aus gruppierten Kategorien über Modal-Selektor
- **Allgemein**: Personenname, E-Mail, Telefon, Kreditkarte, IP-Adresse usw.
- **USA**: SSN, Führerschein, Reisepass usw.
- **UK**: NHS-Nummer, Sozialversicherungsnummer
- **Spanien**: NIF, NIE, CIF
- **Italien**: Steuernummer, Führerschein, Umsatzsteuer-ID
- **Polen**: PESEL, NIP, REGON
- **Singapur**: NRIC/FIN, UEN
- **Australien**: ABN, ACN, TFN, Medicare
- **Indien**: Aadhaar, PAN, Reisepass, Wählernummer
- **Modus**:
- **Erkennen**: Nur PII identifizieren (Standard)
- **Maskieren**: Erkannte PII durch maskierte Werte ersetzen
- **Sprache**: Erkennungssprache (Standard: Englisch)
**Output:**
- `passed`: `false` if any selected PII types are detected
- `detectedEntities`: Array of detected PII with type, location, and confidence
- `maskedText`: Content with PII masked (only if mode = "Mask")
- `error`: Error message if validation fails
**Ausgabe:**
- `passed`: `false` wenn ausgewählte PII-Typen erkannt werden
- `detectedEntities`: Array erkannter PII mit Typ, Position und Konfidenz
- `maskedText`: Inhalt mit maskierter PII (nur wenn Modus = "Mask")
- `error`: Fehlermeldung, wenn die Validierung fehlschlägt
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Sanitize user inputs before processing
**Anwendungsfälle:**
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
- Maskieren von PII vor der Protokollierung oder Speicherung von Daten
- Einhaltung von DSGVO, HIPAA und anderen Datenschutzbestimmungen
- Bereinigung von Benutzereingaben vor der Verarbeitung
## Configuration
## Konfiguration
### Content to Validate
### Zu validierender Inhalt
The input content to validate. This typically comes from:
- Agent block outputs: `<agent.content>`
- Function block results: `<function.output>`
- API responses: `<api.output>`
- Any other block output
Der zu validierende Eingabeinhalt. Dieser stammt typischerweise aus:
- Ausgaben von Agent-Blöcken: `<agent.content>`
- Ergebnisse von Funktionsblöcken: `<function.output>`
- API-Antworten: `<api.output>`
- Jede andere Blockausgabe
### Validation Type
### Validierungstyp
Choose from four validation types:
- **Valid JSON**: Check if content is properly formatted JSON
- **Regex Match**: Verify content matches a regex pattern
- **Hallucination Check**: Validate against knowledge base with LLM scoring
- **PII Detection**: Detect and optionally mask personally identifiable information
Wählen Sie aus vier Validierungstypen:
- **Gültiges JSON**: Prüfen, ob der Inhalt korrekt formatiertes JSON ist
- **Regex-Übereinstimmung**: Überprüfen, ob der Inhalt einem Regex-Muster entspricht
- **Halluzinationsprüfung**: Validierung gegen Wissensdatenbank mit LLM-Bewertung
- **PII-Erkennung**: Erkennung und optional Maskierung personenbezogener Daten
## Outputs
## Ausgaben
All validation types return:
Alle Validierungstypen liefern zurück:
- **`<guardrails.passed>`**: Boolean indicating if validation passed
- **`<guardrails.validationType>`**: The type of validation performed
- **`<guardrails.input>`**: The original input that was validated
- **`<guardrails.error>`**: Error message if validation failed (optional)
- **`<guardrails.passed>`**: Boolean, der angibt, ob die Validierung erfolgreich war
- **`<guardrails.validationType>`**: Die Art der durchgeführten Validierung
- **`<guardrails.input>`**: Die ursprüngliche Eingabe, die validiert wurde
- **`<guardrails.error>`**: Fehlermeldung, wenn die Validierung fehlgeschlagen ist (optional)
Additional outputs by type:
Zusätzliche Ausgaben nach Typ:
**Hallucination Check:**
- **`<guardrails.score>`**: Confidence score (0-10)
- **`<guardrails.reasoning>`**: LLM's explanation
**Halluzinationsprüfung:**
- **`<guardrails.score>`**: Konfidenzwert (0-10)
- **`<guardrails.reasoning>`**: Erklärung des LLM
**PII Detection:**
- **`<guardrails.detectedEntities>`**: Array of detected PII entities
- **`<guardrails.maskedText>`**: Content with PII masked (if mode = "Mask")
**PII-Erkennung:**
- **`<guardrails.detectedEntities>`**: Array erkannter PII-Entitäten
- **`<guardrails.maskedText>`**: Inhalt mit maskierter PII (wenn Modus = "Mask")
## Example Use Cases
## Beispielanwendungsfälle
### Validate JSON Before Parsing
### JSON vor dem Parsen validieren
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Ensure Agent output is valid JSON</h4>
<h4 className="font-medium">Szenario: Sicherstellen, dass die Agent-Ausgabe gültiges JSON ist</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates structured JSON response</li>
<li>Guardrails validates JSON format</li>
<li>Condition block checks `<guardrails.passed>`</li>
<li>If passed → Parse and use data, If failedRetry or handle error</li>
<li>Agent generiert strukturierte JSON-Antwort</li>
<li>Guardrails validiert das JSON-Format</li>
<li>Bedingungsblock prüft `<guardrails.passed>`</li>
<li>Bei Erfolg → Daten parsen und verwenden, Bei FehlerWiederholen oder Fehler behandeln</li>
</ol>
</div>
### Prevent Hallucinations
### Halluzinationen verhindern
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Validate customer support responses</h4>
<h4 className="font-medium">Szenario: Validierung von Kundendienstantworten</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates response to customer question</li>
<li>Guardrails checks against support documentation knowledge base</li>
<li>If confidence score ≥ 3 → Send response</li>
<li>If confidence score \< 3 → Flag for human review</li>
<li>Agent generiert Antwort auf Kundenfrage</li>
<li>Guardrails prüft gegen die Wissensdatenbank der Support-Dokumentation</li>
<li>Wenn Konfidenzwert ≥ 3 → Antwort senden</li>
<li>Wenn Konfidenzwert \< 3 → Für manuelle Überprüfung markieren</li>
</ol>
</div>
### Block PII in User Inputs
### PII in Benutzereingaben blockieren
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Sanitize user-submitted content</h4>
<h4 className="font-medium">Szenario: Bereinigung von benutzergenerierten Inhalten</h4>
<ol className="list-decimal pl-5 text-sm">
<li>User submits form with text content</li>
<li>Guardrails detects PII (emails, phone numbers, SSN, etc.)</li>
<li>If PII detected → Reject submission or mask sensitive data</li>
<li>If no PII → Process normally</li>
<li>Benutzer reicht Formular mit Textinhalt ein</li>
<li>Guardrails erkennt PII (E-Mails, Telefonnummern, Sozialversicherungsnummern usw.)</li>
<li>Bei erkannter PII → Einreichung ablehnen oder sensible Daten maskieren</li>
<li>Ohne PII → Normal verarbeiten</li>
</ol>
</div>
@@ -222,30 +222,29 @@ Additional outputs by type:
<Video src="guardrails-example.mp4" width={500} height={350} />
</div>
### Validate Email Format
### E-Mail-Format validieren
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Check email address format</h4>
<h4 className="font-medium">Szenario: E-Mail-Adressformat überprüfen</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent extracts email from text</li>
<li>Guardrails validates with regex pattern</li>
<li>If valid → Use email for notification</li>
<li>If invalid → Request correction</li>
<li>Agent extrahiert E-Mail aus Text</li>
<li>Guardrails validiert mit Regex-Muster</li>
<li>Bei Gültigkeit → E-Mail für Benachrichtigung verwenden</li>
<li>Bei Ungültigkeit → Korrektur anfordern</li>
</ol>
</div>
## Best Practices
- **Chain with Condition blocks**: Use `<guardrails.passed>` to branch workflow logic based on validation results
- **Use JSON validation before parsing**: Always validate JSON structure before attempting to parse LLM outputs
- **Choose appropriate PII types**: Only select the PII entity types relevant to your use case for better performance
- **Set reasonable confidence thresholds**: For hallucination detection, adjust threshold based on your accuracy requirements (higher = stricter)
- **Use strong models for hallucination detection**: GPT-4o or Claude 3.7 Sonnet provide more accurate confidence scoring
- **Mask PII for logging**: Use "Mask" mode when you need to log or store content that may contain PII
- **Test regex patterns**: Validate your regex patterns thoroughly before deploying to production
- **Monitor validation failures**: Track `<guardrails.error>` messages to identify common validation issues
- **Verkettung mit Condition-Blöcken**: Verwende `<guardrails.passed>` um Workflow-Logik basierend auf Validierungsergebnissen zu verzweigen
- **JSON-Validierung vor dem Parsen verwenden**: Validiere immer die JSON-Struktur, bevor du versuchst, LLM-Ausgaben zu parsen
- **Passende PII-Typen auswählen**: Wähle nur die PII-Entitätstypen aus, die für deinen Anwendungsfall relevant sind, um bessere Leistung zu erzielen
- **Vernünftige Konfidenz-Schwellenwerte festlegen**: Passe für die Halluzinationserkennung den Schwellenwert basierend auf deinen Genauigkeitsanforderungen an (höher = strenger)
- **Starke Modelle für Halluzinationserkennung verwenden**: GPT-4o oder Claude 3.7 Sonnet bieten genauere Konfidenz-Bewertungen
- **PII für Logging maskieren**: Verwende den "Mask"-Modus, wenn du Inhalte protokollieren oder speichern musst, die PII enthalten könnten
- **Regex-Muster testen**: Validiere deine Regex-Muster gründlich, bevor du sie in der Produktion einsetzt
- **Validierungsfehler überwachen**: Verfolge `<guardrails.error>` Nachrichten, um häufige Validierungsprobleme zu identifizieren
<Callout type="info">
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
Guardrails-Validierung erfolgt synchron in deinem Workflow. Für die Halluzinationserkennung solltest du schnellere Modelle (wie GPT-4o-mini) wählen, wenn Latenz kritisch ist.
</Callout>

View File

@@ -535,7 +535,7 @@ app.post('/sim-webhook', (req, res) => {
// Handle error...
} else {
console.log(`Workflow ${workflowId} completed: ${executionId}`);
console.log(`Cost: ${cost.total}`);
console.log(`Cost: $${cost.total}`);
// Process successful execution...
}
break;

View File

@@ -81,7 +81,7 @@ new SimStudioClient(config: SimStudioConfig)
##### executeWorkflow()
Führen Sie einen Workflow mit optionalen Eingabedaten aus.
Führt einen Workflow mit optionalen Eingabedaten aus.
```typescript
const result = await client.executeWorkflow('workflow-id', {
@@ -99,7 +99,7 @@ const result = await client.executeWorkflow('workflow-id', {
- `selectedOutputs` (string[]): Block-Ausgaben, die im `blockName.attribute`Format gestreamt werden sollen (z.B. `["agent1.content"]`)
- `async` (boolean): Asynchron ausführen (Standard: false)
**Rückgabe:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
**Rückgabewert:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
Wenn `async: true`, wird sofort mit einer Task-ID zum Abfragen zurückgegeben. Andernfalls wird auf den Abschluss gewartet.
@@ -115,7 +115,7 @@ console.log('Is deployed:', status.isDeployed);
**Parameter:**
- `workflowId` (string): Die ID des Workflows
**Rückgabe:** `Promise<WorkflowStatus>`
**Rückgabewert:** `Promise<WorkflowStatus>`
##### validateWorkflow()
@@ -131,7 +131,7 @@ if (isReady) {
**Parameter:**
- `workflowId` (string): Die ID des Workflows
**Rückgabe:** `Promise<boolean>`
**Rückgabewert:** `Promise<boolean>`
##### getJobStatus()
@@ -148,7 +148,7 @@ if (status.status === 'completed') {
**Parameter:**
- `taskId` (string): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde
**Rückgabe:** `Promise<JobStatus>`
**Rückgabewert:** `Promise<JobStatus>`
**Antwortfelder:**
- `success` (boolean): Ob die Anfrage erfolgreich war
@@ -161,7 +161,7 @@ if (status.status === 'completed') {
##### executeWithRetry()
Führt einen Workflow mit automatischer Wiederholung bei Ratenlimitfehlern unter Verwendung von exponentiellem Backoff aus.
Einen Workflow mit automatischer Wiederholung bei Rate-Limit-Fehlern unter Verwendung von exponentiellem Backoff ausführen.
```typescript
const result = await client.executeWithRetry('workflow-id', {
@@ -184,13 +184,13 @@ const result = await client.executeWithRetry('workflow-id', {
- `maxDelay` (number): Maximale Verzögerung in ms (Standard: 30000)
- `backoffMultiplier` (number): Backoff-Multiplikator (Standard: 2)
**Rückgabewert:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
**Rückgabe:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
Die Wiederholungslogik verwendet exponentiellen Backoff (1s → 2s → 4s → 8s...) mit ±25% Jitter, um den Thundering-Herd-Effekt zu vermeiden. Wenn die API einen `retry-after`Header bereitstellt, wird dieser stattdessen verwendet.
Die Wiederholungslogik verwendet exponentielles Backoff (1s → 2s → 4s → 8s...) mit ±25% Jitter, um den Thundering-Herd-Effekt zu vermeiden. Wenn die API einen `retry-after` Header bereitstellt, wird dieser stattdessen verwendet.
##### getRateLimitInfo()
Ruft die aktuellen Ratenlimit-Informationen aus der letzten API-Antwort ab.
Ruft die aktuellen Rate-Limit-Informationen aus der letzten API-Antwort ab.
```typescript
const rateLimitInfo = client.getRateLimitInfo();
@@ -201,7 +201,7 @@ if (rateLimitInfo) {
}
```
**Rückgabewert:** `RateLimitInfo | null`
**Rückgabe:** `RateLimitInfo | null`
##### getUsageLimits()
@@ -215,7 +215,7 @@ console.log('Current period cost:', limits.usage.currentPeriodCost);
console.log('Plan:', limits.usage.plan);
```
**Rückgabewert:** `Promise<UsageLimits>`
**Rückgabe:** `Promise<UsageLimits>`
**Antwortstruktur:**
@@ -357,8 +357,8 @@ class SimStudioError extends Error {
**Häufige Fehlercodes:**
- `UNAUTHORIZED`: Ungültiger API-Schlüssel
- `TIMEOUT`: Zeitüberschreitung der Anfrage
- `RATE_LIMIT_EXCEEDED`: Ratengrenze überschritten
- `USAGE_LIMIT_EXCEEDED`: Nutzungsgrenze überschritten
- `RATE_LIMIT_EXCEEDED`: Rate-Limit überschritten
- `USAGE_LIMIT_EXCEEDED`: Nutzungslimit überschritten
- `EXECUTION_ERROR`: Workflow-Ausführung fehlgeschlagen
## Beispiele
@@ -604,26 +604,105 @@ async function executeClientSideWorkflow() {
});
console.log('Workflow result:', result);
// Update UI with result
document.getElementById('result')!.textContent =
document.getElementById('result')!.textContent =
JSON.stringify(result.output, null, 2);
} catch (error) {
console.error('Error:', error);
}
}
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
```
### Datei-Upload
Datei-Objekte werden automatisch erkannt und in das Base64-Format konvertiert. Fügen Sie sie in Ihrem Input unter dem Feldnamen ein, der dem API-Trigger-Inputformat Ihres Workflows entspricht.
Das SDK konvertiert Datei-Objekte in dieses Format:
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
Alternativ können Sie Dateien manuell im URL-Format bereitstellen:
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
<Tabs items={['Browser', 'Node.js']}>
<Tab value="Browser">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.NEXT_PUBLIC_SIM_API_KEY!
});
// From file input
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files || []);
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: files, // Must match your workflow's "files" field name
instructions: 'Analyze these documents'
}
});
console.log('Result:', result);
}
```
</Tab>
<Tab value="Node.js">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
import fs from 'fs';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
// Read file and create File object
const fileBuffer = fs.readFileSync('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', {
type: 'application/pdf'
});
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: [file], // Must match your workflow's "files" field name
query: 'Summarize this document'
}
});
```
</Tab>
</Tabs>
<Callout type="warning">
Bei der Verwendung des SDK im Browser sollten Sie darauf achten, keine sensiblen API-Schlüssel offenzulegen. Erwägen Sie die Verwendung eines Backend-Proxys oder öffentlicher API-Schlüssel mit eingeschränkten Berechtigungen.
</Callout>
### React Hook-Beispiel
### React Hook Beispiel
Erstellen eines benutzerdefinierten React-Hooks für die Workflow-Ausführung:
Erstellen Sie einen benutzerdefinierten React-Hook für die Workflow-Ausführung:
```typescript
import { useState, useCallback } from 'react';
@@ -815,11 +894,27 @@ async function checkUsage() {
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Usage ===');
console.log('Current period cost:
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
### Streaming Workflow Execution
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
Execute workflows with real-time streaming responses:
if (percentUsed > 80) {
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Error checking usage:', error);
}
}
checkUsage();
```
### Streaming-Workflow-Ausführung
Führen Sie Workflows mit Echtzeit-Streaming-Antworten aus:
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -830,33 +925,33 @@ const client = new SimStudioClient({
async function executeWithStreaming() {
try {
// Streaming für bestimmte Block-Ausgaben aktivieren
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', {
input: { message: 'Count to five' },
stream: true,
selectedOutputs: ['agent1.content'] // Format blockName.attribute verwenden
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
});
console.log('Workflow-Ergebnis:', result);
console.log('Workflow result:', result);
} catch (error) {
console.error('Fehler:', error);
console.error('Error:', error);
}
}
```
The streaming response follows the Server-Sent Events (SSE) format:
Die Streaming-Antwort folgt dem Server-Sent Events (SSE) Format:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", zwei"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**React Streaming Example:**
**React Streaming Beispiel:**
```typescript
import { useState, useEffect } from 'react';
@@ -869,13 +964,13 @@ function StreamingWorkflow() {
setLoading(true);
setOutput('');
// WICHTIG: Führen Sie diesen API-Aufruf von Ihrem Backend-Server aus, nicht vom Browser
// Setzen Sie niemals Ihren API-Schlüssel im Client-seitigen Code frei
// IMPORTANT: Make this API call from your backend server, not the browser
// Never expose your API key in client-side code
const response = await fetch('https://sim.ai/api/workflows/WORKFLOW_ID/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SIM_API_KEY! // Nur serverseitige Umgebungsvariable
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
},
body: JSON.stringify({
message: 'Generate a story',
@@ -907,10 +1002,10 @@ function StreamingWorkflow() {
if (parsed.chunk) {
setOutput(prev => prev + parsed.chunk);
} else if (parsed.event === 'done') {
console.log('Ausführung abgeschlossen:', parsed.metadata);
console.log('Execution complete:', parsed.metadata);
}
} catch (e) {
// Ungültiges JSON überspringen
// Skip invalid JSON
}
}
}
@@ -920,7 +1015,7 @@ function StreamingWorkflow() {
return (
<div>
<button onClick={executeStreaming} disabled={loading}>
{loading ? 'Generiere...' : 'Streaming starten'}
{loading ? 'Generating...' : 'Start Streaming'}
</button>
<div style={{ whiteSpace: 'pre-wrap' }}>{output}</div>
</div>
@@ -928,35 +1023,35 @@ function StreamingWorkflow() {
}
```
## Getting Your API Key
## API-Schlüssel erhalten
<Steps>
<Step title="Log in to Sim">
Navigate to [Sim](https://sim.ai) and log in to your account.
<Step title="Bei Sim anmelden">
Navigieren Sie zu [Sim](https://sim.ai) und melden Sie sich bei Ihrem Konto an.
</Step>
<Step title="Open your workflow">
Navigate to the workflow you want to execute programmatically.
<Step title="Öffnen Sie Ihren Workflow">
Navigieren Sie zu dem Workflow, den Sie programmatisch ausführen möchten.
</Step>
<Step title="Deploy your workflow">
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
<Step title="Deployen Sie Ihren Workflow">
Klicken Sie auf "Deploy", um Ihren Workflow zu deployen, falls dies noch nicht geschehen ist.
</Step>
<Step title="Create or select an API key">
During the deployment process, select or create an API key.
<Step title="API-Schlüssel erstellen oder auswählen">
Wählen Sie während des Deployment-Prozesses einen API-Schlüssel aus oder erstellen Sie einen neuen.
</Step>
<Step title="Copy the API key">
Copy the API key to use in your TypeScript/JavaScript application.
<Step title="API-Schlüssel kopieren">
Kopieren Sie den API-Schlüssel zur Verwendung in Ihrer TypeScript/JavaScript-Anwendung.
</Step>
</Steps>
<Callout type="warning">
Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management.
Halten Sie Ihren API-Schlüssel sicher und committen Sie ihn niemals in die Versionskontrolle. Verwenden Sie Umgebungsvariablen oder sicheres Konfigurationsmanagement.
</Callout>
## Requirements
## Anforderungen
- Node.js 16+
- TypeScript 5.0+ (for TypeScript projects)
- TypeScript 5.0+ (für TypeScript-Projekte)
## License
## Lizenz
Apache-2.0

View File

@@ -23,7 +23,210 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
</svg>`}
/>
## Übersicht
Der Generic Webhook-Block ermöglicht es Ihnen, Webhooks von jedem externen Dienst zu empfangen. Dies ist ein flexibler Auslöser, der jede JSON-Nutzlast verarbeiten kann und sich daher ideal für die Integration mit Diensten eignet, die keinen dedizierten Sim-Block haben.
## Grundlegende Verwendung
### Einfacher Durchleitungsmodus
Ohne ein Eingabeformat zu definieren, leitet der Webhook den gesamten Anforderungstext unverändert weiter:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Greifen Sie in nachgelagerten Blöcken auf die Daten zu mit:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Strukturiertes Eingabeformat (Optional)
Definieren Sie ein Eingabeschema, um typisierte Felder zu erhalten und erweiterte Funktionen wie Datei-Uploads zu aktivieren:
**Konfiguration des Eingabeformats:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhook-Anfrage:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## Datei-Uploads
### Unterstützte Dateiformate
Der Webhook unterstützt zwei Dateieingabeformate:
#### 1. Base64-kodierte Dateien
Zum direkten Hochladen von Dateiinhalten:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Maximale Größe**: 20MB pro Datei
- **Format**: Standard-Daten-URL mit Base64-Kodierung
- **Speicherung**: Dateien werden in sicheren Ausführungsspeicher hochgeladen
#### 2. URL-Referenzen
Zum Übergeben vorhandener Datei-URLs:
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Zugriff auf Dateien in nachgelagerten Blöcken
Dateien werden in `UserFile`Objekte mit folgenden Eigenschaften verarbeitet:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Zugriff in Blöcken:**
- `<webhook1.documents[0].url>` → Download-URL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Vollständiges Beispiel für Datei-Upload
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Authentifizierung
### Authentifizierung konfigurieren (optional)
In der Webhook-Konfiguration:
1. Aktiviere "Authentifizierung erforderlich"
2. Setze einen geheimen Token
3. Wähle den Header-Typ:
- **Benutzerdefinierter Header**: `X-Sim-Secret: your-token`
- **Authorization Bearer**: `Authorization: Bearer your-token`
### Verwendung der Authentifizierung
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Best Practices
1. **Eingabeformat für Struktur verwenden**: Definiere ein Eingabeformat, wenn du das erwartete Schema kennst. Dies bietet:
- Typvalidierung
- Bessere Autovervollständigung im Editor
- Datei-Upload-Funktionen
2. **Authentifizierung**: Aktiviere immer die Authentifizierung für Produktions-Webhooks, um unbefugten Zugriff zu verhindern.
3. **Dateigrößenbeschränkungen**: Halte Dateien unter 20 MB. Verwende für größere Dateien stattdessen URL-Referenzen.
4. **Dateiablauf**: Heruntergeladene Dateien haben URLs mit einer Gültigkeit von 5 Minuten. Verarbeite sie umgehend oder speichere sie an anderer Stelle, wenn sie länger benötigt werden.
5. **Fehlerbehandlung**: Die Webhook-Verarbeitung erfolgt asynchron. Überprüfe die Ausführungsprotokolle auf Fehler.
6. **Testen**: Verwende die Schaltfläche "Webhook testen" im Editor, um deine Konfiguration vor der Bereitstellung zu validieren.
## Anwendungsfälle
- **Formularübermittlungen**: Empfange Daten von benutzerdefinierten Formularen mit Datei-Uploads
- **Drittanbieter-Integrationen**: Verbinde mit Diensten, die Webhooks senden (Stripe, GitHub usw.)
- **Dokumentenverarbeitung**: Akzeptiere Dokumente von externen Systemen zur Verarbeitung
- **Ereignisbenachrichtigungen**: Empfange Ereignisdaten aus verschiedenen Quellen
- **Benutzerdefinierte APIs**: Erstelle benutzerdefinierte API-Endpunkte für deine Anwendungen
## Hinweise
- Kategorie: `triggers`
- Typ: `generic_webhook`
- **Dateiunterstützung**: Verfügbar über Eingabeformat-Konfiguration
- **Maximale Dateigröße**: 20 MB pro Datei

View File

@@ -110,6 +110,7 @@ Inhalte aus einem Microsoft Teams-Chat lesen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `chatId` | string | Ja | Die ID des Chats, aus dem gelesen werden soll |
| `includeAttachments` | boolean | Nein | Nachrichtenanhänge \(gehostete Inhalte\) herunterladen und in den Speicher aufnehmen |
#### Ausgabe
@@ -119,9 +120,10 @@ Inhalte aus einem Microsoft Teams-Chat lesen
| `messageCount` | number | Anzahl der aus dem Chat abgerufenen Nachrichten |
| `chatId` | string | ID des Chats, aus dem gelesen wurde |
| `messages` | array | Array von Chat-Nachrichtenobjekten |
| `attachmentCount` | number | Gesamtzahl der gefundenen Anhänge |
| `attachmentCount` | number | Gesamtanzahl der gefundenen Anhänge |
| `attachmentTypes` | array | Arten der gefundenen Anhänge |
| `content` | string | Formatierter Inhalt der Chat-Nachrichten |
| `attachments` | file[] | Hochgeladene Anhänge zur Vereinfachung \(abgeflacht\) |
### `microsoft_teams_write_chat`
@@ -155,19 +157,21 @@ Inhalte aus einem Microsoft Teams-Kanal lesen
| --------- | ---- | -------- | ----------- |
| `teamId` | string | Ja | Die ID des Teams, aus dem gelesen werden soll |
| `channelId` | string | Ja | Die ID des Kanals, aus dem gelesen werden soll |
| `includeAttachments` | boolean | Nein | Nachrichtenanhänge \(gehostete Inhalte\) herunterladen und in den Speicher aufnehmen |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus des Lesevorgangs im Teams-Kanal |
| `success` | boolean | Erfolgsstatus des Teams-Kanal-Lesevorgangs |
| `messageCount` | number | Anzahl der aus dem Kanal abgerufenen Nachrichten |
| `teamId` | string | ID des Teams, aus dem gelesen wurde |
| `channelId` | string | ID des Kanals, aus dem gelesen wurde |
| `messages` | array | Array von Kanalnachrichtenobjekten |
| `messages` | array | Array von Kanal-Nachrichtenobjekten |
| `attachmentCount` | number | Gesamtanzahl der gefundenen Anhänge |
| `attachmentTypes` | array | Arten der gefundenen Anhänge |
| `content` | string | Formatierter Inhalt der Kanalnachrichten |
| `content` | string | Formatierter Inhalt der Kanal-Nachrichten |
| `attachments` | file[] | Hochgeladene Anhänge zur Vereinfachung \(abgeflacht\) |
### `microsoft_teams_write_channel`

View File

@@ -203,6 +203,7 @@ E-Mails aus Outlook lesen
| --------- | ---- | -------- | ----------- |
| `folder` | string | Nein | Ordner-ID, aus der E-Mails gelesen werden sollen \(Standard: Posteingang\) |
| `maxResults` | number | Nein | Maximale Anzahl der abzurufenden E-Mails \(Standard: 1, max: 10\) |
| `includeAttachments` | boolean | Nein | E-Mail-Anhänge herunterladen und einschließen |
#### Ausgabe
@@ -210,6 +211,7 @@ E-Mails aus Outlook lesen
| --------- | ---- | ----------- |
| `message` | string | Erfolgs- oder Statusmeldung |
| `results` | array | Array von E-Mail-Nachrichtenobjekten |
| `attachments` | file[] | Alle E-Mail-Anhänge, zusammengefasst aus allen E-Mails |
### `outlook_forward`

View File

@@ -0,0 +1,242 @@
---
title: Zep
description: Langzeitgedächtnis für KI-Agenten
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="zep"
color="#E8E8E8"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 233 196'
>
<path
d='m231.34,108.7l-1.48-1.55h-10.26l3.59-75.86-14.8-.45-2.77,49.31c-59.6-3.24-119.33-3.24-178.92-.02l-1.73-64.96-14.8.45,2.5,91.53H2.16l-1.41,1.47c-1.55,16.23-.66,32.68,2.26,48.89h10.83l.18,1.27c.67,19.34,16.1,34.68,35.9,34.68s44.86-.92,66.12-.92,46.56.92,65.95.92,35.19-15.29,35.9-34.61l.16-1.34h11.02c2.91-16.19,3.81-32.61,2.26-48.81Zm-158.23,58.01c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Zm86.51,0c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Z'
fill='#FF1493'
/>
<polygon
points='111.77 22.4 93.39 49.97 93.52 50.48 185.88 38.51 190.95 27.68 114.32 36.55 117.7 31.48 117.7 31.47 138.38 .49 138.25 0 47.67 11.6 42.85 22.27 118.34 12.61 111.77 22.4'
fill='#FF1493'
/>
<path
d='m72.97,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
<path
d='m159.48,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
</svg>`}
/>
## Nutzungsanleitung
Integriere Zep für Langzeitgedächtnisverwaltung. Erstelle Threads, füge Nachrichten hinzu, rufe Kontext mit KI-gestützten Zusammenfassungen und Faktenextraktion ab.
## Tools
### `zep_create_thread`
Starte einen neuen Konversations-Thread in Zep
#### Input
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Ja | Eindeutige Kennung für den Thread |
| `userId` | string | Ja | Benutzer-ID, die mit dem Thread verknüpft ist |
| `apiKey` | string | Ja | Dein Zep API-Schlüssel |
#### Output
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `threadId` | string | Die Thread-ID |
| `userId` | string | Die Benutzer-ID |
| `uuid` | string | Interne UUID |
| `createdAt` | string | Erstellungszeitstempel |
| `projectUuid` | string | Projekt-UUID |
### `zep_get_threads`
Liste alle Konversations-Threads auf
#### Input
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `pageSize` | number | Nein | Anzahl der Threads, die pro Seite abgerufen werden sollen |
| `pageNumber` | number | Nein | Seitennummer für Paginierung |
| `orderBy` | string | Nein | Feld, nach dem die Ergebnisse sortiert werden sollen \(created_at, updated_at, user_id, thread_id\) |
| `asc` | boolean | Nein | Sortierrichtung: true für aufsteigend, false für absteigend |
| `apiKey` | string | Ja | Dein Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `threads` | array | Array von Thread-Objekten |
| `responseCount` | number | Anzahl der Threads in dieser Antwort |
| `totalCount` | number | Gesamtanzahl der verfügbaren Threads |
### `zep_delete_thread`
Einen Konversations-Thread aus Zep löschen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Ja | Thread-ID zum Löschen |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `deleted` | boolean | Ob der Thread gelöscht wurde |
### `zep_get_context`
Benutzerkontext aus einem Thread mit Zusammenfassungs- oder Basismodus abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Ja | Thread-ID, aus der der Kontext abgerufen werden soll |
| `mode` | string | Nein | Kontextmodus: "summary" \(natürliche Sprache\) oder "basic" \(rohe Fakten\) |
| `minRating` | number | Nein | Mindestbewertung, nach der relevante Fakten gefiltert werden |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `context` | string | Der Kontextstring \(Zusammenfassung oder Basis\) |
| `facts` | array | Extrahierte Fakten |
| `entities` | array | Extrahierte Entitäten |
| `summary` | string | Konversationszusammenfassung |
### `zep_get_messages`
Nachrichten aus einem Thread abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Ja | Thread-ID, aus der Nachrichten abgerufen werden sollen |
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Nachrichten |
| `cursor` | string | Nein | Cursor für Paginierung |
| `lastn` | number | Nein | Anzahl der neuesten Nachrichten, die zurückgegeben werden sollen \(überschreibt Limit und Cursor\) |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `messages` | array | Array von Nachrichtenobjekten |
| `rowCount` | number | Anzahl der Nachrichten in dieser Antwort |
| `totalCount` | number | Gesamtanzahl der Nachrichten im Thread |
### `zep_add_messages`
Nachrichten zu einem bestehenden Thread hinzufügen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Ja | Thread-ID, zu der Nachrichten hinzugefügt werden sollen |
| `messages` | json | Ja | Array von Nachrichtenobjekten mit Rolle und Inhalt |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `context` | string | Aktualisierter Kontext nach dem Hinzufügen von Nachrichten |
| `messageIds` | array | Array der hinzugefügten Nachrichten-UUIDs |
| `threadId` | string | Die Thread-ID |
### `zep_add_user`
Einen neuen Benutzer in Zep erstellen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `userId` | string | Ja | Eindeutige Kennung für den Benutzer |
| `email` | string | Nein | E-Mail-Adresse des Benutzers |
| `firstName` | string | Nein | Vorname des Benutzers |
| `lastName` | string | Nein | Nachname des Benutzers |
| `metadata` | json | Nein | Zusätzliche Metadaten als JSON-Objekt |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `userId` | string | Die Benutzer-ID |
| `email` | string | E-Mail des Benutzers |
| `firstName` | string | Vorname des Benutzers |
| `lastName` | string | Nachname des Benutzers |
| `uuid` | string | Interne UUID |
| `createdAt` | string | Erstellungszeitstempel |
| `metadata` | object | Benutzermetadaten |
### `zep_get_user`
Benutzerinformationen von Zep abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `userId` | string | Ja | Zu abzurufende Benutzer-ID |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `userId` | string | Die Benutzer-ID |
| `email` | string | E-Mail des Benutzers |
| `firstName` | string | Vorname des Benutzers |
| `lastName` | string | Nachname des Benutzers |
| `uuid` | string | Interne UUID |
| `createdAt` | string | Erstellungszeitstempel |
| `updatedAt` | string | Zeitstempel der letzten Aktualisierung |
| `metadata` | object | Benutzermetadaten |
### `zep_get_user_threads`
Alle Konversations-Threads für einen bestimmten Benutzer auflisten
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `userId` | string | Ja | Benutzer-ID, für die Threads abgerufen werden sollen |
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Threads |
| `apiKey` | string | Ja | Ihr Zep API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `threads` | array | Array von Thread-Objekten für diesen Benutzer |
| `userId` | string | Die Benutzer-ID |
## Hinweise
- Kategorie: `tools`
- Typ: `zep`

View File

@@ -679,10 +679,6 @@ Alternatively, you can manually provide files using the URL format:
</Tab>
</Tabs>
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
```
<Callout type="warning">
When using the SDK in the browser, be careful not to expose sensitive API keys. Consider using a backend proxy or public API keys with limited permissions.
</Callout>

View File

@@ -26,7 +26,208 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
## Overview
The Generic Webhook block allows you to receive webhooks from any external service. This is a flexible trigger that can handle any JSON payload, making it ideal for integrating with services that don't have a dedicated Sim block.
## Basic Usage
### Simple Passthrough Mode
Without defining an input format, the webhook passes through the entire request body as-is:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Access the data in downstream blocks using:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Structured Input Format (Optional)
Define an input schema to get typed fields and enable advanced features like file uploads:
**Input Format Configuration:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhook Request:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## File Uploads
### Supported File Formats
The webhook supports two file input formats:
#### 1. Base64 Encoded Files
For uploading file content directly:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Max size**: 20MB per file
- **Format**: Standard data URL with base64 encoding
- **Storage**: Files are uploaded to secure execution storage
#### 2. URL References
For passing existing file URLs:
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Accessing Files in Downstream Blocks
Files are processed into `UserFile` objects with the following properties:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Access in blocks:**
- `<webhook1.documents[0].url>` → Download URL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Complete File Upload Example
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Authentication
### Configure Authentication (Optional)
In the webhook configuration:
1. Enable "Require Authentication"
2. Set a secret token
3. Choose header type:
- **Custom Header**: `X-Sim-Secret: your-token`
- **Authorization Bearer**: `Authorization: Bearer your-token`
### Using Authentication
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Best Practices
1. **Use Input Format for Structure**: Define an input format when you know the expected schema. This provides:
- Type validation
- Better autocomplete in the editor
- File upload capabilities
2. **Authentication**: Always enable authentication for production webhooks to prevent unauthorized access.
3. **File Size Limits**: Keep files under 20MB. For larger files, use URL references instead.
4. **File Expiration**: Downloaded files have 5-minute expiration URLs. Process them promptly or store them elsewhere if needed longer.
5. **Error Handling**: Webhook processing is asynchronous. Check execution logs for errors.
6. **Testing**: Use the "Test Webhook" button in the editor to validate your configuration before deployment.
## Use Cases
- **Form Submissions**: Receive data from custom forms with file uploads
- **Third-Party Integrations**: Connect with services that send webhooks (Stripe, GitHub, etc.)
- **Document Processing**: Accept documents from external systems for processing
- **Event Notifications**: Receive event data from various sources
- **Custom APIs**: Build custom API endpoints for your applications
## Notes
- Category: `triggers`
- Type: `generic_webhook`
- **File Support**: Available via input format configuration
- **Max File Size**: 20MB per file

View File

@@ -7,30 +7,30 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="zep"
color="#4F46E5"
color="#E8E8E8"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 233 196'
>
<path
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z'
fill='currentColor'
d='m231.34,108.7l-1.48-1.55h-10.26l3.59-75.86-14.8-.45-2.77,49.31c-59.6-3.24-119.33-3.24-178.92-.02l-1.73-64.96-14.8.45,2.5,91.53H2.16l-1.41,1.47c-1.55,16.23-.66,32.68,2.26,48.89h10.83l.18,1.27c.67,19.34,16.1,34.68,35.9,34.68s44.86-.92,66.12-.92,46.56.92,65.95.92,35.19-15.29,35.9-34.61l.16-1.34h11.02c2.91-16.19,3.81-32.61,2.26-48.81Zm-158.23,58.01c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Zm86.51,0c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Z'
fill='#FF1493'
/>
<polygon
points='111.77 22.4 93.39 49.97 93.52 50.48 185.88 38.51 190.95 27.68 114.32 36.55 117.7 31.48 117.7 31.47 138.38 .49 138.25 0 47.67 11.6 42.85 22.27 118.34 12.61 111.77 22.4'
fill='#FF1493'
/>
<path
d='M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z'
fill='currentColor'
d='m72.97,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
<circle cx='12' cy='12' r='2' fill='currentColor' />
<path
d='M8 8h8M8 16h8'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
d='m159.48,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
</svg>`}
/>

View File

@@ -8,213 +8,213 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow.
El bloque Guardrails valida y protege tus flujos de trabajo de IA comprobando el contenido contra múltiples tipos de validación. Asegura la calidad de los datos, previene alucinaciones, detecta información personal identificable (PII) y aplica requisitos de formato antes de que el contenido avance por tu flujo de trabajo.
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails.png"
alt="Guardrails Block"
alt="Bloque Guardrails"
width={500}
height={350}
className="my-6"
/>
</div>
## Overview
## Descripción general
The Guardrails block enables you to:
El bloque Guardrails te permite:
<Steps>
<Step>
<strong>Validate JSON Structure</strong>: Ensure LLM outputs are valid JSON before parsing
<strong>Validar estructura JSON</strong>: Asegura que las salidas de LLM sean JSON válido antes de analizarlas
</Step>
<Step>
<strong>Match Regex Patterns</strong>: Verify content matches specific formats (emails, phone numbers, URLs, etc.)
<strong>Coincidir con patrones Regex</strong>: Verifica que el contenido coincida con formatos específicos (correos electrónicos, números de teléfono, URLs, etc.)
</Step>
<Step>
<strong>Detect Hallucinations</strong>: Use RAG + LLM scoring to validate AI outputs against knowledge base content
<strong>Detectar alucinaciones</strong>: Utiliza puntuación RAG + LLM para validar las salidas de IA contra el contenido de la base de conocimientos
</Step>
<Step>
<strong>Detect PII</strong>: Identify and optionally mask personally identifiable information across 40+ entity types
<strong>Detectar PII</strong>: Identifica y opcionalmente enmascara información personal identificable en más de 40 tipos de entidades
</Step>
</Steps>
## Validation Types
## Tipos de validación
### JSON Validation
### Validación JSON
Validates that content is properly formatted JSON. Perfect for ensuring structured LLM outputs can be safely parsed.
Valida que el contenido tenga un formato JSON adecuado. Perfecto para garantizar que las salidas estructuradas de LLM puedan analizarse de forma segura.
**Use Cases:**
- Validate JSON responses from Agent blocks before parsing
- Ensure API payloads are properly formatted
- Check structured data integrity
**Casos de uso:**
- Validar respuestas JSON de bloques Agent antes de analizarlas
- Asegurar que las cargas útiles de API estén correctamente formateadas
- Comprobar la integridad de datos estructurados
**Output:**
- `passed`: `true` if valid JSON, `false` otherwise
- `error`: Error message if validation fails (e.g., "Invalid JSON: Unexpected token...")
- `passed`: `true` si es JSON válido, `false` en caso contrario
- `error`: Mensaje de error si la validación falla (p. ej., "JSON inválido: Token inesperado...")
### Regex Validation
### Validación Regex
Checks if content matches a specified regular expression pattern.
Comprueba si el contenido coincide con un patrón de expresión regular especificado.
**Use Cases:**
- Validate email addresses
- Check phone number formats
- Verify URLs or custom identifiers
- Enforce specific text patterns
**Casos de uso:**
- Validar direcciones de correo electrónico
- Comprobar formatos de números de teléfono
- Verificar URLs o identificadores personalizados
- Aplicar patrones de texto específicos
**Configuration:**
- **Regex Pattern**: The regular expression to match against (e.g., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` for emails)
**Configuración:**
- **Patrón Regex**: La expresión regular para comparar (p. ej., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` para correos electrónicos)
**Output:**
- `passed`: `true` if content matches pattern, `false` otherwise
- `error`: Error message if validation fails
- `passed`: `true` si el contenido coincide con el patrón, `false` en caso contrario
- `error`: Mensaje de error si la validación falla
### Hallucination Detection
### Detección de alucinaciones
Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-generated content contradicts or isn't grounded in your knowledge base.
Utiliza generación aumentada por recuperación (RAG) con puntuación de LLM para detectar cuando el contenido generado por IA contradice o no está fundamentado en tu base de conocimientos.
**How It Works:**
1. Queries your knowledge base for relevant context
2. Sends both the AI output and retrieved context to an LLM
3. LLM assigns a confidence score (0-10 scale)
- **0** = Full hallucination (completely ungrounded)
- **10** = Fully grounded (completely supported by knowledge base)
4. Validation passes if score ≥ threshold (default: 3)
**Cómo funciona:**
1. Consulta tu base de conocimientos para obtener contexto relevante
2. Envía tanto la salida de la IA como el contexto recuperado a un LLM
3. El LLM asigna una puntuación de confianza (escala de 0-10)
- **0** = Alucinación completa (totalmente infundada)
- **10** = Completamente fundamentado (totalmente respaldado por la base de conocimientos)
4. La validación se aprueba si la puntuación ≥ umbral (predeterminado: 3)
**Configuration:**
- **Knowledge Base**: Select from your existing knowledge bases
- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended)
- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama models)
- **Confidence Threshold**: Minimum score to pass (0-10, default: 3)
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10)
**Configuración:**
- **Base de conocimientos**: Selecciona entre tus bases de conocimientos existentes
- **Modelo**: Elige LLM para puntuación (requiere razonamiento sólido - se recomienda GPT-4o, Claude 3.7 Sonnet)
- **Clave API**: Autenticación para el proveedor LLM seleccionado (oculta automáticamente para modelos alojados/Ollama)
- **Umbral de confianza**: Puntuación mínima para aprobar (0-10, predeterminado: 3)
- **Top K** (Avanzado): Número de fragmentos de la base de conocimientos a recuperar (predeterminado: 10)
**Output:**
- `passed`: `true` if confidence score ≥ threshold
- `score`: Confidence score (0-10)
- `reasoning`: LLM's explanation for the score
- `error`: Error message if validation fails
- `passed`: `true` si la puntuación de confianza ≥ umbral
- `score`: Puntuación de confianza (0-10)
- `reasoning`: Explicación del LLM para la puntuación
- `error`: Mensaje de error si la validación falla
**Use Cases:**
- Validate Agent responses against documentation
- Ensure customer support answers are factually accurate
- Verify generated content matches source material
- Quality control for RAG applications
**Casos de uso:**
- Validar respuestas de agentes contra documentación
- Asegurar que las respuestas de atención al cliente sean precisas
- Verificar que el contenido generado coincida con el material de origen
- Control de calidad para aplicaciones RAG
### PII Detection
### Detección de PII
Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages.
Detecta información de identificación personal utilizando Microsoft Presidio. Compatible con más de 40 tipos de entidades en múltiples países e idiomas.
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**How It Works:**
1. Scans content for PII entities using pattern matching and NLP
2. Returns detected entities with locations and confidence scores
3. Optionally masks detected PII in the output
**Cómo funciona:**
1. Escanea el contenido en busca de entidades PII mediante coincidencia de patrones y PNL
2. Devuelve las entidades detectadas con ubicaciones y puntuaciones de confianza
3. Opcionalmente enmascara la PII detectada en la salida
**Configuration:**
- **PII Types to Detect**: Select from grouped categories via modal selector
- **Common**: Person name, Email, Phone, Credit card, IP address, etc.
- **USA**: SSN, Driver's license, Passport, etc.
- **UK**: NHS number, National insurance number
- **Spain**: NIF, NIE, CIF
- **Italy**: Fiscal code, Driver's license, VAT code
- **Poland**: PESEL, NIP, REGON
- **Singapore**: NRIC/FIN, UEN
**Configuración:**
- **Tipos de PII a detectar**: Seleccione de categorías agrupadas mediante selector modal
- **Común**: Nombre de persona, Email, Teléfono, Tarjeta de crédito, Dirección IP, etc.
- **EE.UU.**: SSN, Licencia de conducir, Pasaporte, etc.
- **Reino Unido**: Número NHS, Número de seguro nacional
- **España**: NIF, NIE, CIF
- **Italia**: Código fiscal, Licencia de conducir, Código de IVA
- **Polonia**: PESEL, NIP, REGON
- **Singapur**: NRIC/FIN, UEN
- **Australia**: ABN, ACN, TFN, Medicare
- **India**: Aadhaar, PAN, Passport, Voter number
- **Mode**:
- **Detect**: Only identify PII (default)
- **Mask**: Replace detected PII with masked values
- **Language**: Detection language (default: English)
- **India**: Aadhaar, PAN, Pasaporte, Número de votante
- **Modo**:
- **Detectar**: Solo identificar PII (predeterminado)
- **Enmascarar**: Reemplazar PII detectada con valores enmascarados
- **Idioma**: Idioma de detección (predeterminado: inglés)
**Output:**
- `passed`: `false` if any selected PII types are detected
- `detectedEntities`: Array of detected PII with type, location, and confidence
- `maskedText`: Content with PII masked (only if mode = "Mask")
- `error`: Error message if validation fails
**Salida:**
- `passed`: `false` si se detectan los tipos de PII seleccionados
- `detectedEntities`: Array de PII detectada con tipo, ubicación y confianza
- `maskedText`: Contenido con PII enmascarada (solo si modo = "Mask")
- `error`: Mensaje de error si la validación falla
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Sanitize user inputs before processing
**Casos de uso:**
- Bloquear contenido que contiene información personal sensible
- Enmascarar PII antes de registrar o almacenar datos
- Cumplimiento con GDPR, HIPAA y otras regulaciones de privacidad
- Sanear entradas de usuario antes del procesamiento
## Configuration
## Configuración
### Content to Validate
### Contenido a validar
The input content to validate. This typically comes from:
- Agent block outputs: `<agent.content>`
- Function block results: `<function.output>`
- API responses: `<api.output>`
- Any other block output
El contenido de entrada para validar. Esto típicamente proviene de:
- Salidas de bloques de agente: `<agent.content>`
- Resultados de bloques de función: `<function.output>`
- Respuestas de API: `<api.output>`
- Cualquier otra salida de bloque
### Validation Type
### Tipo de validación
Choose from four validation types:
- **Valid JSON**: Check if content is properly formatted JSON
- **Regex Match**: Verify content matches a regex pattern
- **Hallucination Check**: Validate against knowledge base with LLM scoring
- **PII Detection**: Detect and optionally mask personally identifiable information
Elija entre cuatro tipos de validación:
- **JSON válido**: Comprobar si el contenido es JSON correctamente formateado
- **Coincidencia Regex**: Verificar si el contenido coincide con un patrón regex
- **Comprobación de alucinaciones**: Validar contra base de conocimiento con puntuación LLM
- **Detección de PII**: Detectar y opcionalmente enmascarar información de identificación personal
## Outputs
## Salidas
All validation types return:
Todos los tipos de validación devuelven:
- **`<guardrails.passed>`**: Boolean indicating if validation passed
- **`<guardrails.validationType>`**: The type of validation performed
- **`<guardrails.input>`**: The original input that was validated
- **`<guardrails.error>`**: Error message if validation failed (optional)
- **`<guardrails.passed>`**: Booleano que indica si la validación fue exitosa
- **`<guardrails.validationType>`**: El tipo de validación realizada
- **`<guardrails.input>`**: La entrada original que fue validada
- **`<guardrails.error>`**: Mensaje de error si la validación falló (opcional)
Additional outputs by type:
Salidas adicionales por tipo:
**Hallucination Check:**
- **`<guardrails.score>`**: Confidence score (0-10)
- **`<guardrails.reasoning>`**: LLM's explanation
**Verificación de alucinaciones:**
- **`<guardrails.score>`**: Puntuación de confianza (0-10)
- **`<guardrails.reasoning>`**: Explicación del LLM
**PII Detection:**
- **`<guardrails.detectedEntities>`**: Array of detected PII entities
- **`<guardrails.maskedText>`**: Content with PII masked (if mode = "Mask")
**Detección de PII:**
- **`<guardrails.detectedEntities>`**: Array de entidades PII detectadas
- **`<guardrails.maskedText>`**: Contenido con PII enmascarado (si el modo = "Mask")
## Example Use Cases
## Ejemplos de casos de uso
### Validate JSON Before Parsing
### Validar JSON antes de analizarlo
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Ensure Agent output is valid JSON</h4>
<h4 className="font-medium">Escenario: Asegurar que la salida del agente sea JSON válido</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates structured JSON response</li>
<li>Guardrails validates JSON format</li>
<li>Condition block checks `<guardrails.passed>`</li>
<li>If passed → Parse and use data, If failed → Retry or handle error</li>
<li>El agente genera una respuesta JSON estructurada</li>
<li>Guardrails valida el formato JSON</li>
<li>El bloque de condición verifica `<guardrails.passed>`</li>
<li>Si pasa → Analizar y usar datos, Si falla → Reintentar o manejar el error</li>
</ol>
</div>
### Prevent Hallucinations
### Prevenir alucinaciones
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Validate customer support responses</h4>
<h4 className="font-medium">Escenario: Validar respuestas de atención al cliente</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates response to customer question</li>
<li>Guardrails checks against support documentation knowledge base</li>
<li>If confidence score ≥ 3 → Send response</li>
<li>If confidence score \< 3 → Flag for human review</li>
<li>El agente genera una respuesta a la pregunta del cliente</li>
<li>Guardrails verifica contra la base de conocimientos de documentación de soporte</li>
<li>Si la puntuación de confianza ≥ 3 → Enviar respuesta</li>
<li>Si la puntuación de confianza \< 3 → Marcar para revisión humana</li>
</ol>
</div>
### Block PII in User Inputs
### Bloquear PII en entradas de usuario
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Sanitize user-submitted content</h4>
<h4 className="font-medium">Escenario: Sanear contenido enviado por usuarios</h4>
<ol className="list-decimal pl-5 text-sm">
<li>User submits form with text content</li>
<li>Guardrails detects PII (emails, phone numbers, SSN, etc.)</li>
<li>If PII detected → Reject submission or mask sensitive data</li>
<li>If no PII → Process normally</li>
<li>El usuario envía un formulario con contenido de texto</li>
<li>Guardrails detecta PII (correos electrónicos, números de teléfono, SSN, etc.)</li>
<li>Si se detecta PII → Rechazar el envío o enmascarar datos sensibles</li>
<li>Si no hay PII → Procesar normalmente</li>
</ol>
</div>
@@ -222,30 +222,29 @@ Additional outputs by type:
<Video src="guardrails-example.mp4" width={500} height={350} />
</div>
### Validate Email Format
### Validar formato de correo electrónico
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Check email address format</h4>
<h4 className="font-medium">Escenario: Comprobar el formato de dirección de correo electrónico</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent extracts email from text</li>
<li>Guardrails validates with regex pattern</li>
<li>If valid → Use email for notification</li>
<li>If invalid → Request correction</li>
<li>El agente extrae el correo electrónico del texto</li>
<li>Guardrails valida con un patrón regex</li>
<li>Si es válido → Usar el correo electrónico para notificación</li>
<li>Si no es válidoSolicitar corrección</li>
</ol>
</div>
## Best Practices
## Mejores prácticas
- **Chain with Condition blocks**: Use `<guardrails.passed>` to branch workflow logic based on validation results
- **Use JSON validation before parsing**: Always validate JSON structure before attempting to parse LLM outputs
- **Choose appropriate PII types**: Only select the PII entity types relevant to your use case for better performance
- **Set reasonable confidence thresholds**: For hallucination detection, adjust threshold based on your accuracy requirements (higher = stricter)
- **Use strong models for hallucination detection**: GPT-4o or Claude 3.7 Sonnet provide more accurate confidence scoring
- **Mask PII for logging**: Use "Mask" mode when you need to log or store content that may contain PII
- **Test regex patterns**: Validate your regex patterns thoroughly before deploying to production
- **Monitor validation failures**: Track `<guardrails.error>` messages to identify common validation issues
- **Encadena con bloques de Condición**: Usa `<guardrails.passed>` para ramificar la lógica del flujo de trabajo según los resultados de validación
- **Usa validación JSON antes de analizar**: Siempre valida la estructura JSON antes de intentar analizar las salidas de LLM
- **Elige los tipos de PII apropiados**: Selecciona solo los tipos de entidades PII relevantes para tu caso de uso para un mejor rendimiento
- **Establece umbrales de confianza razonables**: Para la detección de alucinaciones, ajusta el umbral según tus requisitos de precisión (más alto = más estricto)
- **Usa modelos potentes para la detección de alucinaciones**: GPT-4o o Claude 3.7 Sonnet proporcionan una puntuación de confianza más precisa
- **Enmascara PII para el registro**: Usa el modo "Mask" cuando necesites registrar o almacenar contenido que pueda contener PII
- **Prueba patrones regex**: Valida tus patrones de expresiones regulares minuciosamente antes de implementarlos en producción
- **Monitorea fallos de validación**: Rastrea los mensajes `<guardrails.error>` para identificar problemas comunes de validación
<Callout type="info">
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
La validación de Guardrails ocurre de forma sincrónica en tu flujo de trabajo. Para la detección de alucinaciones, elige modelos más rápidos (como GPT-4o-mini) si la latencia es crítica.
</Callout>

View File

@@ -535,7 +535,7 @@ app.post('/sim-webhook', (req, res) => {
// Handle error...
} else {
console.log(`Workflow ${workflowId} completed: ${executionId}`);
console.log(`Cost: ${cost.total}`);
console.log(`Cost: $${cost.total}`);
// Process successful execution...
}
break;

View File

@@ -1,5 +1,5 @@
---
title: TypeScript/JavaScript SDK
title: SDK de TypeScript/JavaScript
---
import { Callout } from 'fumadocs-ui/components/callout'
@@ -7,10 +7,10 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
El SDK oficial de TypeScript/JavaScript para Sim proporciona seguridad de tipos completa y es compatible tanto con entornos Node.js como de navegador, lo que te permite ejecutar flujos de trabajo programáticamente desde tus aplicaciones Node.js, aplicaciones web y otros entornos JavaScript.
El SDK oficial de TypeScript/JavaScript para Sim proporciona seguridad de tipos completa y es compatible tanto con entornos Node.js como con navegadores, lo que te permite ejecutar flujos de trabajo programáticamente desde tus aplicaciones Node.js, aplicaciones web y otros entornos JavaScript.
<Callout type="info">
El SDK de TypeScript proporciona seguridad de tipos completa, soporte para ejecución asíncrona, limitación automática de velocidad con retroceso exponencial y seguimiento de uso.
El SDK de TypeScript proporciona seguridad de tipos completa, soporte para ejecución asíncrona, limitación automática de tasa con retroceso exponencial y seguimiento de uso.
</Callout>
## Instalación
@@ -96,12 +96,12 @@ const result = await client.executeWorkflow('workflow-id', {
- `input` (any): Datos de entrada para pasar al flujo de trabajo
- `timeout` (number): Tiempo de espera en milisegundos (predeterminado: 30000)
- `stream` (boolean): Habilitar respuestas en streaming (predeterminado: false)
- `selectedOutputs` (string[]): Bloquear salidas para transmitir en formato `blockName.attribute` (por ejemplo, `["agent1.content"]`)
- `selectedOutputs` (string[]): Salidas de bloques para transmitir en formato `blockName.attribute` (p. ej., `["agent1.content"]`)
- `async` (boolean): Ejecutar de forma asíncrona (predeterminado: false)
**Devuelve:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
Cuando `async: true`, devuelve inmediatamente un ID de tarea para sondeo. De lo contrario, espera a que se complete.
Cuando `async: true`, devuelve inmediatamente un ID de tarea para consultar. De lo contrario, espera hasta completarse.
##### getWorkflowStatus()
@@ -146,7 +146,7 @@ if (status.status === 'completed') {
```
**Parámetros:**
- `taskId` (string): El ID de tarea devuelto de la ejecución asíncrona
- `taskId` (string): El ID de tarea devuelto por la ejecución asíncrona
**Devuelve:** `Promise<JobStatus>`
@@ -161,7 +161,7 @@ if (status.status === 'completed') {
##### executeWithRetry()
Ejecuta un flujo de trabajo con reintento automático en errores de límite de tasa utilizando retroceso exponencial.
Ejecutar un flujo de trabajo con reintento automático en errores de límite de tasa usando retroceso exponencial.
```typescript
const result = await client.executeWithRetry('workflow-id', {
@@ -247,7 +247,7 @@ console.log('Plan:', limits.usage.plan);
##### setApiKey()
Actualiza la clave API.
Actualiza la clave de API.
```typescript
client.setApiKey('new-api-key');
@@ -501,9 +501,9 @@ Configura el cliente usando variables de entorno:
</Tab>
</Tabs>
### Integración con Express de Node.js
### Integración con Node.js Express
Integra con un servidor Express.js:
Integración con un servidor Express.js:
```typescript
import express from 'express';
@@ -582,7 +582,7 @@ export default async function handler(
}
```
### Uso del navegador
### Uso en el navegador
Uso en el navegador (con configuración CORS adecuada):
@@ -604,19 +604,98 @@ async function executeClientSideWorkflow() {
});
console.log('Workflow result:', result);
// Update UI with result
document.getElementById('result')!.textContent =
document.getElementById('result')!.textContent =
JSON.stringify(result.output, null, 2);
} catch (error) {
console.error('Error:', error);
}
}
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
```
### Carga de archivos
Los objetos File son detectados automáticamente y convertidos a formato base64. Inclúyelos en tu entrada bajo el nombre de campo que coincida con el formato de entrada del disparador API de tu flujo de trabajo.
El SDK convierte los objetos File a este formato:
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
Alternativamente, puedes proporcionar archivos manualmente usando el formato URL:
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
<Tabs items={['Browser', 'Node.js']}>
<Tab value="Browser">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.NEXT_PUBLIC_SIM_API_KEY!
});
// From file input
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files || []);
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: files, // Must match your workflow's "files" field name
instructions: 'Analyze these documents'
}
});
console.log('Result:', result);
}
```
</Tab>
<Tab value="Node.js">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
import fs from 'fs';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
// Read file and create File object
const fileBuffer = fs.readFileSync('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', {
type: 'application/pdf'
});
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: [file], // Must match your workflow's "files" field name
query: 'Summarize this document'
}
});
```
</Tab>
</Tabs>
<Callout type="warning">
Cuando uses el SDK en el navegador, ten cuidado de no exponer claves API sensibles. Considera usar un proxy de backend o claves API públicas con permisos limitados.
</Callout>
@@ -748,9 +827,9 @@ async function executeAsync() {
executeAsync();
```
### Límite de tasa y reintentos
### Limitación de tasa y reintentos
Maneja límites de tasa automáticamente con retroceso exponencial:
Maneja los límites de tasa automáticamente con retroceso exponencial:
```typescript
import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk';
@@ -788,7 +867,7 @@ async function executeWithRetryHandling() {
### Monitoreo de uso
Monitorea el uso de tu cuenta y sus límites:
Monitorea el uso y los límites de tu cuenta:
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -814,19 +893,19 @@ async function checkUsage() {
console.log(' Resets at:', limits.rateLimit.async.resetAt);
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Uso ===');
console.log('Costo del período actual: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Límite: $' + limits.usage.limit.toFixed(2));
console.log('\n=== Usage ===');
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Uso: ' + percentUsed.toFixed(1) + '%');
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
if (percentUsed > 80) {
console.warn('⚠️ Advertencia: ¡Estás acercándote a tu límite de uso!');
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Error al verificar el uso:', error);
console.error('Error checking usage:', error);
}
}
@@ -846,40 +925,35 @@ const client = new SimStudioClient({
async function executeWithStreaming() {
try {
// Habilita streaming para salidas de bloques específicos
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', {
input: { message: 'Count to five' },
stream: true,
selectedOutputs: ['agent1.content'] // Usa el formato blockName.attribute
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
});
console.log('Resultado del flujo de trabajo:', result);
console.log('Workflow result:', result);
} catch (error) {
console.error('Error:', error);
}
}
```
The streaming response follows the Server-Sent Events (SSE) format:
La respuesta en streaming sigue el formato de eventos enviados por el servidor (SSE):
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", dos"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**React Streaming Example:**
**Ejemplo de streaming en React:**
```
typescript
```typescript
import { useState, useEffect } from 'react';
function StreamingWorkflow() {
@@ -941,44 +1015,43 @@ function StreamingWorkflow() {
return (
<div>
<button onClick={executeStreaming} disabled={loading}>
{loading ? 'Generando...' : 'Iniciar streaming'}
{loading ? 'Generating...' : 'Start Streaming'}
</button>
<div style={{ whiteSpace: 'pre-wrap' }}>{output}</div>
</div>
);
}
```
## Getting Your API Key
## Obtener tu clave API
<Steps>
<Step title="Log in to Sim">
Navigate to [Sim](https://sim.ai) and log in to your account.
<Step title="Inicia sesión en Sim">
Navega a [Sim](https://sim.ai) e inicia sesión en tu cuenta.
</Step>
<Step title="Open your workflow">
Navigate to the workflow you want to execute programmatically.
<Step title="Abre tu flujo de trabajo">
Navega al flujo de trabajo que quieres ejecutar programáticamente.
</Step>
<Step title="Deploy your workflow">
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
<Step title="Despliega tu flujo de trabajo">
Haz clic en "Desplegar" para desplegar tu flujo de trabajo si aún no ha sido desplegado.
</Step>
<Step title="Create or select an API key">
During the deployment process, select or create an API key.
<Step title="Crea o selecciona una clave API">
Durante el proceso de despliegue, selecciona o crea una clave API.
</Step>
<Step title="Copy the API key">
Copy the API key to use in your TypeScript/JavaScript application.
<Step title="Copia la clave API">
Copia la clave API para usarla en tu aplicación TypeScript/JavaScript.
</Step>
</Steps>
<Callout type="warning">
Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management.
Mantén tu clave API segura y nunca la incluyas en el control de versiones. Utiliza variables de entorno o gestión segura de configuración.
</Callout>
## Requirements
## Requisitos
- Node.js 16+
- TypeScript 5.0+ (for TypeScript projects)
- TypeScript 5.0+ (para proyectos TypeScript)
## License
## Licencia
Apache-2.0

View File

@@ -22,7 +22,210 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
</svg>`}
/>
## Descripción general
El bloque de Webhook Genérico te permite recibir webhooks desde cualquier servicio externo. Este es un disparador flexible que puede manejar cualquier carga útil JSON, lo que lo hace ideal para integrarse con servicios que no tienen un bloque Sim dedicado.
## Uso básico
### Modo de paso simple
Sin definir un formato de entrada, el webhook transmite todo el cuerpo de la solicitud tal como está:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Accede a los datos en bloques posteriores usando:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Formato de entrada estructurado (opcional)
Define un esquema de entrada para obtener campos tipados y habilitar funciones avanzadas como cargas de archivos:
**Configuración del formato de entrada:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Solicitud de webhook:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## Cargas de archivos
### Formatos de archivo compatibles
El webhook admite dos formatos de entrada de archivos:
#### 1. Archivos codificados en Base64
Para cargar contenido de archivos directamente:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Tamaño máximo**: 20MB por archivo
- **Formato**: URL de datos estándar con codificación base64
- **Almacenamiento**: Los archivos se cargan en un almacenamiento de ejecución seguro
#### 2. Referencias URL
Para pasar URLs de archivos existentes:
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Acceso a archivos en bloques posteriores
Los archivos se procesan en objetos `UserFile` con las siguientes propiedades:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Acceso en bloques:**
- `<webhook1.documents[0].url>` → URL de descarga
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Ejemplo completo de carga de archivos
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Autenticación
### Configurar autenticación (opcional)
En la configuración del webhook:
1. Habilita "Requerir autenticación"
2. Establece un token secreto
3. Elige el tipo de encabezado:
- **Encabezado personalizado**: `X-Sim-Secret: your-token`
- **Autorización Bearer**: `Authorization: Bearer your-token`
### Uso de la autenticación
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Mejores prácticas
1. **Usa formato de entrada para la estructura**: define un formato de entrada cuando conozcas el esquema esperado. Esto proporciona:
- Validación de tipo
- Mejor autocompletado en el editor
- Capacidades de carga de archivos
2. **Autenticación**: habilita siempre la autenticación para webhooks en producción para prevenir accesos no autorizados.
3. **Límites de tamaño de archivo**: mantén los archivos por debajo de 20MB. Para archivos más grandes, usa referencias URL en su lugar.
4. **Caducidad de archivos**: los archivos descargados tienen URLs con caducidad de 5 minutos. Procésalos rápidamente o almacénalos en otro lugar si los necesitas por más tiempo.
5. **Manejo de errores**: el procesamiento de webhooks es asíncrono. Revisa los registros de ejecución para ver errores.
6. **Pruebas**: usa el botón "Probar webhook" en el editor para validar tu configuración antes de implementarla.
## Casos de uso
- **Envíos de formularios**: recibe datos de formularios personalizados con cargas de archivos
- **Integraciones con terceros**: conéctate con servicios que envían webhooks (Stripe, GitHub, etc.)
- **Procesamiento de documentos**: acepta documentos de sistemas externos para procesamiento
- **Notificaciones de eventos**: recibe datos de eventos de varias fuentes
- **APIs personalizadas**: construye endpoints de API personalizados para tus aplicaciones
## Notas
- Categoría: `triggers`
- Tipo: `generic_webhook`
- **Soporte de archivos**: disponible a través de la configuración del formato de entrada
- **Tamaño máximo de archivo**: 20MB por archivo

View File

@@ -108,8 +108,9 @@ Leer contenido de un chat de Microsoft Teams
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | -------- | ----------- |
| `chatId` | string | Sí | El ID del chat del que leer |
| `includeAttachments` | boolean | No | Descargar e incluir archivos adjuntos de mensajes \(contenidos alojados\) en el almacenamiento |
#### Salida
@@ -122,6 +123,7 @@ Leer contenido de un chat de Microsoft Teams
| `attachmentCount` | number | Número total de archivos adjuntos encontrados |
| `attachmentTypes` | array | Tipos de archivos adjuntos encontrados |
| `content` | string | Contenido formateado de los mensajes de chat |
| `attachments` | file[] | Archivos adjuntos subidos para mayor comodidad \(aplanados\) |
### `microsoft_teams_write_chat`
@@ -155,6 +157,7 @@ Leer contenido de un canal de Microsoft Teams
| --------- | ---- | -------- | ----------- |
| `teamId` | string | Sí | El ID del equipo del que leer |
| `channelId` | string | Sí | El ID del canal del que leer |
| `includeAttachments` | boolean | No | Descargar e incluir archivos adjuntos de mensajes \(contenidos alojados\) en el almacenamiento |
#### Salida
@@ -168,6 +171,7 @@ Leer contenido de un canal de Microsoft Teams
| `attachmentCount` | number | Número total de archivos adjuntos encontrados |
| `attachmentTypes` | array | Tipos de archivos adjuntos encontrados |
| `content` | string | Contenido formateado de los mensajes del canal |
| `attachments` | file[] | Archivos adjuntos subidos para mayor comodidad \(aplanados\) |
### `microsoft_teams_write_channel`

View File

@@ -201,8 +201,9 @@ Leer correos electrónicos desde Outlook
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `folder` | string | No | ID de la carpeta desde donde leer los correos electrónicos \(predeterminado: Bandeja de entrada\) |
| `folder` | string | No | ID de la carpeta para leer correos electrónicos \(predeterminado: Bandeja de entrada\) |
| `maxResults` | number | No | Número máximo de correos electrónicos a recuperar \(predeterminado: 1, máximo: 10\) |
| `includeAttachments` | boolean | No | Descargar e incluir archivos adjuntos de correo electrónico |
#### Salida
@@ -210,6 +211,7 @@ Leer correos electrónicos desde Outlook
| --------- | ---- | ----------- |
| `message` | string | Mensaje de éxito o estado |
| `results` | array | Array de objetos de mensajes de correo electrónico |
| `attachments` | file[] | Todos los archivos adjuntos de correo electrónico aplanados de todos los correos |
### `outlook_forward`

View File

@@ -0,0 +1,242 @@
---
title: Zep
description: Memoria a largo plazo para agentes de IA
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="zep"
color="#E8E8E8"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 233 196'
>
<path
d='m231.34,108.7l-1.48-1.55h-10.26l3.59-75.86-14.8-.45-2.77,49.31c-59.6-3.24-119.33-3.24-178.92-.02l-1.73-64.96-14.8.45,2.5,91.53H2.16l-1.41,1.47c-1.55,16.23-.66,32.68,2.26,48.89h10.83l.18,1.27c.67,19.34,16.1,34.68,35.9,34.68s44.86-.92,66.12-.92,46.56.92,65.95.92,35.19-15.29,35.9-34.61l.16-1.34h11.02c2.91-16.19,3.81-32.61,2.26-48.81Zm-158.23,58.01c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Zm86.51,0c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Z'
fill='#FF1493'
/>
<polygon
points='111.77 22.4 93.39 49.97 93.52 50.48 185.88 38.51 190.95 27.68 114.32 36.55 117.7 31.48 117.7 31.47 138.38 .49 138.25 0 47.67 11.6 42.85 22.27 118.34 12.61 111.77 22.4'
fill='#FF1493'
/>
<path
d='m72.97,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
<path
d='m159.48,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
</svg>`}
/>
## Instrucciones de uso
Integra Zep para la gestión de memoria a largo plazo. Crea hilos, añade mensajes, recupera contexto con resúmenes potenciados por IA y extracción de datos relevantes.
## Herramientas
### `zep_create_thread`
Iniciar un nuevo hilo de conversación en Zep
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Sí | Identificador único para el hilo |
| `userId` | string | Sí | ID de usuario asociado con el hilo |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `threadId` | string | El ID del hilo |
| `userId` | string | El ID del usuario |
| `uuid` | string | UUID interno |
| `createdAt` | string | Marca de tiempo de creación |
| `projectUuid` | string | UUID del proyecto |
### `zep_get_threads`
Listar todos los hilos de conversación
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `pageSize` | number | No | Número de hilos a recuperar por página |
| `pageNumber` | number | No | Número de página para paginación |
| `orderBy` | string | No | Campo para ordenar resultados \(created_at, updated_at, user_id, thread_id\) |
| `asc` | boolean | No | Dirección de ordenamiento: true para ascendente, false para descendente |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `threads` | array | Array de objetos de hilo |
| `responseCount` | number | Número de hilos en esta respuesta |
| `totalCount` | number | Número total de hilos disponibles |
### `zep_delete_thread`
Eliminar un hilo de conversación de Zep
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Sí | ID del hilo a eliminar |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `deleted` | boolean | Indica si el hilo fue eliminado |
### `zep_get_context`
Recuperar el contexto del usuario de un hilo con modo resumen o básico
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Sí | ID del hilo del que obtener el contexto |
| `mode` | string | No | Modo de contexto: "summary" \(lenguaje natural\) o "basic" \(hechos sin procesar\) |
| `minRating` | number | No | Calificación mínima para filtrar hechos relevantes |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `context` | string | La cadena de contexto \(resumen o básico\) |
| `facts` | array | Hechos extraídos |
| `entities` | array | Entidades extraídas |
| `summary` | string | Resumen de la conversación |
### `zep_get_messages`
Recuperar mensajes de un hilo
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Sí | ID del hilo del que obtener mensajes |
| `limit` | number | No | Número máximo de mensajes a devolver |
| `cursor` | string | No | Cursor para paginación |
| `lastn` | number | No | Número de mensajes más recientes a devolver \(anula el límite y el cursor\) |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `messages` | array | Array de objetos de mensaje |
| `rowCount` | number | Número de mensajes en esta respuesta |
| `totalCount` | number | Número total de mensajes en el hilo |
### `zep_add_messages`
Añadir mensajes a un hilo existente
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Sí | ID del hilo al que añadir mensajes |
| `messages` | json | Sí | Array de objetos de mensaje con rol y contenido |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `context` | string | Contexto actualizado después de añadir mensajes |
| `messageIds` | array | Array de UUIDs de mensajes añadidos |
| `threadId` | string | El ID del hilo |
### `zep_add_user`
Crear un nuevo usuario en Zep
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `userId` | string | Sí | Identificador único para el usuario |
| `email` | string | No | Dirección de correo electrónico del usuario |
| `firstName` | string | No | Nombre del usuario |
| `lastName` | string | No | Apellido del usuario |
| `metadata` | json | No | Metadatos adicionales como objeto JSON |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `userId` | string | El ID del usuario |
| `email` | string | Correo electrónico del usuario |
| `firstName` | string | Nombre del usuario |
| `lastName` | string | Apellido del usuario |
| `uuid` | string | UUID interno |
| `createdAt` | string | Marca de tiempo de creación |
| `metadata` | object | Metadatos del usuario |
### `zep_get_user`
Recuperar información del usuario de Zep
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `userId` | string | Sí | ID del usuario a recuperar |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `userId` | string | El ID del usuario |
| `email` | string | Correo electrónico del usuario |
| `firstName` | string | Nombre del usuario |
| `lastName` | string | Apellido del usuario |
| `uuid` | string | UUID interno |
| `createdAt` | string | Marca de tiempo de creación |
| `updatedAt` | string | Marca de tiempo de última actualización |
| `metadata` | object | Metadatos del usuario |
### `zep_get_user_threads`
Listar todos los hilos de conversación para un usuario específico
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `userId` | string | Sí | ID del usuario para obtener los hilos |
| `limit` | number | No | Número máximo de hilos a devolver |
| `apiKey` | string | Sí | Tu clave API de Zep |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `threads` | array | Array de objetos de hilo para este usuario |
| `userId` | string | El ID del usuario |
## Notas
- Categoría: `tools`
- Tipo: `zep`

View File

@@ -8,213 +8,213 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow.
Le bloc Guardrails valide et protège vos flux de travail IA en vérifiant le contenu selon plusieurs types de validation. Assurez la qualité des données, prévenez les hallucinations, détectez les PII et imposez des exigences de format avant que le contenu ne circule dans votre flux de travail.
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails.png"
alt="Guardrails Block"
alt="Bloc Guardrails"
width={500}
height={350}
className="my-6"
/>
</div>
## Overview
## Aperçu
The Guardrails block enables you to:
Le bloc Guardrails vous permet de :
<Steps>
<Step>
<strong>Validate JSON Structure</strong>: Ensure LLM outputs are valid JSON before parsing
<strong>Valider la structure JSON</strong> : garantir que les sorties LLM sont en JSON valide avant l'analyse
</Step>
<Step>
<strong>Match Regex Patterns</strong>: Verify content matches specific formats (emails, phone numbers, URLs, etc.)
<strong>Correspondre aux modèles Regex</strong> : rifier que le contenu correspond à des formats spécifiques (emails, numéros de téléphone, URLs, etc.)
</Step>
<Step>
<strong>Detect Hallucinations</strong>: Use RAG + LLM scoring to validate AI outputs against knowledge base content
<strong>Détecter les hallucinations</strong> : utiliser le scoring RAG + LLM pour valider les sorties IA par rapport au contenu de la base de connaissances
</Step>
<Step>
<strong>Detect PII</strong>: Identify and optionally mask personally identifiable information across 40+ entity types
<strong>Détecter les PII</strong> : identifier et éventuellement masquer les informations personnellement identifiables à travers plus de 40 types d'entités
</Step>
</Steps>
## Validation Types
## Types de validation
### JSON Validation
### Validation JSON
Validates that content is properly formatted JSON. Perfect for ensuring structured LLM outputs can be safely parsed.
Vérifie que le contenu est correctement formaté en JSON. Parfait pour s'assurer que les sorties LLM structurées peuvent être analysées en toute sécurité.
**Use Cases:**
- Validate JSON responses from Agent blocks before parsing
- Ensure API payloads are properly formatted
- Check structured data integrity
**Cas d'utilisation :**
- Valider les réponses JSON des blocs Agent avant l'analyse
- S'assurer que les charges utiles API sont correctement formatées
- Vérifier l'intégrité des données structurées
**Output:**
- `passed`: `true` if valid JSON, `false` otherwise
- `error`: Error message if validation fails (e.g., "Invalid JSON: Unexpected token...")
- `passed`: `true` si le JSON est valide, `false` sinon
- `error`: Message d'erreur si la validation échoue (par ex., "JSON invalide : Token inattendu...")
### Regex Validation
### Validation Regex
Checks if content matches a specified regular expression pattern.
Vérifie si le contenu correspond à un modèle d'expression régulière spécifié.
**Use Cases:**
- Validate email addresses
- Check phone number formats
- Verify URLs or custom identifiers
- Enforce specific text patterns
**Cas d'utilisation :**
- Valider les adresses email
- Vérifier les formats de numéros de téléphone
- Vérifier les URLs ou identifiants personnalisés
- Imposer des modèles de texte spécifiques
**Configuration:**
- **Regex Pattern**: The regular expression to match against (e.g., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` for emails)
**Configuration :**
- **Modèle Regex** : L'expression régulière à faire correspondre (par ex., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` pour les emails)
**Output:**
- `passed`: `true` if content matches pattern, `false` otherwise
- `error`: Error message if validation fails
- `passed`: `true` si le contenu correspond au modèle, `false` sinon
- `error`: Message d'erreur si la validation échoue
### Hallucination Detection
### Détection d'hallucination
Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-generated content contradicts or isn't grounded in your knowledge base.
Utilise la génération augmentée par récupération (RAG) avec notation par LLM pour détecter quand le contenu généré par l'IA contredit ou n'est pas fondé sur votre base de connaissances.
**How It Works:**
1. Queries your knowledge base for relevant context
2. Sends both the AI output and retrieved context to an LLM
3. LLM assigns a confidence score (0-10 scale)
- **0** = Full hallucination (completely ungrounded)
- **10** = Fully grounded (completely supported by knowledge base)
4. Validation passes if score ≥ threshold (default: 3)
**Comment ça fonctionne :**
1. Interroge votre base de connaissances pour obtenir un contexte pertinent
2. Envoie à la fois la sortie de l'IA et le contexte récupéré à un LLM
3. Le LLM attribue un score de confiance (échelle de 0 à 10)
- **0** = Hallucination complète (totalement non fondée)
- **10** = Entièrement fondé (complètement soutenu par la base de connaissances)
4. La validation réussit si le score ≥ seuil (par défaut : 3)
**Configuration:**
- **Knowledge Base**: Select from your existing knowledge bases
- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended)
- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama models)
- **Confidence Threshold**: Minimum score to pass (0-10, default: 3)
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10)
**Configuration :**
- **Base de connaissances** : Sélectionnez parmi vos bases de connaissances existantes
- **Modèle** : Choisissez un LLM pour la notation (nécessite un raisonnement solide - GPT-4o, Claude 3.7 Sonnet recommandés)
- **Clé API** : Authentification pour le fournisseur LLM sélectionné (masquée automatiquement pour les modèles hébergés/Ollama)
- **Seuil de confiance** : Score minimum pour réussir (0-10, par défaut : 3)
- **Top K** (Avancé) : Nombre de fragments de base de connaissances à récupérer (par défaut : 10)
**Output:**
- `passed`: `true` if confidence score ≥ threshold
- `score`: Confidence score (0-10)
- `reasoning`: LLM's explanation for the score
- `error`: Error message if validation fails
- `passed`: `true` si le score de confiance ≥ seuil
- `score`: Score de confiance (0-10)
- `reasoning`: Explication du LLM pour le score
- `error`: Message d'erreur si la validation échoue
**Use Cases:**
- Validate Agent responses against documentation
- Ensure customer support answers are factually accurate
- Verify generated content matches source material
- Quality control for RAG applications
**Cas d'utilisation :**
- Valider les réponses des agents par rapport à la documentation
- Assurer que les réponses du support client sont factuellement exactes
- Vérifier que le contenu généré correspond au matériel source
- Contrôle qualité pour les applications RAG
### PII Detection
### tection de PII
Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages.
Détecte les informations personnellement identifiables à l'aide de Microsoft Presidio. Prend en charge plus de 40 types d'entités dans plusieurs pays et langues.
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**How It Works:**
1. Scans content for PII entities using pattern matching and NLP
2. Returns detected entities with locations and confidence scores
3. Optionally masks detected PII in the output
**Comment ça fonctionne :**
1. Analyse le contenu pour détecter les entités PII en utilisant la correspondance de modèles et le NLP
2. Renvoie les entités détectées avec leurs emplacements et scores de confiance
3. Masque optionnellement les PII détectées dans la sortie
**Configuration:**
- **PII Types to Detect**: Select from grouped categories via modal selector
- **Common**: Person name, Email, Phone, Credit card, IP address, etc.
- **USA**: SSN, Driver's license, Passport, etc.
- **UK**: NHS number, National insurance number
- **Spain**: NIF, NIE, CIF
- **Italy**: Fiscal code, Driver's license, VAT code
- **Poland**: PESEL, NIP, REGON
- **Singapore**: NRIC/FIN, UEN
- **Australia**: ABN, ACN, TFN, Medicare
- **India**: Aadhaar, PAN, Passport, Voter number
- **Mode**:
- **Detect**: Only identify PII (default)
- **Mask**: Replace detected PII with masked values
- **Language**: Detection language (default: English)
**Configuration :**
- **Types de PII à détecter** : Sélectionnez parmi les catégories groupées via le sélecteur modal
- **Commun** : Nom de personne, Email, Téléphone, Carte de crédit, Adresse IP, etc.
- **USA** : SSN, Permis de conduire, Passeport, etc.
- **Royaume-Uni** : Numéro NHS, Numéro d'assurance nationale
- **Espagne** : NIF, NIE, CIF
- **Italie** : Code fiscal, Permis de conduire, Code TVA
- **Pologne** : PESEL, NIP, REGON
- **Singapour** : NRIC/FIN, UEN
- **Australie** : ABN, ACN, TFN, Medicare
- **Inde** : Aadhaar, PAN, Passeport, Numéro d'électeur
- **Mode** :
- **Détecter** : Identifier uniquement les PII (par défaut)
- **Masquer** : Remplacer les PII détectées par des valeurs masquées
- **Langue** : Langue de détection (par défaut : anglais)
**Output:**
- `passed`: `false` if any selected PII types are detected
- `detectedEntities`: Array of detected PII with type, location, and confidence
- `maskedText`: Content with PII masked (only if mode = "Mask")
- `error`: Error message if validation fails
**Sortie :**
- `passed` : `false` si des types de PII sélectionnés sont détectés
- `detectedEntities` : Tableau des PII détectées avec type, emplacement et niveau de confiance
- `maskedText` : Contenu avec PII masquées (uniquement si mode = "Mask")
- `error` : Message d'erreur si la validation échoue
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Sanitize user inputs before processing
**Cas d'utilisation :**
- Bloquer le contenu contenant des informations personnelles sensibles
- Masquer les PII avant la journalisation ou le stockage des données
- Conformité avec le RGPD, HIPAA et autres réglementations sur la confidentialité
- Assainir les entrées utilisateur avant traitement
## Configuration
### Content to Validate
### Contenu à valider
The input content to validate. This typically comes from:
- Agent block outputs: `<agent.content>`
- Function block results: `<function.output>`
- API responses: `<api.output>`
- Any other block output
Le contenu d'entrée à valider. Cela provient généralement de :
- Sorties de blocs d'agent : `<agent.content>`
- Résultats de blocs de fonction : `<function.output>`
- ponses API : `<api.output>`
- Toute autre sortie de bloc
### Validation Type
### Type de validation
Choose from four validation types:
- **Valid JSON**: Check if content is properly formatted JSON
- **Regex Match**: Verify content matches a regex pattern
- **Hallucination Check**: Validate against knowledge base with LLM scoring
- **PII Detection**: Detect and optionally mask personally identifiable information
Choisissez parmi quatre types de validation :
- **JSON valide** : Vérifier si le contenu est au format JSON correctement formaté
- **Correspondance Regex** : Vérifier si le contenu correspond à un modèle regex
- **Vérification d'hallucination** : Valider par rapport à une base de connaissances avec notation LLM
- **tection de PII** : Détecter et éventuellement masquer les informations personnellement identifiables
## Outputs
## Sorties
All validation types return:
Tous les types de validation renvoient :
- **`<guardrails.passed>`**: Boolean indicating if validation passed
- **`<guardrails.validationType>`**: The type of validation performed
- **`<guardrails.input>`**: The original input that was validated
- **`<guardrails.error>`**: Error message if validation failed (optional)
- **`<guardrails.passed>`** : Booléen indiquant si la validation a réussi
- **`<guardrails.validationType>`** : Le type de validation effectuée
- **`<guardrails.input>`** : L'entrée originale qui a été validée
- **`<guardrails.error>`** : Message d'erreur si la validation a échoué (facultatif)
Additional outputs by type:
Sorties supplémentaires par type :
**Hallucination Check:**
- **`<guardrails.score>`**: Confidence score (0-10)
- **`<guardrails.reasoning>`**: LLM's explanation
**Vérification d'hallucination :**
- **`<guardrails.score>`** : Score de confiance (0-10)
- **`<guardrails.reasoning>`** : Explication du LLM
**PII Detection:**
- **`<guardrails.detectedEntities>`**: Array of detected PII entities
- **`<guardrails.maskedText>`**: Content with PII masked (if mode = "Mask")
**tection de PII :**
- **`<guardrails.detectedEntities>`** : Tableau des entités PII détectées
- **`<guardrails.maskedText>`** : Contenu avec PII masquées (si mode = "Mask")
## Example Use Cases
## Exemples de cas d'utilisation
### Validate JSON Before Parsing
### Valider le JSON avant l'analyse
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Ensure Agent output is valid JSON</h4>
<h4 className="font-medium">Scénario : s'assurer que la sortie de l'agent est un JSON valide</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates structured JSON response</li>
<li>Guardrails validates JSON format</li>
<li>Condition block checks `<guardrails.passed>`</li>
<li>If passed → Parse and use data, If failed → Retry or handle error</li>
<li>L'agent génère une réponse JSON structurée</li>
<li>Guardrails valide le format JSON</li>
<li>Le bloc de condition vérifie `<guardrails.passed>`</li>
<li>Si réussi → Analyser et utiliser les données, Si échoué → Réessayer ou gérer l'erreur</li>
</ol>
</div>
### Prevent Hallucinations
### Prévenir les hallucinations
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Validate customer support responses</h4>
<h4 className="font-medium">Scénario : valider les réponses du support client</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates response to customer question</li>
<li>Guardrails checks against support documentation knowledge base</li>
<li>If confidence score ≥ 3 → Send response</li>
<li>If confidence score \< 3 → Flag for human review</li>
<li>L'agent génère une réponse à la question du client</li>
<li>Guardrails vérifie par rapport à la base de connaissances de la documentation d'assistance</li>
<li>Si le score de confiance ≥ 3 → Envoyer la réponse</li>
<li>Si le score de confiance \< 3 → Signaler pour révision humaine</li>
</ol>
</div>
### Block PII in User Inputs
### Bloquer les PII dans les entrées utilisateur
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Sanitize user-submitted content</h4>
<h4 className="font-medium">Scénario : assainir le contenu soumis par l'utilisateur</h4>
<ol className="list-decimal pl-5 text-sm">
<li>User submits form with text content</li>
<li>Guardrails detects PII (emails, phone numbers, SSN, etc.)</li>
<li>If PII detected → Reject submission or mask sensitive data</li>
<li>If no PII → Process normally</li>
<li>L'utilisateur soumet un formulaire avec du contenu textuel</li>
<li>Guardrails détecte les PII (emails, numéros de téléphone, numéros de sécurité sociale, etc.)</li>
<li>Si PII détectées → Rejeter la soumission ou masquer les données sensibles</li>
<li>Si aucune PII → Traiter normalement</li>
</ol>
</div>
@@ -222,30 +222,29 @@ Additional outputs by type:
<Video src="guardrails-example.mp4" width={500} height={350} />
</div>
### Validate Email Format
### Valider le format d'email
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Check email address format</h4>
<h4 className="font-medium">Scénario : vérifier le format d'adresse email</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent extracts email from text</li>
<li>Guardrails validates with regex pattern</li>
<li>If valid → Use email for notification</li>
<li>If invalid → Request correction</li>
<li>L'agent extrait l'email du texte</li>
<li>Guardrails valide avec un modèle d'expression régulière</li>
<li>Si valide → Utiliser l'email pour la notification</li>
<li>Si invalideDemander une correction</li>
</ol>
</div>
## Best Practices
## Bonnes pratiques
- **Chain with Condition blocks**: Use `<guardrails.passed>` to branch workflow logic based on validation results
- **Use JSON validation before parsing**: Always validate JSON structure before attempting to parse LLM outputs
- **Choose appropriate PII types**: Only select the PII entity types relevant to your use case for better performance
- **Set reasonable confidence thresholds**: For hallucination detection, adjust threshold based on your accuracy requirements (higher = stricter)
- **Use strong models for hallucination detection**: GPT-4o or Claude 3.7 Sonnet provide more accurate confidence scoring
- **Mask PII for logging**: Use "Mask" mode when you need to log or store content that may contain PII
- **Test regex patterns**: Validate your regex patterns thoroughly before deploying to production
- **Monitor validation failures**: Track `<guardrails.error>` messages to identify common validation issues
- **Chaîner avec des blocs Condition** : Utilisez `<guardrails.passed>` pour créer des branches dans la logique du workflow selon les résultats de validation
- **Valider le JSON avant l'analyse** : Toujours valider la structure JSON avant de tenter d'analyser les sorties du LLM
- **Choisir les types de PII appropriés** : Sélectionnez uniquement les types d'entités PII pertinents pour votre cas d'utilisation afin d'améliorer les performances
- **Définir des seuils de confiance raisonnables** : Pour la détection d'hallucinations, ajustez le seuil selon vos exigences de précision (plus élevé = plus strict)
- **Utiliser des modèles performants pour la détection d'hallucinations** : GPT-4o ou Claude 3.7 Sonnet fournissent un score de confiance plus précis
- **Masquer les PII pour la journalisation** : Utilisez le mode « Mask » lorsque vous devez journaliser ou stocker du contenu susceptible de contenir des PII
- **Tester les modèles regex** : Validez soigneusement vos modèles d'expressions régulières avant de les déployer en production
- **Surveiller les échecs de validation** : Suivez les messages `<guardrails.error>` pour identifier les problèmes de validation courants
<Callout type="info">
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
La validation des guardrails s'effectue de manière synchrone dans votre workflow. Pour la détection d'hallucinations, choisissez des modèles plus rapides (comme GPT-4o-mini) si la latence est critique.
</Callout>

View File

@@ -535,7 +535,7 @@ app.post('/sim-webhook', (req, res) => {
// Handle error...
} else {
console.log(`Workflow ${workflowId} completed: ${executionId}`);
console.log(`Cost: ${cost.total}`);
console.log(`Cost: $${cost.total}`);
// Process successful execution...
}
break;

View File

@@ -1,5 +1,5 @@
---
title: TypeScript/JavaScript SDK
title: SDK TypeScript/JavaScript
---
import { Callout } from 'fumadocs-ui/components/callout'
@@ -7,10 +7,10 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Le SDK officiel TypeScript/JavaScript pour Sim offre une sécurité de type complète et prend en charge les environnements Node.js et navigateur, vous permettant d'exécuter des workflows par programmation depuis vos applications Node.js, applications web et autres environnements JavaScript.
Le SDK officiel TypeScript/JavaScript pour Sim offre une sécurité de type complète et prend en charge les environnements Node.js et navigateur, vous permettant d'exécuter des flux de travail par programmation depuis vos applications Node.js, applications web et autres environnements JavaScript.
<Callout type="info">
Le SDK TypeScript offre une sécurité de type complète, la prise en charge de l'exécution asynchrone, une limitation automatique du débit avec backoff exponentiel et le suivi d'utilisation.
Le SDK TypeScript fournit une sécurité de type complète, une prise en charge de l'exécution asynchrone, une limitation automatique du débit avec backoff exponentiel et un suivi d'utilisation.
</Callout>
## Installation
@@ -43,7 +43,7 @@ Installez le SDK en utilisant votre gestionnaire de paquets préféré :
## Démarrage rapide
Voici un exemple simple pour commencer :
Voici un exemple simple pour vous aider à démarrer :
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -74,14 +74,14 @@ new SimStudioClient(config: SimStudioConfig)
```
**Configuration :**
- `config.apiKey` (string) : votre clé API Sim
- `config.apiKey` (string) : Votre clé API Sim
- `config.baseUrl` (string, optionnel) : URL de base pour l'API Sim (par défaut `https://sim.ai`)
#### Méthodes
##### executeWorkflow()
Exécuter un workflow avec des données d'entrée optionnelles.
Exécuter un flux de travail avec des données d'entrée optionnelles.
```typescript
const result = await client.executeWorkflow('workflow-id', {
@@ -91,17 +91,17 @@ const result = await client.executeWorkflow('workflow-id', {
```
**Paramètres :**
- `workflowId` (string) : L'ID du workflow à exécuter
- `options` (ExecutionOptions, optionnel) :
- `workflowId` (string) : L'identifiant du workflow à exécuter
- `options` (ExecutionOptions, facultatif) :
- `input` (any) : Données d'entrée à transmettre au workflow
- `timeout` (number) : Délai d'expiration en millisecondes (par défaut : 30000)
- `stream` (boolean) : Activer les réponses en streaming (par défaut : false)
- `selectedOutputs` (string[]) : Bloquer les sorties à diffuser au format `blockName.attribute` (par exemple, `["agent1.content"]`)
- `selectedOutputs` (string[]) : Sorties de blocs à diffuser au format `blockName.attribute` (par exemple, `["agent1.content"]`)
- `async` (boolean) : Exécuter de manière asynchrone (par défaut : false)
**Retourne :** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
Lorsque `async: true`, retourne immédiatement avec un ID de tâche pour l'interrogation. Sinon, attend la fin de l'exécution.
Lorsque `async: true`, retourne immédiatement un identifiant de tâche pour l'interrogation. Sinon, attend la fin de l'exécution.
##### getWorkflowStatus()
@@ -113,7 +113,7 @@ console.log('Is deployed:', status.isDeployed);
```
**Paramètres :**
- `workflowId` (string) : L'ID du workflow
- `workflowId` (string) : L'identifiant du workflow
**Retourne :** `Promise<WorkflowStatus>`
@@ -129,7 +129,7 @@ if (isReady) {
```
**Paramètres :**
- `workflowId` (string) : L'ID du workflow
- `workflowId` (string) : L'identifiant du workflow
**Retourne :** `Promise<boolean>`
@@ -146,22 +146,22 @@ if (status.status === 'completed') {
```
**Paramètres :**
- `taskId` (string) : L'ID de tâche retourné par l'exécution asynchrone
- `taskId` (string) : L'identifiant de tâche retourné par l'exécution asynchrone
**Retourne :** `Promise<JobStatus>`
**Champs de réponse :**
- `success` (boolean) : Indique si la requête a réussi
- `taskId` (string) : L'ID de la tâche
- `status` (string) : L'un des statuts suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `metadata` (object) : Contient `startedAt`, `completedAt`, et `duration`
- `output` (any, optionnel) : La sortie du workflow (une fois terminé)
- `error` (any, optionnel) : Détails de l'erreur (en cas d'échec)
- `estimatedDuration` (number, optionnel) : Durée estimée en millisecondes (lorsqu'en traitement/en file d'attente)
- `taskId` (string) : L'identifiant de la tâche
- `status` (string) : L'un des états suivants : `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `metadata` (object) : Contient `startedAt`, `completedAt` et `duration`
- `output` (any, facultatif) : La sortie du workflow (une fois terminé)
- `error` (any, facultatif) : Détails de l'erreur (en cas d'échec)
- `estimatedDuration` (number, facultatif) : Durée estimée en millisecondes (lorsqu'en traitement/en file d'attente)
##### executeWithRetry()
Exécute un workflow avec une nouvelle tentative automatique en cas d'erreurs de limite de débit en utilisant un backoff exponentiel.
Exécuter un workflow avec une nouvelle tentative automatique en cas d'erreurs de limitation de débit, en utilisant un backoff exponentiel.
```typescript
const result = await client.executeWithRetry('workflow-id', {
@@ -186,11 +186,11 @@ const result = await client.executeWithRetry('workflow-id', {
**Retourne :** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
La logique de nouvelle tentative utilise un backoff exponentiel (1s → 2s → 4s → 8s...) avec une variation aléatoire de ±25 % pour éviter l'effet de rafale. Si l'API fournit un en-tête `retry-after`, celui-ci sera utilisé à la place.
La logique de nouvelle tentative utilise un backoff exponentiel (1s → 2s → 4s → 8s...) avec une variation aléatoire de ±25 % pour éviter l'effet de horde. Si l'API fournit un en-tête `retry-after`, celui-ci sera utilisé à la place.
##### getRateLimitInfo()
Obtient les informations actuelles sur les limites de débit à partir de la dernière réponse de l'API.
Obtenir les informations actuelles de limitation de débit à partir de la dernière réponse de l'API.
```typescript
const rateLimitInfo = client.getRateLimitInfo();
@@ -205,7 +205,7 @@ if (rateLimitInfo) {
##### getUsageLimits()
Obtient les limites d'utilisation actuelles et les informations de quota pour votre compte.
Obtenir les limites d'utilisation actuelles et les informations de quota pour votre compte.
```typescript
const limits = await client.getUsageLimits();
@@ -247,7 +247,7 @@ console.log('Plan:', limits.usage.plan);
##### setApiKey()
Met à jour la clé API.
Mettre à jour la clé API.
```typescript
client.setApiKey('new-api-key');
@@ -255,7 +255,7 @@ client.setApiKey('new-api-key');
##### setBaseUrl()
Met à jour l'URL de base.
Mettre à jour l'URL de base.
```typescript
client.setBaseUrl('https://my-custom-domain.com');
@@ -356,7 +356,7 @@ class SimStudioError extends Error {
**Codes d'erreur courants :**
- `UNAUTHORIZED` : Clé API invalide
- `TIMEOUT` : Délai d'attente de la requête dépassé
- `TIMEOUT` : Délai d'attente dépassé
- `RATE_LIMIT_EXCEEDED` : Limite de débit dépassée
- `USAGE_LIMIT_EXCEEDED` : Limite d'utilisation dépassée
- `EXECUTION_ERROR` : Échec de l'exécution du workflow
@@ -501,7 +501,7 @@ Configurez le client en utilisant des variables d'environnement :
</Tab>
</Tabs>
### Intégration avec Express de Node.js
### Intégration avec Node.js Express
Intégration avec un serveur Express.js :
@@ -604,26 +604,105 @@ async function executeClientSideWorkflow() {
});
console.log('Workflow result:', result);
// Update UI with result
document.getElementById('result')!.textContent =
document.getElementById('result')!.textContent =
JSON.stringify(result.output, null, 2);
} catch (error) {
console.error('Error:', error);
}
}
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
```
### Téléchargement de fichiers
Les objets File sont automatiquement détectés et convertis au format base64. Incluez-les dans votre entrée sous le nom de champ correspondant au format d'entrée du déclencheur API de votre workflow.
Le SDK convertit les objets File dans ce format :
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
Alternativement, vous pouvez fournir manuellement des fichiers en utilisant le format URL :
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
<Tabs items={['Browser', 'Node.js']}>
<Tab value="Browser">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.NEXT_PUBLIC_SIM_API_KEY!
});
// From file input
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files || []);
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: files, // Must match your workflow's "files" field name
instructions: 'Analyze these documents'
}
});
console.log('Result:', result);
}
```
</Tab>
<Tab value="Node.js">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
import fs from 'fs';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
// Read file and create File object
const fileBuffer = fs.readFileSync('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', {
type: 'application/pdf'
});
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: [file], // Must match your workflow's "files" field name
query: 'Summarize this document'
}
});
```
</Tab>
</Tabs>
<Callout type="warning">
Lors de l'utilisation du SDK dans le navigateur, veillez à ne pas exposer de clés API sensibles. Envisagez d'utiliser un proxy backend ou des clés API publiques avec des permissions limitées.
Lorsque vous utilisez le SDK dans le navigateur, faites attention à ne pas exposer des clés API sensibles. Envisagez d'utiliser un proxy backend ou des clés API publiques avec des permissions limitées.
</Callout>
### Exemple de hook React
Créer un hook React personnalisé pour l'exécution de workflow :
Créez un hook React personnalisé pour l'exécution du workflow :
```typescript
import { useState, useCallback } from 'react';
@@ -699,9 +778,9 @@ function WorkflowComponent() {
}
```
### Exécution asynchrone de workflow
### Exécution asynchrone du workflow
Exécuter des workflows de manière asynchrone pour les tâches de longue durée :
Exécutez des workflows de manière asynchrone pour les tâches de longue durée :
```typescript
import { SimStudioClient, AsyncExecutionResult } from 'simstudio-ts-sdk';
@@ -750,7 +829,7 @@ executeAsync();
### Limitation de débit et nouvelle tentative
Gérer automatiquement les limites de débit avec backoff exponentiel :
Gérez automatiquement les limites de débit avec un backoff exponentiel :
```typescript
import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk';
@@ -786,9 +865,9 @@ async function executeWithRetryHandling() {
}
```
### Surveillance d'utilisation
### Surveillance de l'utilisation
Surveiller l'utilisation et les limites de votre compte :
Surveillez l'utilisation et les limites de votre compte :
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -814,28 +893,28 @@ async function checkUsage() {
console.log(' Resets at:', limits.rateLimit.async.resetAt);
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Utilisation ===');
console.log('Coût de la période actuelle : ' + limits.usage.currentPeriodCost.toFixed(2) + ' $');
console.log('Limite : ' + limits.usage.limit.toFixed(2) + ' $');
console.log('Forfait :', limits.usage.plan);
console.log('\n=== Usage ===');
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Utilisation : ' + percentUsed.toFixed(1) + ' %');
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
if (percentUsed > 80) {
console.warn('⚠️ Attention : vous approchez de votre limite d\'utilisation !');
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Erreur lors de la vérification de l\'utilisation :', error);
console.error('Error checking usage:', error);
}
}
checkUsage();
```
### Exécution de flux de travail avec streaming
### Exécution de workflow en streaming
Exécutez des flux de travail avec des réponses en streaming en temps réel :
Exécutez des workflows avec des réponses en streaming en temps réel :
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -846,40 +925,35 @@ const client = new SimStudioClient({
async function executeWithStreaming() {
try {
// Activer le streaming pour des sorties de blocs spécifiques
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', {
input: { message: 'Compter jusqu'à cinq' },
input: { message: 'Count to five' },
stream: true,
selectedOutputs: ['agent1.content'] // Utiliser le format blockName.attribute
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
});
console.log('Résultat du workflow :', result);
console.log('Workflow result:', result);
} catch (error) {
console.error('Erreur :', error);
console.error('Error:', error);
}
}
```
The streaming response follows the Server-Sent Events (SSE) format:
La réponse en streaming suit le format Server-Sent Events (SSE) :
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", deux"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**React Streaming Example:**
**Exemple de streaming avec React :**
```
typescript
```typescript
import { useState, useEffect } from 'react';
function StreamingWorkflow() {
@@ -941,44 +1015,43 @@ function StreamingWorkflow() {
return (
<div>
<button onClick={executeStreaming} disabled={loading}>
{loading ? 'Génération en cours...' : 'Démarrer le streaming'}
{loading ? 'Generating...' : 'Start Streaming'}
</button>
<div style={{ whiteSpace: 'pre-wrap' }}>{output}</div>
</div>
);
}
```
## Getting Your API Key
## Obtenir votre clé API
<Steps>
<Step title="Log in to Sim">
Navigate to [Sim](https://sim.ai) and log in to your account.
<Step title="Connectez-vous à Sim">
Accédez à [Sim](https://sim.ai) et connectez-vous à votre compte.
</Step>
<Step title="Open your workflow">
Navigate to the workflow you want to execute programmatically.
<Step title="Ouvrez votre workflow">
Accédez au workflow que vous souhaitez exécuter par programmation.
</Step>
<Step title="Deploy your workflow">
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
<Step title="Déployez votre workflow">
Cliquez sur "Déployer" pour déployer votre workflow s'il n'a pas encore été déployé.
</Step>
<Step title="Create or select an API key">
During the deployment process, select or create an API key.
<Step title="Créez ou sélectionnez une clé API">
Pendant le processus de déploiement, sélectionnez ou créez une clé API.
</Step>
<Step title="Copy the API key">
Copy the API key to use in your TypeScript/JavaScript application.
<Step title="Copiez la clé API">
Copiez la clé API pour l'utiliser dans votre application TypeScript/JavaScript.
</Step>
</Steps>
<Callout type="warning">
Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management.
Gardez votre clé API en sécurité et ne la publiez jamais dans un système de contrôle de version. Utilisez des variables d'environnement ou une gestion sécurisée de configuration.
</Callout>
## Requirements
## Prérequis
- Node.js 16+
- TypeScript 5.0+ (for TypeScript projects)
- TypeScript 5.0+ (pour les projets TypeScript)
## License
## Licence
Apache-2.0

View File

@@ -23,7 +23,210 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
</svg>`}
/>
## Notes
## Aperçu
Le bloc Webhook générique vous permet de recevoir des webhooks depuis n'importe quel service externe. C'est un déclencheur flexible qui peut traiter n'importe quelle charge utile JSON, ce qui le rend idéal pour l'intégration avec des services qui n'ont pas de bloc Sim dédié.
## Utilisation de base
### Mode de transmission simple
Sans définir un format d'entrée, le webhook transmet l'intégralité du corps de la requête tel quel :
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Accédez aux données dans les blocs en aval en utilisant :
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Format d'entrée structuré (optionnel)
Définissez un schéma d'entrée pour obtenir des champs typés et activer des fonctionnalités avancées comme les téléchargements de fichiers :
**Configuration du format d'entrée :**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Requête webhook :**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## Téléchargements de fichiers
### Formats de fichiers pris en charge
Le webhook prend en charge deux formats d'entrée de fichiers :
#### 1. Fichiers encodés en Base64
Pour télécharger directement le contenu du fichier :
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Taille maximale** : 20 Mo par fichier
- **Format** : URL de données standard avec encodage base64
- **Stockage** : Les fichiers sont téléchargés dans un stockage d'exécution sécurisé
#### 2. Références URL
Pour transmettre des URL de fichiers existants :
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Accès aux fichiers dans les blocs en aval
Les fichiers sont traités en objets `UserFile` avec les propriétés suivantes :
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Accès dans les blocs :**
- `<webhook1.documents[0].url>` → URL de téléchargement
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Exemple complet de téléchargement de fichier
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Authentification
### Configurer l'authentification (facultatif)
Dans la configuration du webhook :
1. Activez "Exiger l'authentification"
2. Définissez un jeton secret
3. Choisissez le type d'en-tête :
- **En-tête personnalisé** : `X-Sim-Secret: your-token`
- **Autorisation Bearer** : `Authorization: Bearer your-token`
### Utilisation de l'authentification
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Bonnes pratiques
1. **Utilisez le format d'entrée pour la structure** : définissez un format d'entrée lorsque vous connaissez le schéma attendu. Cela fournit :
- Validation de type
- Meilleure autocomplétion dans l'éditeur
- Capacités de téléchargement de fichiers
2. **Authentification** : activez toujours l'authentification pour les webhooks de production afin d'empêcher les accès non autorisés.
3. **Limites de taille de fichier** : gardez les fichiers sous 20 Mo. Pour les fichiers plus volumineux, utilisez plutôt des références URL.
4. **Expiration des fichiers** : les fichiers téléchargés ont des URL d'expiration de 5 minutes. Traitez-les rapidement ou stockez-les ailleurs si vous en avez besoin plus longtemps.
5. **Gestion des erreurs** : le traitement des webhooks est asynchrone. Vérifiez les journaux d'exécution pour les erreurs.
6. **Tests** : utilisez le bouton "Tester le webhook" dans l'éditeur pour valider votre configuration avant le déploiement.
## Cas d'utilisation
- **Soumissions de formulaires** : recevez des données de formulaires personnalisés avec téléchargements de fichiers
- **Intégrations tierces** : connectez-vous à des services qui envoient des webhooks (Stripe, GitHub, etc.)
- **Traitement de documents** : acceptez des documents de systèmes externes pour traitement
- **Notifications d'événements** : recevez des données d'événements de diverses sources
- **API personnalisées** : créez des points de terminaison API personnalisés pour vos applications
## Remarques
- Catégorie : `triggers`
- Type : `generic_webhook`
- **Support de fichiers** : disponible via la configuration du format d'entrée
- **Taille maximale de fichier** : 20 Mo par fichier

View File

@@ -108,20 +108,22 @@ Lire le contenu d'un chat Microsoft Teams
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `chatId` | chaîne | Oui | L'ID du chat à lire |
| --------- | ---- | -------- | ----------- |
| `chatId` | chaîne | Oui | L'ID de la conversation à partir de laquelle lire |
| `includeAttachments` | booléen | Non | Télécharger et inclure les pièces jointes des messages \(contenus hébergés\) dans le stockage |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération de lecture du chat Teams |
| `messageCount` | nombre | Nombre de messages récupérés du chat |
| `chatId` | chaîne | ID du chat qui a été lu |
| `messages` | tableau | Tableau d'objets de messages de chat |
| `success` | booléen | Statut de réussite de l'opération de lecture de la conversation Teams |
| `messageCount` | nombre | Nombre de messages récupérés de la conversation |
| `chatId` | chaîne | ID de la conversation qui a été lue |
| `messages` | tableau | Tableau d'objets de messages de conversation |
| `attachmentCount` | nombre | Nombre total de pièces jointes trouvées |
| `attachmentTypes` | tableau | Types de pièces jointes trouvées |
| `content` | chaîne | Contenu formaté des messages de chat |
| `content` | chaîne | Contenu formaté des messages de conversation |
| `attachments` | fichier[] | Pièces jointes téléchargées pour plus de commodité \(aplaties\) |
### `microsoft_teams_write_chat`
@@ -155,6 +157,7 @@ Lire le contenu d'un canal Microsoft Teams
| --------- | ---- | -------- | ----------- |
| `teamId` | chaîne | Oui | L'ID de l'équipe à partir de laquelle lire |
| `channelId` | chaîne | Oui | L'ID du canal à partir duquel lire |
| `includeAttachments` | booléen | Non | Télécharger et inclure les pièces jointes des messages \(contenus hébergés\) dans le stockage |
#### Sortie
@@ -162,12 +165,13 @@ Lire le contenu d'un canal Microsoft Teams
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération de lecture du canal Teams |
| `messageCount` | nombre | Nombre de messages récupérés du canal |
| `teamId` | chaîne | ID de l'équipe à partir de laquelle la lecture a été effectuée |
| `channelId` | chaîne | ID du canal à partir duquel la lecture a été effectuée |
| `teamId` | chaîne | ID de l'équipe qui a été lue |
| `channelId` | chaîne | ID du canal qui a été lu |
| `messages` | tableau | Tableau d'objets de messages du canal |
| `attachmentCount` | nombre | Nombre total de pièces jointes trouvées |
| `attachmentTypes` | tableau | Types de pièces jointes trouvées |
| `content` | chaîne | Contenu formaté des messages du canal |
| `attachments` | fichier[] | Pièces jointes téléchargées pour plus de commodité \(aplaties\) |
### `microsoft_teams_write_channel`

View File

@@ -200,16 +200,18 @@ Lire des e-mails depuis Outlook
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| --------- | ---- | ---------- | ----------- |
| `folder` | string | Non | ID du dossier pour lire les e-mails \(par défaut : Boîte de réception\) |
| `maxResults` | number | Non | Nombre maximum d'e-mails à récupérer \(par défaut : 1, max : 10\) |
| `includeAttachments` | boolean | Non | Télécharger et inclure les pièces jointes des e-mails |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Message de succès ou d'état |
| `results` | array | Tableau d'objets de messages électroniques |
| `message` | string | Message de succès ou de statut |
| `results` | array | Tableau d'objets de messages e-mail |
| `attachments` | file[] | Toutes les pièces jointes des e-mails regroupées de tous les e-mails |
### `outlook_forward`

View File

@@ -0,0 +1,242 @@
---
title: Zep
description: Mémoire à long terme pour les agents IA
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="zep"
color="#E8E8E8"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 233 196'
>
<path
d='m231.34,108.7l-1.48-1.55h-10.26l3.59-75.86-14.8-.45-2.77,49.31c-59.6-3.24-119.33-3.24-178.92-.02l-1.73-64.96-14.8.45,2.5,91.53H2.16l-1.41,1.47c-1.55,16.23-.66,32.68,2.26,48.89h10.83l.18,1.27c.67,19.34,16.1,34.68,35.9,34.68s44.86-.92,66.12-.92,46.56.92,65.95.92,35.19-15.29,35.9-34.61l.16-1.34h11.02c2.91-16.19,3.81-32.61,2.26-48.81Zm-158.23,58.01c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Zm86.51,0c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Z'
fill='#FF1493'
/>
<polygon
points='111.77 22.4 93.39 49.97 93.52 50.48 185.88 38.51 190.95 27.68 114.32 36.55 117.7 31.48 117.7 31.47 138.38 .49 138.25 0 47.67 11.6 42.85 22.27 118.34 12.61 111.77 22.4'
fill='#FF1493'
/>
<path
d='m72.97,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
<path
d='m159.48,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
</svg>`}
/>
## Instructions d'utilisation
Intégrez Zep pour la gestion de la mémoire à long terme. Créez des fils de discussion, ajoutez des messages, récupérez du contexte avec des résumés générés par IA et extraction de faits.
## Outils
### `zep_create_thread`
Démarrer un nouveau fil de conversation dans Zep
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Oui | Identifiant unique pour le fil de discussion |
| `userId` | string | Oui | ID utilisateur associé au fil de discussion |
| `apiKey` | string | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `threadId` | string | L'ID du fil de discussion |
| `userId` | string | L'ID utilisateur |
| `uuid` | string | UUID interne |
| `createdAt` | string | Horodatage de création |
| `projectUuid` | string | UUID du projet |
### `zep_get_threads`
Lister tous les fils de conversation
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `pageSize` | number | Non | Nombre de fils à récupérer par page |
| `pageNumber` | number | Non | Numéro de page pour la pagination |
| `orderBy` | string | Non | Champ pour ordonner les résultats \(created_at, updated_at, user_id, thread_id\) |
| `asc` | boolean | Non | Direction de tri : true pour ascendant, false pour descendant |
| `apiKey` | string | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `threads` | array | Tableau d'objets de fil de discussion |
| `responseCount` | number | Nombre de fils de discussion dans cette réponse |
| `totalCount` | number | Nombre total de fils de discussion disponibles |
### `zep_delete_thread`
Supprimer un fil de conversation de Zep
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Oui | ID du fil de discussion à supprimer |
| `apiKey` | string | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Indique si le fil de discussion a été supprimé |
### `zep_get_context`
Récupérer le contexte utilisateur d'un fil de discussion en mode résumé ou basique
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | Oui | ID du fil de discussion pour obtenir le contexte |
| `mode` | string | Non | Mode de contexte : "summary" \(langage naturel\) ou "basic" \(faits bruts\) |
| `minRating` | number | Non | Note minimale pour filtrer les faits pertinents |
| `apiKey` | string | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `context` | string | La chaîne de contexte \(résumé ou basique\) |
| `facts` | array | Faits extraits |
| `entities` | array | Entités extraites |
| `summary` | string | Résumé de la conversation |
### `zep_get_messages`
Récupérer les messages d'un fil de discussion
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `threadId` | chaîne | Oui | ID du fil de discussion pour récupérer les messages |
| `limit` | nombre | Non | Nombre maximum de messages à renvoyer |
| `cursor` | chaîne | Non | Curseur pour la pagination |
| `lastn` | nombre | Non | Nombre de messages les plus récents à renvoyer \(remplace la limite et le curseur\) |
| `apiKey` | chaîne | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `messages` | tableau | Tableau d'objets message |
| `rowCount` | nombre | Nombre de messages dans cette réponse |
| `totalCount` | nombre | Nombre total de messages dans le fil de discussion |
### `zep_add_messages`
Ajouter des messages à un fil de discussion existant
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `threadId` | chaîne | Oui | ID du fil de discussion auquel ajouter des messages |
| `messages` | json | Oui | Tableau d'objets message avec rôle et contenu |
| `apiKey` | chaîne | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `context` | chaîne | Contexte mis à jour après l'ajout des messages |
| `messageIds` | tableau | Tableau des UUID des messages ajoutés |
| `threadId` | chaîne | L'ID du fil de discussion |
### `zep_add_user`
Créer un nouvel utilisateur dans Zep
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `userId` | string | Oui | Identifiant unique pour l'utilisateur |
| `email` | string | Non | Adresse e-mail de l'utilisateur |
| `firstName` | string | Non | Prénom de l'utilisateur |
| `lastName` | string | Non | Nom de famille de l'utilisateur |
| `metadata` | json | Non | Métadonnées supplémentaires sous forme d'objet JSON |
| `apiKey` | string | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `userId` | string | L'identifiant de l'utilisateur |
| `email` | string | E-mail de l'utilisateur |
| `firstName` | string | Prénom de l'utilisateur |
| `lastName` | string | Nom de famille de l'utilisateur |
| `uuid` | string | UUID interne |
| `createdAt` | string | Horodatage de création |
| `metadata` | object | Métadonnées de l'utilisateur |
### `zep_get_user`
Récupérer les informations d'un utilisateur depuis Zep
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `userId` | string | Oui | ID de l'utilisateur à récupérer |
| `apiKey` | string | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `userId` | string | L'identifiant de l'utilisateur |
| `email` | string | E-mail de l'utilisateur |
| `firstName` | string | Prénom de l'utilisateur |
| `lastName` | string | Nom de famille de l'utilisateur |
| `uuid` | string | UUID interne |
| `createdAt` | string | Horodatage de création |
| `updatedAt` | string | Horodatage de dernière mise à jour |
| `metadata` | object | Métadonnées de l'utilisateur |
### `zep_get_user_threads`
Lister tous les fils de conversation pour un utilisateur spécifique
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `userId` | chaîne | Oui | ID de l'utilisateur pour lequel obtenir les fils |
| `limit` | nombre | Non | Nombre maximum de fils à retourner |
| `apiKey` | chaîne | Oui | Votre clé API Zep |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `threads` | tableau | Tableau d'objets de fils pour cet utilisateur |
| `userId` | chaîne | L'ID de l'utilisateur |
## Notes
- Catégorie : `tools`
- Type : `zep`

View File

@@ -1,5 +1,5 @@
---
title: Guardrails
title: ガードレール
---
import { Callout } from 'fumadocs-ui/components/callout'
@@ -8,213 +8,213 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow.
ガードレールブロックは、複数の検証タイプに対してコンテンツをチェックすることで、AIワークフローを検証し保護します。データ品質の確保、ハルシネーション幻覚の防止、個人情報の検出、フォーマット要件の強制などをワークフローに組み込む前に行います。
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails.png"
alt="Guardrails Block"
alt="ガードレールブロック"
width={500}
height={350}
className="my-6"
/>
</div>
## Overview
## 概要
The Guardrails block enables you to:
ガードレールブロックでは以下のことが可能です:
<Steps>
<Step>
<strong>Validate JSON Structure</strong>: Ensure LLM outputs are valid JSON before parsing
<strong>JSON構造の検証</strong>パース前にLLM出力が有効なJSONであることを確認
</Step>
<Step>
<strong>Match Regex Patterns</strong>: Verify content matches specific formats (emails, phone numbers, URLs, etc.)
<strong>正規表現パターンの一致</strong>コンテンツが特定のフォーマットメール、電話番号、URLなどに一致するか確認
</Step>
<Step>
<strong>Detect Hallucinations</strong>: Use RAG + LLM scoring to validate AI outputs against knowledge base content
<strong>ハルシネーション(幻覚)の検出</strong>RAG + LLMスコアリングを使用してAI出力をナレッジベースコンテンツと照合して検証
</Step>
<Step>
<strong>Detect PII</strong>: Identify and optionally mask personally identifiable information across 40+ entity types
<strong>個人情報の検出</strong>40種類以上のエンティティタイプにわたる個人を特定できる情報を識別し、オプションでマスク処理
</Step>
</Steps>
## Validation Types
## 検証タイプ
### JSON Validation
### JSON検証
Validates that content is properly formatted JSON. Perfect for ensuring structured LLM outputs can be safely parsed.
コンテンツが適切にフォーマットされたJSONであることを検証します。構造化されたLLM出力を安全にパースできることを確認するのに最適です。
**Use Cases:**
- Validate JSON responses from Agent blocks before parsing
- Ensure API payloads are properly formatted
- Check structured data integrity
**ユースケース:**
- パース前にエージェントブロックからのJSON応答を検証
- APIペイロードが適切にフォーマットされていることを確認
- 構造化データの整合性をチェック
**Output:**
- `passed`: `true` if valid JSON, `false` otherwise
- `error`: Error message if validation fails (e.g., "Invalid JSON: Unexpected token...")
**出力:**
- `passed`: 有効なJSONの場合は`true`、それ以外は`false`
- `error`: 検証が失敗した場合のエラーメッセージ「無効なJSON予期しないトークン...」)
### Regex Validation
### 正規表現検証
Checks if content matches a specified regular expression pattern.
コンテンツが指定された正規表現パターンに一致するかどうかをチェックします。
**Use Cases:**
- Validate email addresses
- Check phone number formats
- Verify URLs or custom identifiers
- Enforce specific text patterns
**ユースケース:**
- メールアドレスの検証
- 電話番号フォーマットのチェック
- URLやカスタム識別子の確認
- 特定のテキストパターンの強制
**Configuration:**
- **Regex Pattern**: The regular expression to match against (e.g., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` for emails)
**設定:**
- **正規表現パターン**:一致させる正規表現(例:メールの場合は`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
**Output:**
- `passed`: `true` if content matches pattern, `false` otherwise
- `error`: Error message if validation fails
**出力:**
- `passed`: コンテンツがパターンに一致する場合は `true`、それ以外の場合は `false`
- `error`: 検証が失敗した場合のエラーメッセージ
### Hallucination Detection
### 幻覚検出
Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-generated content contradicts or isn't grounded in your knowledge base.
検索拡張生成RAGとLLMスコアリングを使用して、AI生成コンテンツがナレッジベースと矛盾している場合や、ナレッジベースに根拠がない場合を検出します。
**How It Works:**
1. Queries your knowledge base for relevant context
2. Sends both the AI output and retrieved context to an LLM
3. LLM assigns a confidence score (0-10 scale)
- **0** = Full hallucination (completely ungrounded)
- **10** = Fully grounded (completely supported by knowledge base)
4. Validation passes if score ≥ threshold (default: 3)
**仕組み:**
1. 関連するコンテキストについてナレッジベースに問い合わせます
2. AI出力と取得したコンテキストの両方をLLMに送信します
3. LLMが信頼度スコア0〜10のスケールを割り当てます
- **0** = 完全な幻覚(まったく根拠なし)
- **10** = 完全に根拠あり(ナレッジベースで完全にサポートされている)
4. スコアが閾値以上デフォルト3であれば検証に合格します
**Configuration:**
- **Knowledge Base**: Select from your existing knowledge bases
- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended)
- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama models)
- **Confidence Threshold**: Minimum score to pass (0-10, default: 3)
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10)
**設定:**
- **ナレッジベース**: 既存のナレッジベースから選択
- **モデル**: スコアリング用のLLMを選択強力な推論能力が必要 - GPT-4oClaude 3.7 Sonnetを推奨)
- **APIキー**: 選択したLLMプロバイダーの認証ホスト型/Ollamaモデルでは自動的に非表示
- **信頼度閾値**: 合格するための最小スコア0〜10、デフォルト3
- **Top K**(詳細設定): 取得するナレッジベースのチャンク数(デフォルト:10
**Output:**
- `passed`: `true` if confidence score ≥ threshold
- `score`: Confidence score (0-10)
- `reasoning`: LLM's explanation for the score
- `error`: Error message if validation fails
**出力:**
- `passed`: 信頼度スコアが閾値以上の場合は `true`
- `score`: 信頼度スコア0〜10
- `reasoning`: スコアに対するLLMの説明
- `error`: 検証が失敗した場合のエラーメッセージ
**Use Cases:**
- Validate Agent responses against documentation
- Ensure customer support answers are factually accurate
- Verify generated content matches source material
- Quality control for RAG applications
**ユースケース:**
- エージェントの応答をドキュメントに対して検証
- カスタマーサポートの回答が事実に基づいていることを確認
- 生成されたコンテンツがソース資料と一致することを確認
- RAGアプリケーションの品質管理
### PII Detection
### PII検出
Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages.
Microsoft Presidioを使用して個人を特定できる情報を検出します。複数の国や言語にわたる40以上のエンティティタイプをサポートしています。
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**How It Works:**
1. Scans content for PII entities using pattern matching and NLP
2. Returns detected entities with locations and confidence scores
3. Optionally masks detected PII in the output
**仕組み:**
1. パターンマッチングとNLPを使用してコンテンツ内のPIIエンティティをスキャンします
2. 検出されたエンティティの位置と信頼度スコアを返します
3. オプションで出力内の検出されたPIIをマスクします
**Configuration:**
- **PII Types to Detect**: Select from grouped categories via modal selector
- **Common**: Person name, Email, Phone, Credit card, IP address, etc.
- **USA**: SSN, Driver's license, Passport, etc.
- **UK**: NHS number, National insurance number
- **Spain**: NIF, NIE, CIF
- **Italy**: Fiscal code, Driver's license, VAT code
- **Poland**: PESEL, NIP, REGON
- **Singapore**: NRIC/FIN, UEN
- **Australia**: ABN, ACN, TFN, Medicare
- **India**: Aadhaar, PAN, Passport, Voter number
- **Mode**:
- **Detect**: Only identify PII (default)
- **Mask**: Replace detected PII with masked values
- **Language**: Detection language (default: English)
**設定:**
- **検出するPIIタイプ**: モーダルセレクターからグループ化されたカテゴリーを選択
- **一般**: 個人名、メールアドレス、電話番号、クレジットカード、IPアドレスなど
- **アメリカ**: 社会保障番号、運転免許証、パスポートなど
- **イギリス**: NHS番号、国民保険番号
- **スペイン**: NIFNIECIF
- **イタリア**: 納税者番号、運転免許証、VAT番号
- **ポーランド**: PESELNIPREGON
- **シンガポール**: NRIC/FINUEN
- **オーストラリア**: ABNACNTFN、メディケア
- **インド**: AadhaarPAN、パスポート、有権者番号
- **モード**:
- **検出**: PIIの識別のみデフォルト
- **マスク**: 検出されたPIIをマスク値に置き換え
- **言語**: 検出言語(デフォルト:英語)
**Output:**
- `passed`: `false` if any selected PII types are detected
- `detectedEntities`: Array of detected PII with type, location, and confidence
- `maskedText`: Content with PII masked (only if mode = "Mask")
- `error`: Error message if validation fails
**出力:**
- `passed`: 選択したPIIタイプが検出された場合は `false`
- `detectedEntities`: タイプ、位置、信頼度を含む検出されたPIIの配列
- `maskedText`: PIIがマスクされたコンテンツモード = "Mask"の場合のみ)
- `error`: 検証が失敗した場合のエラーメッセージ
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Sanitize user inputs before processing
**ユースケース:**
- 機密性の高い個人情報を含むコンテンツのブロック
- データのログ記録や保存前のPIIマスキング
- GDPRHIPAAなどのプライバシー規制への準拠
- 処理前のユーザー入力のサニタイズ
## Configuration
## 設定
### Content to Validate
### 検証するコンテンツ
The input content to validate. This typically comes from:
- Agent block outputs: `<agent.content>`
- Function block results: `<function.output>`
- API responses: `<api.output>`
- Any other block output
検証する入力コンテンツ。通常、以下から取得されます:
- エージェントブロックの出力: `<agent.content>`
- ファンクションブロックの結果: `<function.output>`
- APIレスポンス: `<api.output>`
- その他のブロック出力
### Validation Type
### 検証タイプ
Choose from four validation types:
- **Valid JSON**: Check if content is properly formatted JSON
- **Regex Match**: Verify content matches a regex pattern
- **Hallucination Check**: Validate against knowledge base with LLM scoring
- **PII Detection**: Detect and optionally mask personally identifiable information
4つの検証タイプから選択
- **有効なJSON**: コンテンツが適切にフォーマットされたJSONかどうかを確認
- **正規表現マッチ**: コンテンツが正規表現パターンに一致するか検証
- **幻覚チェック**: LLMスコアリングによる知識ベースとの検証
- **PII検出**: 個人を特定できる情報の検出と任意のマスキング
## Outputs
## 出力
All validation types return:
すべての検証タイプは以下を返します:
- **`<guardrails.passed>`**: Boolean indicating if validation passed
- **`<guardrails.validationType>`**: The type of validation performed
- **`<guardrails.input>`**: The original input that was validated
- **`<guardrails.error>`**: Error message if validation failed (optional)
- **`<guardrails.passed>`**: 検証が成功したかどうかを示すブール値
- **`<guardrails.validationType>`**: 実行された検証のタイプ
- **`<guardrails.input>`**: 検証された元の入力
- **`<guardrails.error>`**: 検証が失敗した場合のエラーメッセージ(オプション)
Additional outputs by type:
タイプ別の追加出力:
**Hallucination Check:**
- **`<guardrails.score>`**: Confidence score (0-10)
- **`<guardrails.reasoning>`**: LLM's explanation
**幻覚チェック:**
- **`<guardrails.score>`**: 信頼度スコア0〜10
- **`<guardrails.reasoning>`**: LLMの説明
**PII Detection:**
- **`<guardrails.detectedEntities>`**: Array of detected PII entities
- **`<guardrails.maskedText>`**: Content with PII masked (if mode = "Mask")
**PII検出:**
- **`<guardrails.detectedEntities>`**: 検出されたPIIエンティティの配列
- **`<guardrails.maskedText>`**: PIIがマスクされたコンテンツモード = "Mask"の場合)
## Example Use Cases
## 使用例
### Validate JSON Before Parsing
### パース前にJSONを検証する
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Ensure Agent output is valid JSON</h4>
<h4 className="font-medium">シナリオエージェントの出力が有効なJSONであることを確認する</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates structured JSON response</li>
<li>Guardrails validates JSON format</li>
<li>Condition block checks `<guardrails.passed>`</li>
<li>If passed → Parse and use data, If failed → Retry or handle error</li>
<li>エージェントが構造化されたJSON応答を生成</li>
<li>ガードレールがJSON形式を検証</li>
<li>条件ブロックが`<guardrails.passed>`をチェック</li>
<li>成功した場合→データを解析して使用、失敗した場合→再試行またはエラー処理</li>
</ol>
</div>
### Prevent Hallucinations
### 幻覚を防止する
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Validate customer support responses</h4>
<h4 className="font-medium">シナリオ:カスタマーサポートの回答を検証する</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates response to customer question</li>
<li>Guardrails checks against support documentation knowledge base</li>
<li>If confidence score ≥ 3 → Send response</li>
<li>If confidence score \< 3 → Flag for human review</li>
<li>エージェントが顧客の質問に対する回答を生成</li>
<li>ガードレールがサポートドキュメントのナレッジベースと照合</li>
<li>信頼度スコアが3以上→回答を送信</li>
<li>信頼度スコアが3未満→人間によるレビューにフラグを立てる</li>
</ol>
</div>
### Block PII in User Inputs
### ユーザー入力のPIIをブロックする
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Sanitize user-submitted content</h4>
<h4 className="font-medium">シナリオ:ユーザー提出コンテンツを無害化する</h4>
<ol className="list-decimal pl-5 text-sm">
<li>User submits form with text content</li>
<li>Guardrails detects PII (emails, phone numbers, SSN, etc.)</li>
<li>If PII detected → Reject submission or mask sensitive data</li>
<li>If no PII → Process normally</li>
<li>ユーザーがテキストコンテンツを含むフォームを提出</li>
<li>ガードレールがPIIメール、電話番号、社会保障番号などを検出</li>
<li>PIIが検出された場合→提出を拒否または機密データをマスク</li>
<li>PIIがない場合→通常通り処理</li>
</ol>
</div>
@@ -222,30 +222,29 @@ Additional outputs by type:
<Video src="guardrails-example.mp4" width={500} height={350} />
</div>
### Validate Email Format
### メール形式を検証する
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Check email address format</h4>
<h4 className="font-medium">シナリオ:メールアドレス形式をチェックする</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent extracts email from text</li>
<li>Guardrails validates with regex pattern</li>
<li>If valid → Use email for notification</li>
<li>If invalid → Request correction</li>
<li>エージェントがテキストからメールを抽出</li>
<li>ガードレールが正規表現パターンで検証</li>
<li>有効な場合→メールを通知に使用</li>
<li>無効な場合→修正を要求</li>
</ol>
</div>
## Best Practices
## ベストプラクティス
- **Chain with Condition blocks**: Use `<guardrails.passed>` to branch workflow logic based on validation results
- **Use JSON validation before parsing**: Always validate JSON structure before attempting to parse LLM outputs
- **Choose appropriate PII types**: Only select the PII entity types relevant to your use case for better performance
- **Set reasonable confidence thresholds**: For hallucination detection, adjust threshold based on your accuracy requirements (higher = stricter)
- **Use strong models for hallucination detection**: GPT-4o or Claude 3.7 Sonnet provide more accurate confidence scoring
- **Mask PII for logging**: Use "Mask" mode when you need to log or store content that may contain PII
- **Test regex patterns**: Validate your regex patterns thoroughly before deploying to production
- **Monitor validation failures**: Track `<guardrails.error>` messages to identify common validation issues
- **条件ブロックと連携する**: `<guardrails.passed>` を使用して検証結果に基づいてワークフローのロジックを分岐させる
- **JSONの解析前に検証する**: LLM出力を解析する前に、必ずJSON構造を検証する
- **適切なPIIタイプを選択する**: パフォーマンス向上のため、ユースケースに関連するPIIエンティティタイプのみを選択する
- **適切な信頼度しきい値を設定する**: 幻覚検出では、精度要件に基づいてしきい値を調整する(高いほど厳格)
- **幻覚検出には高性能モデルを使用する**: GPT-4oまたはClaude 3.7 Sonnetは、より正確な信頼度スコアリングを提供する
- **ログ記録のためにPIIをマスクする**: PIIを含む可能性のあるコンテンツをログに記録または保存する必要がある場合は、「マスク」モードを使用する
- **正規表現パターンをテストする**: 本番環境にデプロイする前に、正規表現パターンを徹底的に検証する
- **検証失敗を監視する**: `<guardrails.error>` メッセージを追跡して、一般的な検証の問題を特定する
<Callout type="info">
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
ガードレールの検証はワークフロー内で同期的に行われます。レイテンシーが重要な場合は、幻覚検出にはより高速なモデルGPT-4o-miniなどを選択してください。
</Callout>

View File

@@ -535,7 +535,7 @@ app.post('/sim-webhook', (req, res) => {
// Handle error...
} else {
console.log(`Workflow ${workflowId} completed: ${executionId}`);
console.log(`Cost: ${cost.total}`);
console.log(`Cost: $${cost.total}`);
// Process successful execution...
}
break;

View File

@@ -7,10 +7,10 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Simの公式TypeScript/JavaScript SDKは完全な型安全性を提供し、Node.jsとブラウザ環境の両方をサポートしています。これにより、Node.jsアプリケーション、Webアプリケーション、その他のJavaScript環境からプログラムによってワークフローを実行することができます。
Simの公式TypeScript/JavaScript SDKは完全な型安全性を提供し、Node.jsとブラウザ環境の両方をサポートしています。これにより、Node.jsアプリケーション、Webアプリケーション、その他のJavaScript環境からプログラムワークフローを実行することができます。
<Callout type="info">
TypeScript SDKは完全な型安全性、非同期実行サポート、指数バックオフによる自動レート制限、使用状況追跡を提供します。
TypeScript SDKは完全な型安全性、非同期実行サポート、指数バックオフによる自動レート制限、使用状況追跡を提供します。
</Callout>
## インストール
@@ -43,7 +43,7 @@ Sim用の公式TypeScript/JavaScript SDKは、完全な型安全性を提供し
## クイックスタート
以下は、始めるための簡単な例です:
以下は簡単な使用例です:
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -96,12 +96,12 @@ const result = await client.executeWorkflow('workflow-id', {
- `input` (any): ワークフローに渡す入力データ
- `timeout` (number): タイムアウト(ミリ秒)(デフォルト: 30000
- `stream` (boolean): ストリーミングレスポンスを有効にする(デフォルト: false
- `selectedOutputs` (string[]): `blockName.attribute`形式でストリーミングするブロック出力(例: `["agent1.content"]`
- `selectedOutputs` (string[]): `blockName.attribute` 形式でストリーミングするブロック出力(例: `["agent1.content"]`
- `async` (boolean): 非同期実行(デフォルト: false
**戻り値:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
`async: true`の場合、ポーリング用のタスクIDをすぐに返します。それ以外の場合は、完了を待ちます。
`async: true` の場合、ポーリング用のタスクIDをすぐに返します。それ以外の場合は、完了を待ちます。
##### getWorkflowStatus()
@@ -161,7 +161,7 @@ if (status.status === 'completed') {
##### executeWithRetry()
レート制限エラー時に指数バックオフを使用して自動的に再試行するワークフロー実行。
指数バックオフを使用してレート制限エラー時に自動的に再試行するワークフロー実行。
```typescript
const result = await client.executeWithRetry('workflow-id', {
@@ -177,7 +177,7 @@ const result = await client.executeWithRetry('workflow-id', {
**パラメータ:**
- `workflowId` (string): 実行するワークフローのID
- `options` (ExecutionOptions, オプション): `executeWorkflow()`と同じ
- `options` (ExecutionOptions, オプション): `executeWorkflow()` と同じ
- `retryOptions` (RetryOptions, オプション):
- `maxRetries` (number): 最大再試行回数(デフォルト: 3
- `initialDelay` (number): 初期遅延(ミリ秒)(デフォルト: 1000
@@ -186,7 +186,7 @@ const result = await client.executeWithRetry('workflow-id', {
**戻り値:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
再試行ロジックは、サンダリングハード問題を防ぐために±25%のジッターを含む指数バックオフ1秒→2秒→4秒→8秒...)を使用します。APIが`retry-after`ヘッダーを提供する場合、代わりにそれが使用されます。
リトライロジックは指数バックオフ1秒 → 2秒 → 4秒 → 8秒...)を使用し、サンダリングハード問題を防ぐために±25%のジッターを用します。APIが `retry-after` ヘッダーを提供する場合、代わりにそれが使用されます。
##### getRateLimitInfo()
@@ -306,7 +306,7 @@ interface WorkflowStatus {
}
```
### RateLimitInfo
### レート制限情報
```typescript
interface RateLimitInfo {
@@ -317,7 +317,7 @@ interface RateLimitInfo {
}
```
### UsageLimits
### 使用制限
```typescript
interface UsageLimits {
@@ -345,7 +345,7 @@ interface UsageLimits {
}
```
### SimStudioError
### SimStudioエラー
```typescript
class SimStudioError extends Error {
@@ -356,10 +356,10 @@ class SimStudioError extends Error {
**一般的なエラーコード:**
- `UNAUTHORIZED`: 無効なAPIキー
- `TIMEOUT`: リクエストタイムアウトしました
- `RATE_LIMIT_EXCEEDED`: レート制限を超えました
- `USAGE_LIMIT_EXCEEDED`: 使用制限を超えました
- `EXECUTION_ERROR`: ワークフロー実行失敗しました
- `TIMEOUT`: リクエストタイムアウト
- `RATE_LIMIT_EXCEEDED`: レート制限超過
- `USAGE_LIMIT_EXCEEDED`: 使用制限超過
- `EXECUTION_ERROR`: ワークフロー実行失敗
## 例
@@ -376,7 +376,7 @@ class SimStudioError extends Error {
入力データでワークフローを実行します。
</Step>
<Step title="結果の処理">
実行結果を処理し、エラーがあれば対処します。
実行結果を処理し、エラーを適切に扱います。
</Step>
</Steps>
@@ -462,8 +462,8 @@ async function executeWithErrorHandling() {
環境変数を使用してクライアントを設定します:
<Tabs items={['Development', 'Production']}>
<Tab value="Development">
<Tabs items={['開発環境', '本番環境']}>
<Tab value="開発環境">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -481,7 +481,7 @@ async function executeWithErrorHandling() {
```
</Tab>
<Tab value="Production">
<Tab value="本番環境">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -501,7 +501,7 @@ async function executeWithErrorHandling() {
</Tab>
</Tabs>
### Node.js Express統合
### Node.js Expressとの統合
Express.jsサーバーとの統合
@@ -545,7 +545,7 @@ app.listen(3000, () => {
### Next.js APIルート
Next.js APIルートでの使用
Next.js APIルートでの使用方法
```typescript
// pages/api/workflow.ts or app/api/workflow/route.ts
@@ -584,7 +584,7 @@ export default async function handler(
### ブラウザでの使用
ブラウザでの使用適切なCORS設定が必要
ブラウザでの使用方法適切なCORS設定が必要
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -604,26 +604,105 @@ async function executeClientSideWorkflow() {
});
console.log('Workflow result:', result);
// Update UI with result
document.getElementById('result')!.textContent =
document.getElementById('result')!.textContent =
JSON.stringify(result.output, null, 2);
} catch (error) {
console.error('Error:', error);
}
}
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
```
### ファイルアップロード
Fileオブジェクトは自動的に検出され、base64形式に変換されます。ワークフローのAPIトリガー入力形式に一致するフィールド名で入力に含めてください。
SDKはFileオブジェクトを以下の形式に変換します
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
または、URL形式を使用して手動でファイルを提供することもできます
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
<Tabs items={['Browser', 'Node.js']}>
<Tab value="Browser">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.NEXT_PUBLIC_SIM_API_KEY!
});
// From file input
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files || []);
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: files, // Must match your workflow's "files" field name
instructions: 'Analyze these documents'
}
});
console.log('Result:', result);
}
```
</Tab>
<Tab value="Node.js">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
import fs from 'fs';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
// Read file and create File object
const fileBuffer = fs.readFileSync('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', {
type: 'application/pdf'
});
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: [file], // Must match your workflow's "files" field name
query: 'Summarize this document'
}
});
```
</Tab>
</Tabs>
<Callout type="warning">
ブラウザでSDKを使用する場合、機密性の高いAPIキーを公開しないよう注意してください。バックエンドプロキシや権限が制限された公開APIキーの使用を検討してください。
</Callout>
### Reactフックの例
ワークフロー実行用のカスタムReactフックを作成
ワークフロー実行用のカスタムReactフックを作成する
```typescript
import { useState, useCallback } from 'react';
@@ -701,7 +780,7 @@ function WorkflowComponent() {
### 非同期ワークフロー実行
長時間実行タスク向けに非同期でワークフローを実行
長時間実行されるタスクのためにワークフローを非同期で実行する
```typescript
import { SimStudioClient, AsyncExecutionResult } from 'simstudio-ts-sdk';
@@ -750,7 +829,7 @@ executeAsync();
### レート制限とリトライ
指数バックオフによるレート制限の自動処理
指数バックオフを使用して自動的にレート制限を処理する
```typescript
import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk';
@@ -786,9 +865,9 @@ async function executeWithRetryHandling() {
}
```
### 使用状況モニタリング
### 使用状況モニタリング
アカウントの使用状況と制限モニタリング:
アカウントの使用状況と制限モニタリングします
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -815,11 +894,27 @@ async function checkUsage() {
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Usage ===');
console.log('Current period cost:
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
### Streaming Workflow Execution
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
Execute workflows with real-time streaming responses:
if (percentUsed > 80) {
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Error checking usage:', error);
}
}
checkUsage();
```
### ワークフローのストリーミング実行
リアルタイムのストリーミングレスポンスでワークフローを実行します:
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -830,21 +925,21 @@ const client = new SimStudioClient({
async function executeWithStreaming() {
try {
// 特定のブロック出力のストリーミングを有効化
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', {
input: { message: 'Count to five' },
stream: true,
selectedOutputs: ['agent1.content'] // blockName.attribute形式を使用
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
});
console.log('ワークフロー結果:', result);
console.log('Workflow result:', result);
} catch (error) {
console.error('エラー:', error);
console.error('Error:', error);
}
}
```
The streaming response follows the Server-Sent Events (SSE) format:
ストリーミングレスポンスはServer-Sent EventsSSE)形式に従います:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
@@ -856,7 +951,7 @@ data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**React Streaming Example:**
**Reactストリーミングの例:**
```typescript
import { useState, useEffect } from 'react';
@@ -928,36 +1023,35 @@ function StreamingWorkflow() {
}
```
## Getting Your API Key
## APIキーの取得
<Steps>
<Step title="Log in to Sim">
Navigate to [Sim](https://sim.ai) and log in to your account.
<Step title="Simにログイン">
[Sim](https://sim.ai)に移動してアカウントにログインします。
</Step>
<Step title="Open your workflow">
Navigate to the workflow you want to execute programmatically.
<Step title="ワークフローを開く">
プログラムで実行したいワークフローに移動します。
</Step>
<Step title="Deploy your workflow">
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
<Step title="ワークフローをデプロイする">
まだデプロイされていない場合は、「デプロイ」をクリックしてワークフローをデプロイします。
</Step>
<Step title="Create or select an API key">
During the deployment process, select or create an API key.
<Step title="APIキーを作成または選択する">
デプロイプロセス中に、APIキーを選択または作成します。
</Step>
<Step title="Copy the API key">
Copy the API key to use in your TypeScript/JavaScript application.
<Step title="APIキーをコピーする">
TypeScript/JavaScriptアプリケーションで使用するAPIキーをコピーします。
</Step>
</Steps>
<Callout type="warning">
Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management.
APIキーは安全に保管し、バージョン管理システムにコミットしないでください。環境変数や安全な設定管理を使用してください。
</Callout>
## Requirements
## 要件
- Node.js 16+
- TypeScript 5.0+ (for TypeScript projects)
## License
- Node.js 16以上
- TypeScript 5.0以上(TypeScriptプロジェクトの場合)
## ライセンス
Apache-2.0

View File

@@ -22,7 +22,210 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
</svg>`}
/>
## 概要
Generic Webhookブロックは、外部サービスからのWebhookを受信することができます。これはあらゆるJSONペイロードを処理できる柔軟なトリガーであり、専用のSimブロックを持たないサービスとの統合に最適です。
## 基本的な使用方法
### シンプルなパススルーモード
入力フォーマットを定義しない場合、Webhookはリクエスト本文全体をそのまま渡します
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
下流のブロックでデータにアクセスする方法:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### 構造化入力フォーマット(オプション)
入力スキーマを定義して、型付きフィールドを取得し、ファイルアップロードなどの高度な機能を有効にします:
**入力フォーマット設定:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhookリクエスト**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## ファイルアップロード
### サポートされているファイル形式
Webhookは2つのファイル入力形式をサポートしています
#### 1. Base64エンコードファイル
ファイルコンテンツを直接アップロードする場合:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **最大サイズ**: ファイルあたり20MB
- **フォーマット**: Base64エンコーディングを使用した標準データURL
- **ストレージ**: ファイルは安全な実行ストレージにアップロードされます
#### 2. URL参照
既存のファイルURLを渡す場合
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### 下流ブロックでのファイルへのアクセス
ファイルは以下のプロパティを持つ`UserFile`オブジェクトに処理されます:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**ブロックでのアクセス:**
- `<webhook1.documents[0].url>` → ダウンロードURL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### 完全なファイルアップロード例
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## 認証
### 認証の設定(オプション)
Webhookの設定で
1. 「認証を要求する」を有効にする
2. シークレットトークンを設定する
3. ヘッダータイプを選択する:
- **カスタムヘッダー**`X-Sim-Secret: your-token`
- **認証ベアラー**`Authorization: Bearer your-token`
### 認証の使用
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## ベストプラクティス
1. **構造化のための入力フォーマットを使用する**:予想されるスキーマがわかっている場合は入力フォーマットを定義してください。これにより以下が提供されます:
- 型の検証
- エディタでのより良いオートコンプリート
- ファイルアップロード機能
2. **認証**不正アクセスを防ぐため、本番環境のWebhookには常に認証を有効にしてください。
3. **ファイルサイズの制限**ファイルは20MB未満に保つようにしてください。より大きなファイルの場合は、代わりにURL参照を使用してください。
4. **ファイルの有効期限**ダウンロードされたファイルのURLは5分間有効です。すぐに処理するか、長期間必要な場合は別の場所に保存してください。
5. **エラー処理**Webhook処理は非同期です。エラーについては実行ログを確認してください。
6. **テスト**デプロイ前に設定を検証するために、エディタの「Webhookをテスト」ボタンを使用してください。
## ユースケース
- **フォーム送信**:ファイルアップロード機能を持つカスタムフォームからデータを受け取る
- **サードパーティ連携**Webhookを送信するサービスStripe、GitHubなどと接続する
- **ドキュメント処理**:外部システムからドキュメントを受け取って処理する
- **イベント通知**:様々なソースからイベントデータを受け取る
- **カスタムAPI**アプリケーション用のカスタムAPIエンドポイントを構築する
## 注意事項
- カテゴリー: `triggers`
- タイプ: `generic_webhook`
- カテゴリ`triggers`
- タイプ`generic_webhook`
- **ファイルサポート**:入力フォーマット設定で利用可能
- **最大ファイルサイズ**ファイルあたり20MB

View File

@@ -110,6 +110,7 @@ Microsoft Teamsチャットからコンテンツを読み取る
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `chatId` | string | はい | 読み取り元のチャットID |
| `includeAttachments` | boolean | いいえ | メッセージの添付ファイル(ホストされたコンテンツ)をダウンロードしてストレージに含める |
#### 出力
@@ -122,6 +123,7 @@ Microsoft Teamsチャットからコンテンツを読み取る
| `attachmentCount` | number | 見つかった添付ファイルの総数 |
| `attachmentTypes` | array | 見つかった添付ファイルの種類 |
| `content` | string | チャットメッセージのフォーマット済みコンテンツ |
| `attachments` | file[] | 利便性のためにアップロードされた添付ファイル(フラット化済み) |
### `microsoft_teams_write_chat`
@@ -155,19 +157,21 @@ Microsoft Teams チャネルからコンテンツを読み取る
| --------- | ---- | -------- | ----------- |
| `teamId` | string | はい | 読み取り元のチームID |
| `channelId` | string | はい | 読み取り元のチャネルID |
| `includeAttachments` | boolean | いいえ | メッセージの添付ファイル(ホストされたコンテンツ)をダウンロードしてストレージに含める |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | Teams チャネル読み取り操作の成功ステータス |
| `messageCount` | number | チャネルから取得したメッセージ数 |
| `success` | boolean | Teamsチャネル読み取り操作の成功ステータス |
| `messageCount` | number | チャネルから取得したメッセージ数 |
| `teamId` | string | 読み取り元のチームID |
| `channelId` | string | 読み取り元のチャネルID |
| `messages` | array | チャネルメッセージオブジェクトの配列 |
| `attachmentCount` | number | 見つかった添付ファイルの総数 |
| `attachmentTypes` | array | 見つかった添付ファイルの種類 |
| `content` | string | チャネルメッセージのフォーマット済みコンテンツ |
| `attachments` | file[] | 利便性のためにアップロードされた添付ファイル(フラット化済み) |
### `microsoft_teams_write_channel`

View File

@@ -201,8 +201,9 @@ Outlookからメールを読み取る
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `folder` | string | いいえ | メールを読み取るフォルダIDデフォルト受信トレイ |
| `folder` | string | いいえ | メールを読み込むフォルダIDデフォルト受信トレイ |
| `maxResults` | number | いいえ | 取得するメールの最大数デフォルト1、最大10 |
| `includeAttachments` | boolean | いいえ | メールの添付ファイルをダウンロードして含める |
#### 出力
@@ -210,6 +211,7 @@ Outlookからメールを読み取る
| --------- | ---- | ----------- |
| `message` | string | 成功またはステータスメッセージ |
| `results` | array | メールメッセージオブジェクトの配列 |
| `attachments` | file[] | すべてのメールからの添付ファイルをまとめたもの |
### `outlook_forward`

View File

@@ -0,0 +1,242 @@
---
title: Zep
description: AIエージェント用の長期記憶
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="zep"
color="#E8E8E8"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 233 196'
>
<path
d='m231.34,108.7l-1.48-1.55h-10.26l3.59-75.86-14.8-.45-2.77,49.31c-59.6-3.24-119.33-3.24-178.92-.02l-1.73-64.96-14.8.45,2.5,91.53H2.16l-1.41,1.47c-1.55,16.23-.66,32.68,2.26,48.89h10.83l.18,1.27c.67,19.34,16.1,34.68,35.9,34.68s44.86-.92,66.12-.92,46.56.92,65.95.92,35.19-15.29,35.9-34.61l.16-1.34h11.02c2.91-16.19,3.81-32.61,2.26-48.81Zm-158.23,58.01c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Zm86.51,0c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Z'
fill='#FF1493'
/>
<polygon
points='111.77 22.4 93.39 49.97 93.52 50.48 185.88 38.51 190.95 27.68 114.32 36.55 117.7 31.48 117.7 31.47 138.38 .49 138.25 0 47.67 11.6 42.85 22.27 118.34 12.61 111.77 22.4'
fill='#FF1493'
/>
<path
d='m72.97,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
<path
d='m159.48,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
</svg>`}
/>
## 使用方法
Zepを統合して長期記憶管理を実現。スレッドの作成、メッセージの追加、AI駆動の要約や事実抽出によるコンテキストの取得が可能です。
## ツール
### `zep_create_thread`
Zepで新しい会話スレッドを開始する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | はい | スレッドの一意の識別子 |
| `userId` | string | はい | スレッドに関連付けられたユーザーID |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `threadId` | string | スレッドID |
| `userId` | string | ユーザーID |
| `uuid` | string | 内部UUID |
| `createdAt` | string | 作成タイムスタンプ |
| `projectUuid` | string | プロジェクトUUID |
### `zep_get_threads`
すべての会話スレッドを一覧表示する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `pageSize` | number | いいえ | 1ページあたりに取得するスレッド数 |
| `pageNumber` | number | いいえ | ページネーションのページ番号 |
| `orderBy` | string | いいえ | 結果を並べ替える項目created_at、updated_at、user_id、thread_id |
| `asc` | boolean | いいえ | 並べ替え方向昇順はtrue、降順はfalse |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `threads` | array | スレッドオブジェクトの配列 |
| `responseCount` | number | このレスポンスに含まれるスレッドの数 |
| `totalCount` | number | 利用可能なスレッドの総数 |
### `zep_delete_thread`
Zepから会話スレッドを削除する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | はい | 削除するスレッドID |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `deleted` | boolean | スレッドが削除されたかどうか |
### `zep_get_context`
サマリーモードまたは基本モードでスレッドからユーザーコンテキストを取得する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | はい | コンテキストを取得するスレッドID |
| `mode` | string | いいえ | コンテキストモード「summary」自然言語または「basic」生のファクト |
| `minRating` | number | いいえ | 関連するファクトをフィルタリングする最小評価 |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `context` | string | コンテキスト文字列(サマリーまたは基本) |
| `facts` | array | 抽出されたファクト |
| `entities` | array | 抽出されたエンティティ |
| `summary` | string | 会話のサマリー |
### `zep_get_messages`
スレッドからメッセージを取得する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | はい | メッセージを取得するスレッドID |
| `limit` | number | いいえ | 返すメッセージの最大数 |
| `cursor` | string | いいえ | ページネーション用のカーソル |
| `lastn` | number | いいえ | 返す最新メッセージの数limitとcursorより優先される |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `messages` | array | メッセージオブジェクトの配列 |
| `rowCount` | number | このレスポンスに含まれるメッセージの数 |
| `totalCount` | number | スレッド内のメッセージの総数 |
### `zep_add_messages`
既存のスレッドにメッセージを追加する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | はい | メッセージを追加するスレッドID |
| `messages` | json | はい | ロールとコンテンツを持つメッセージオブジェクトの配列 |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `context` | string | メッセージ追加後の更新されたコンテキスト |
| `messageIds` | array | 追加されたメッセージUUIDの配列 |
| `threadId` | string | スレッドID |
### `zep_add_user`
Zepで新しいユーザーを作成する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `userId` | string | はい | ユーザーの一意の識別子 |
| `email` | string | いいえ | ユーザーのメールアドレス |
| `firstName` | string | いいえ | ユーザーの名 |
| `lastName` | string | いいえ | ユーザーの姓 |
| `metadata` | json | いいえ | JSONオブジェクトとしての追加メタデータ |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `userId` | string | ユーザーID |
| `email` | string | ユーザーのメール |
| `firstName` | string | ユーザーの名 |
| `lastName` | string | ユーザーの姓 |
| `uuid` | string | 内部UUID |
| `createdAt` | string | 作成タイムスタンプ |
| `metadata` | object | ユーザーメタデータ |
### `zep_get_user`
Zepからユーザー情報を取得する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `userId` | string | はい | 取得するユーザーID |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `userId` | string | ユーザーID |
| `email` | string | ユーザーのメール |
| `firstName` | string | ユーザーの名 |
| `lastName` | string | ユーザーの姓 |
| `uuid` | string | 内部UUID |
| `createdAt` | string | 作成タイムスタンプ |
| `updatedAt` | string | 最終更新タイムスタンプ |
| `metadata` | object | ユーザーメタデータ |
### `zep_get_user_threads`
特定のユーザーのすべての会話スレッドを一覧表示する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `userId` | string | はい | スレッドを取得するユーザーID |
| `limit` | number | いいえ | 返すスレッドの最大数 |
| `apiKey` | string | はい | あなたのZep APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `threads` | array | このユーザーのスレッドオブジェクトの配列 |
| `userId` | string | ユーザーID |
## 注意事項
- カテゴリー: `tools`
- タイプ: `zep`

View File

@@ -8,213 +8,213 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow.
Guardrails 模块通过针对多种验证类型检查内容,验证并保护您的 AI 工作流。在内容进入工作流之前,确保数据质量、防止幻觉、检测 PII个人身份信息并强制执行格式要求。
<div className="flex justify-center">
<Image
src="/static/blocks/guardrails.png"
alt="Guardrails Block"
alt="Guardrails 模块"
width={500}
height={350}
className="my-6"
/>
</div>
## Overview
## 概述
The Guardrails block enables you to:
Guardrails 模块使您能够:
<Steps>
<Step>
<strong>Validate JSON Structure</strong>: Ensure LLM outputs are valid JSON before parsing
<strong>验证 JSON 结构</strong>:确保 LLM 输出在解析之前是有效的 JSON
</Step>
<Step>
<strong>Match Regex Patterns</strong>: Verify content matches specific formats (emails, phone numbers, URLs, etc.)
<strong>匹配正则表达式模式</strong>验证内容是否符合特定格式如电子邮件、电话号码、URL 等)
</Step>
<Step>
<strong>Detect Hallucinations</strong>: Use RAG + LLM scoring to validate AI outputs against knowledge base content
<strong>检测幻觉</strong>:使用 RAG + LLM 评分验证 AI 输出是否符合知识库内容
</Step>
<Step>
<strong>Detect PII</strong>: Identify and optionally mask personally identifiable information across 40+ entity types
<strong>检测 PII</strong>:识别并可选择性地屏蔽 40 多种实体类型的个人身份信息
</Step>
</Steps>
## Validation Types
## 验证类型
### JSON Validation
### JSON 验证
Validates that content is properly formatted JSON. Perfect for ensuring structured LLM outputs can be safely parsed.
验证内容是否为正确格式的 JSON。非常适合确保结构化的 LLM 输出可以安全解析。
**Use Cases:**
- Validate JSON responses from Agent blocks before parsing
- Ensure API payloads are properly formatted
- Check structured data integrity
**使用场景:**
- 在解析之前验证来自 Agent 模块的 JSON 响应
- 确保 API 负载格式正确
- 检查结构化数据的完整性
**Output:**
- `passed`: `true` if valid JSON, `false` otherwise
- `error`: Error message if validation fails (e.g., "Invalid JSON: Unexpected token...")
**输出:**
- `passed`:如果是有效的 JSON则为 `true`,否则为 `false`
- `error`:如果验证失败,则为错误消息(例如,“无效的 JSON意外的标记...”)
### Regex Validation
### 正则表达式验证
Checks if content matches a specified regular expression pattern.
检查内容是否符合指定的正则表达式模式。
**Use Cases:**
- Validate email addresses
- Check phone number formats
- Verify URLs or custom identifiers
- Enforce specific text patterns
**使用场景:**
- 验证电子邮件地址
- 检查电话号码格式
- 验证 URL 或自定义标识符
- 强制执行特定文本模式
**Configuration:**
- **Regex Pattern**: The regular expression to match against (e.g., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` for emails)
**配置:**
- **正则表达式模式**:要匹配的正则表达式(例如,电子邮件的 `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
**Output:**
- `passed`: `true` if content matches pattern, `false` otherwise
- `error`: Error message if validation fails
**输出:**
- `passed`:如果内容符合模式,则为 `true`,否则为 `false`
- `error`:如果验证失败,则显示错误信息
### Hallucination Detection
### 幻觉检测
Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-generated content contradicts or isn't grounded in your knowledge base.
使用基于检索增强生成 (RAG) 的方法,并结合 LLM 评分,检测 AI 生成的内容是否与您的知识库相矛盾或不相关。
**How It Works:**
1. Queries your knowledge base for relevant context
2. Sends both the AI output and retrieved context to an LLM
3. LLM assigns a confidence score (0-10 scale)
- **0** = Full hallucination (completely ungrounded)
- **10** = Fully grounded (completely supported by knowledge base)
4. Validation passes if score ≥ threshold (default: 3)
**工作原理:**
1. 查询您的知识库以获取相关上下文
2. 将 AI 输出和检索到的上下文发送到 LLM
3. LLM 分配一个置信评分0-10 分制)
- **0** = 完全幻觉(完全无依据)
- **10** = 完全有依据(完全由知识库支持)
4. 如果评分 ≥ 阈值默认值3验证通过
**Configuration:**
- **Knowledge Base**: Select from your existing knowledge bases
- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended)
- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama models)
- **Confidence Threshold**: Minimum score to pass (0-10, default: 3)
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10)
**配置:**
- **知识库**:从现有知识库中选择
- **模型**:选择用于评分的 LLM需要强大的推理能力推荐 GPT-4oClaude 3.7 Sonnet
- **API 密钥**:所选 LLM 提供商的身份验证(托管/Ollama 模型自动隐藏)
- **置信阈值**通过验证的最低评分0-10默认值3
- **Top K**(高级):检索的知识库块数量(默认值:10
**Output:**
- `passed`: `true` if confidence score ≥ threshold
- `score`: Confidence score (0-10)
- `reasoning`: LLM's explanation for the score
- `error`: Error message if validation fails
**输出:**
- `passed`:如果置信评分 ≥ 阈值,则为 `true`
- `score`:置信评分(0-10
- `reasoning`LLM 对评分的解释
- `error`:如果验证失败,则显示错误信息
**Use Cases:**
- Validate Agent responses against documentation
- Ensure customer support answers are factually accurate
- Verify generated content matches source material
- Quality control for RAG applications
**使用场景:**
- 验证代理响应是否符合文档
- 确保客户支持回答的事实准确性
- 验证生成的内容是否与源材料匹配
- RAG 应用的质量控制
### PII Detection
### PII 检测
Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages.
使用 Microsoft Presidio 检测个人身份信息 (PII)。支持 40 多种实体类型,覆盖多个国家和语言。
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
<Video src="guardrails.mp4" width={500} height={350} />
</div>
**How It Works:**
1. Scans content for PII entities using pattern matching and NLP
2. Returns detected entities with locations and confidence scores
3. Optionally masks detected PII in the output
**工作原理:**
1. 使用模式匹配和 NLP 扫描内容中的 PII 实体
2. 返回检测到的实体及其位置和置信评分
3. 可选择在输出中屏蔽检测到的 PII
**Configuration:**
- **PII Types to Detect**: Select from grouped categories via modal selector
- **Common**: Person name, Email, Phone, Credit card, IP address, etc.
- **USA**: SSN, Driver's license, Passport, etc.
- **UK**: NHS number, National insurance number
- **Spain**: NIF, NIE, CIF
- **Italy**: Fiscal code, Driver's license, VAT code
- **Poland**: PESEL, NIP, REGON
- **Singapore**: NRIC/FIN, UEN
- **Australia**: ABN, ACN, TFN, Medicare
- **India**: Aadhaar, PAN, Passport, Voter number
- **Mode**:
- **Detect**: Only identify PII (default)
- **Mask**: Replace detected PII with masked values
- **Language**: Detection language (default: English)
**配置:**
- **要检测的 PII 类型**:通过模态选择器从分组类别中选择
- **常见**姓名、电子邮件、电话、信用卡、IP 地址等
- **美国**:社会安全号码 (SSN)、驾驶执照、护照等
- **英国**NHS 编号、国家保险号码
- **西班牙**NIFNIECIF
- **意大利**:税号、驾驶执照、增值税号
- **波兰**PESELNIPREGON
- **新加坡**NRIC/FINUEN
- **澳大利亚**ABNACNTFNMedicare
- **印度**AadhaarPAN、护照、选民编号
- **模式:**
- **检测**:仅识别 PII默认
- **掩码**:将检测到的 PII 替换为掩码值
- **语言:**检测语言(默认:英语)
**Output:**
- `passed`: `false` if any selected PII types are detected
- `detectedEntities`: Array of detected PII with type, location, and confidence
- `maskedText`: Content with PII masked (only if mode = "Mask")
- `error`: Error message if validation fails
**输出:**
- `passed`:如果检测到任何选定的 PII 类型
- `false`:检测到的 PII 数组,包括类型、位置和置信度
- `detectedEntities`:带有 PII 掩码的内容(仅当模式为 "掩码" 时)
- `maskedText`:如果验证失败的错误消息
**Use Cases:**
- Block content containing sensitive personal information
- Mask PII before logging or storing data
- Compliance with GDPR, HIPAA, and other privacy regulations
- Sanitize user inputs before processing
**使用场景:**
- 阻止包含敏感个人信息的内容
- 在记录或存储数据之前对 PII 进行掩码
- 符合 GDPRHIPAA 和其他隐私法规
- 在处理之前清理用户输入
## Configuration
## 配置
### Content to Validate
### 要验证的内容
The input content to validate. This typically comes from:
- Agent block outputs: `<agent.content>`
- Function block results: `<function.output>`
- API responses: `<api.output>`
- Any other block output
要验证的输入内容。通常来自:
- 代理块输出:`<agent.content>`
- 功能块结果:`<function.output>`
- API 响应:`<api.output>`
- 任何其他块输出
### Validation Type
### 验证类型
Choose from four validation types:
- **Valid JSON**: Check if content is properly formatted JSON
- **Regex Match**: Verify content matches a regex pattern
- **Hallucination Check**: Validate against knowledge base with LLM scoring
- **PII Detection**: Detect and optionally mask personally identifiable information
从四种验证类型中选择:
- **有效 JSON**:检查内容是否为正确格式的 JSON
- **正则表达式匹配**:验证内容是否匹配正则表达式模式
- **幻觉检查**:通过 LLM 评分与知识库验证
- **PII 检测**:检测并可选地掩码个人身份信息
## Outputs
## 输出
All validation types return:
所有验证类型返回:
- **`<guardrails.passed>`**: Boolean indicating if validation passed
- **`<guardrails.validationType>`**: The type of validation performed
- **`<guardrails.input>`**: The original input that was validated
- **`<guardrails.error>`**: Error message if validation failed (optional)
- **`<guardrails.passed>`**:布尔值,指示验证是否通过
- **`<guardrails.validationType>`**:执行的验证类型
- **`<guardrails.input>`**:被验证的原始输入
- **`<guardrails.error>`**:如果验证失败的错误信息(可选)
Additional outputs by type:
按类型的附加输出:
**Hallucination Check:**
- **`<guardrails.score>`**: Confidence score (0-10)
- **`<guardrails.reasoning>`**: LLM's explanation
**幻觉检查:**
- **`<guardrails.score>`**:置信分数(0-10
- **`<guardrails.reasoning>`**LLM 的解释
**PII Detection:**
- **`<guardrails.detectedEntities>`**: Array of detected PII entities
- **`<guardrails.maskedText>`**: Content with PII masked (if mode = "Mask")
**PII 检测:**
- **`<guardrails.detectedEntities>`**:检测到的 PII 实体数组
- **`<guardrails.maskedText>`**:已屏蔽 PII 的内容(如果模式为 "Mask"
## Example Use Cases
## 示例用例
### Validate JSON Before Parsing
### 在解析前验证 JSON
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Ensure Agent output is valid JSON</h4>
<h4 className="font-medium">场景:确保代理输出为有效的 JSON</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates structured JSON response</li>
<li>Guardrails validates JSON format</li>
<li>Condition block checks `<guardrails.passed>`</li>
<li>If passed → Parse and use data, If failed → Retry or handle error</li>
<li>代理生成结构化的 JSON 响应</li>
<li>Guardrails 验证 JSON 格式</li>
<li>条件块检查 `<guardrails.passed>`</li>
<li>如果通过 → 解析并使用数据,如果失败 → 重试或处理错误</li>
</ol>
</div>
### Prevent Hallucinations
### 防止幻觉
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Validate customer support responses</h4>
<h4 className="font-medium">场景:验证客户支持响应</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent generates response to customer question</li>
<li>Guardrails checks against support documentation knowledge base</li>
<li>If confidence score ≥ 3 → Send response</li>
<li>If confidence score \< 3 → Flag for human review</li>
<li>代理生成对客户问题的响应</li>
<li>Guardrails 根据支持文档知识库进行检查</li>
<li>如果置信分数 ≥ 3 → 发送响应</li>
<li>如果置信分数 \< 3 → 标记为人工审核</li>
</ol>
</div>
### Block PII in User Inputs
### 阻止用户输入中的 PII
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Sanitize user-submitted content</h4>
<h4 className="font-medium">场景:清理用户提交的内容</h4>
<ol className="list-decimal pl-5 text-sm">
<li>User submits form with text content</li>
<li>Guardrails detects PII (emails, phone numbers, SSN, etc.)</li>
<li>If PII detected → Reject submission or mask sensitive data</li>
<li>If no PII → Process normally</li>
<li>用户提交带有文本内容的表单</li>
<li>Guardrails 检测 PII电子邮件、电话号码、社会安全号码等</li>
<li>如果检测到 PII → 拒绝提交或屏蔽敏感数据</li>
<li>如果没有 PII → 正常处理</li>
</ol>
</div>
@@ -222,30 +222,29 @@ Additional outputs by type:
<Video src="guardrails-example.mp4" width={500} height={350} />
</div>
### Validate Email Format
### 验证电子邮件格式
<div className="mb-4 rounded-md border p-4">
<h4 className="font-medium">Scenario: Check email address format</h4>
<h4 className="font-medium">场景:检查电子邮件地址格式</h4>
<ol className="list-decimal pl-5 text-sm">
<li>Agent extracts email from text</li>
<li>Guardrails validates with regex pattern</li>
<li>If valid → Use email for notification</li>
<li>If invalid → Request correction</li>
<li>代理从文本中提取电子邮件</li>
<li>Guardrails 使用正则表达式模式进行验证</li>
<li>如果有效 → 使用电子邮件进行通知</li>
<li>如果无效 → 请求更正</li>
</ol>
</div>
## Best Practices
## 最佳实践
- **Chain with Condition blocks**: Use `<guardrails.passed>` to branch workflow logic based on validation results
- **Use JSON validation before parsing**: Always validate JSON structure before attempting to parse LLM outputs
- **Choose appropriate PII types**: Only select the PII entity types relevant to your use case for better performance
- **Set reasonable confidence thresholds**: For hallucination detection, adjust threshold based on your accuracy requirements (higher = stricter)
- **Use strong models for hallucination detection**: GPT-4o or Claude 3.7 Sonnet provide more accurate confidence scoring
- **Mask PII for logging**: Use "Mask" mode when you need to log or store content that may contain PII
- **Test regex patterns**: Validate your regex patterns thoroughly before deploying to production
- **Monitor validation failures**: Track `<guardrails.error>` messages to identify common validation issues
- **与条件块链式使用**:使用 `<guardrails.passed>` 根据验证结果分支工作流逻辑
- **在解析前使用 JSON 验证**:在尝试解析 LLM 输出之前,始终验证 JSON 结构
- **选择合适的 PII 类型**:仅选择与您的用例相关的 PII 实体类型以获得更好的性能
- **设置合理的置信度阈值**:对于幻觉检测,根据您的准确性要求调整阈值(越高 = 越严格)
- **使用强大的模型进行幻觉检测**GPT-4o Claude 3.7 Sonnet 提供更准确的置信度评分
- **对日志中的 PII 进行掩码**:当需要记录或存储可能包含 PII 的内容时,使用“掩码”模式
- **测试正则表达式模式**:在部署到生产环境之前,彻底验证您的正则表达式模式
- **监控验证失败**:跟踪 `<guardrails.error>` 消息以识别常见的验证问题
<Callout type="info">
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
Guardrails 验证在您的工作流中同步进行。对于幻觉检测,如果延迟至关重要,请选择更快的模型(如 GPT-4o-mini
</Callout>

View File

@@ -535,7 +535,7 @@ app.post('/sim-webhook', (req, res) => {
// Handle error...
} else {
console.log(`Workflow ${workflowId} completed: ${executionId}`);
console.log(`Cost: ${cost.total}`);
console.log(`Cost: $${cost.total}`);
// Process successful execution...
}
break;

View File

@@ -7,10 +7,10 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Sim 的官方 TypeScript/JavaScript SDK 提供完整的类型安全支持 Node.js 和浏览器环境,允许您从 Node.js 应用程序、Web 应用程序和其他 JavaScript 环境中以编程方式执行工作流。
Sim 的官方 TypeScript/JavaScript SDK 提供完整的类型安全性,并支持 Node.js 和浏览器环境,允许您从 Node.js 应用程序、Web 应用程序和其他 JavaScript 环境中以编程方式执行工作流。
<Callout type="info">
TypeScript SDK 提供完整的类型安全、异步执行支持、带有指数回退的自动速率限制以及使用跟踪。
TypeScript SDK 提供完整的类型安全、异步执行支持、带有指数回退的自动速率限制以及使用情况跟踪。
</Callout>
## 安装
@@ -74,14 +74,14 @@ new SimStudioClient(config: SimStudioConfig)
```
**配置:**
- `config.apiKey` (字符串): 您的 Sim API 密钥
- `config.baseUrl` (字符串,可选): Sim API 的基础 URL默认为 `https://sim.ai`
- `config.apiKey` (string)您的 Sim API 密钥
- `config.baseUrl` (string,可选)Sim API 的基础 URL默认为 `https://sim.ai`
#### 方法
##### executeWorkflow()
执行带有可选输入数据工作流。
使用可选输入数据执行工作流。
```typescript
const result = await client.executeWorkflow('workflow-id', {
@@ -91,13 +91,13 @@ const result = await client.executeWorkflow('workflow-id', {
```
**参数:**
- `workflowId` (字符串): 要执行的工作流的 ID
- `options` (ExecutionOptions可选):
- `input` (任意类型): 传递给工作流的输入数据
- `timeout` (数字): 超时时间以毫秒为单位默认值30000
- `stream` (布尔值): 启用流式响应默认值false
- `selectedOutputs` (字符串数组): 以 `blockName.attribute` 格式阻止流中的输出(例如,`["agent1.content"]`
- `async` (布尔值): 异步执行默认值false
- `workflowId`字符串要执行的工作流的 ID
- `options`ExecutionOptions可选
- `input`任意类型传递给工作流的输入数据
- `timeout`(数字):超时时间以毫秒为单位默认值30000
- `stream`布尔值启用流式响应默认值false
- `selectedOutputs`字符串数组以 `blockName.attribute` 格式流式传输的块输出(例如,`["agent1.content"]`
- `async`布尔值异步执行默认值false
**返回值:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
@@ -113,13 +113,13 @@ console.log('Is deployed:', status.isDeployed);
```
**参数:**
- `workflowId` (字符串): 工作流的 ID
- `workflowId`字符串工作流的 ID
**返回值:** `Promise<WorkflowStatus>`
##### validateWorkflow()
验证工作流是否准备好执行。
验证工作流是否准备好执行。
```typescript
const isReady = await client.validateWorkflow('workflow-id');
@@ -129,7 +129,7 @@ if (isReady) {
```
**参数:**
- `workflowId` (字符串): 工作流的 ID
- `workflowId`字符串工作流的 ID
**返回值:** `Promise<boolean>`
@@ -146,22 +146,22 @@ if (status.status === 'completed') {
```
**参数:**
- `taskId` (字符串): 异步执行返回的任务 ID
- `taskId`字符串异步执行返回的任务 ID
**返回值:** `Promise<JobStatus>`
**响应字段:**
- `success` (布尔值): 请求是否成功
- `taskId` (字符串): 任务 ID
- `status` (字符串): 可能的值包括 `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `metadata` (对象): 包含 `startedAt`, `completedAt` 和 `duration`
- `output` (任意类型,可选): 工作流输出(完成时)
- `error` (任意类型,可选): 错误详情(失败时)
- `estimatedDuration` (数字,可选): 估计持续时间(以毫秒为单位,处理中/排队时)
- `success`布尔值请求是否成功
- `taskId`字符串任务 ID
- `status`字符串):以下之一 `'queued'``'processing'``'completed'``'failed'``'cancelled'`
- `metadata`(对象):包含 `startedAt``completedAt` 和 `duration`
- `output`任意类型,可选工作流输出(完成时)
- `error`任意类型,可选错误详情(失败时)
- `estimatedDuration`数字,可选估计持续时间(以毫秒为单位,处理中/排队时)
##### executeWithRetry()
使用指数退避机制,在遇到速率限制错误时自动重试执行工作流。
使用指数退避自动重试速率限制错误的工作流执行
```typescript
const result = await client.executeWithRetry('workflow-id', {
@@ -186,11 +186,11 @@ const result = await client.executeWithRetry('workflow-id', {
**返回值:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
重试逻辑使用指数退避1秒 → 2秒 → 4秒 → 8秒...并带有 ±25% 的抖动以防止蜂拥效应。如果 API 提供了 `retry-after` 头,则会使用该头。
重试逻辑使用指数退避1 秒 → 2 秒 → 4 秒 → 8 秒...)并带有 ±25% 的抖动以防止惊群效应。如果 API 提供了 `retry-after` 头,则会使用该头。
##### getRateLimitInfo()
从上一次 API 响应中获取当前速率限制信息。
从上一次 API 响应中获取当前速率限制信息。
```typescript
const rateLimitInfo = client.getRateLimitInfo();
@@ -604,26 +604,105 @@ async function executeClientSideWorkflow() {
});
console.log('Workflow result:', result);
// Update UI with result
document.getElementById('result')!.textContent =
document.getElementById('result')!.textContent =
JSON.stringify(result.output, null, 2);
} catch (error) {
console.error('Error:', error);
}
}
// Attach to button click
document.getElementById('executeBtn')?.addEventListener('click', executeClientSideWorkflow);
```
### 文件上传
文件对象会被自动检测并转换为 base64 格式。将它们包含在输入中,字段名称需与工作流的 API 触发输入格式匹配。
SDK 将文件对象转换为以下格式:
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
或者,您可以使用 URL 格式手动提供文件:
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
<Tabs items={['浏览器', 'Node.js']}>
<Tab value="浏览器">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.NEXT_PUBLIC_SIM_API_KEY!
});
// From file input
async function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const files = Array.from(input.files || []);
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: files, // Must match your workflow's "files" field name
instructions: 'Analyze these documents'
}
});
console.log('Result:', result);
}
```
</Tab>
<Tab value="Node.js">
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
import fs from 'fs';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
// Read file and create File object
const fileBuffer = fs.readFileSync('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', {
type: 'application/pdf'
});
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
input: {
documents: [file], // Must match your workflow's "files" field name
query: 'Summarize this document'
}
});
```
</Tab>
</Tabs>
<Callout type="warning">
在浏览器中使用 SDK 时,请注意不要暴露敏感的 API 密钥。建议使用后端代理或具有有限权限的公共 API 密钥。
</Callout>
### React Hook 示例
为工作流执行创建自定义 React hook
为工作流执行创建一个自定义 React hook
```typescript
import { useState, useCallback } from 'react';
@@ -815,11 +894,27 @@ async function checkUsage() {
console.log(' Is limited:', limits.rateLimit.async.isLimited);
console.log('\n=== Usage ===');
console.log('Current period cost:
console.log('Current period cost: $' + limits.usage.currentPeriodCost.toFixed(2));
console.log('Limit: $' + limits.usage.limit.toFixed(2));
console.log('Plan:', limits.usage.plan);
### Streaming Workflow Execution
const percentUsed = (limits.usage.currentPeriodCost / limits.usage.limit) * 100;
console.log('Usage: ' + percentUsed.toFixed(1) + '%');
Execute workflows with real-time streaming responses:
if (percentUsed > 80) {
console.warn('⚠️ Warning: You are approaching your usage limit!');
}
} catch (error) {
console.error('Error checking usage:', error);
}
}
checkUsage();
```
### 流式工作流执行
通过实时流式响应执行工作流:
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
@@ -830,21 +925,21 @@ const client = new SimStudioClient({
async function executeWithStreaming() {
try {
// 为特定的块输出启用流式传输
// Enable streaming for specific block outputs
const result = await client.executeWorkflow('workflow-id', {
input: { message: 'Count to five' },
stream: true,
selectedOutputs: ['agent1.content'] // 使用 blockName.attribute 格式
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
});
console.log('工作流结果:', result);
console.log('Workflow result:', result);
} catch (error) {
console.error('错误:', error);
console.error('Error:', error);
}
}
```
The streaming response follows the Server-Sent Events (SSE) format:
流式响应遵循服务器发送事件 (SSE) 格式:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
@@ -856,7 +951,7 @@ data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**React Streaming Example:**
**React 流式示例:**
```typescript
import { useState, useEffect } from 'react';
@@ -869,16 +964,16 @@ function StreamingWorkflow() {
setLoading(true);
setOutput('');
// 重要提示:请从您的后端服务器发起此 API 调用,而不是从浏览器发起
// 切勿在客户端代码中暴露您的 API 密钥
// IMPORTANT: Make this API call from your backend server, not the browser
// Never expose your API key in client-side code
const response = await fetch('https://sim.ai/api/workflows/WORKFLOW_ID/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SIM_API_KEY! // 仅限服务器端环境变量
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
},
body: JSON.stringify({
message: '生成一个故事',
message: 'Generate a story',
stream: true,
selectedOutputs: ['agent1.content']
})
@@ -907,10 +1002,10 @@ function StreamingWorkflow() {
if (parsed.chunk) {
setOutput(prev => prev + parsed.chunk);
} else if (parsed.event === 'done') {
console.log('执行完成:', parsed.metadata);
console.log('Execution complete:', parsed.metadata);
}
} catch (e) {
// 跳过无效的 JSON
// Skip invalid JSON
}
}
}
@@ -920,7 +1015,7 @@ function StreamingWorkflow() {
return (
<div>
<button onClick={executeStreaming} disabled={loading}>
{loading ? '生成中...' : '开始流式处理'}
{loading ? 'Generating...' : 'Start Streaming'}
</button>
<div style={{ whiteSpace: 'pre-wrap' }}>{output}</div>
</div>
@@ -928,34 +1023,34 @@ function StreamingWorkflow() {
}
```
## Getting Your API Key
## 获取您的 API 密钥
<Steps>
<Step title="Log in to Sim">
Navigate to [Sim](https://sim.ai) and log in to your account.
<Step title="登录到 Sim">
访问 [Sim](https://sim.ai) 并登录到您的账户。
</Step>
<Step title="Open your workflow">
Navigate to the workflow you want to execute programmatically.
<Step title="打开您的工作流">
导航到您想要以编程方式执行的工作流。
</Step>
<Step title="Deploy your workflow">
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
<Step title="部署您的工作流">
如果尚未部署,请点击“部署”以部署您的工作流。
</Step>
<Step title="Create or select an API key">
During the deployment process, select or create an API key.
<Step title="创建或选择一个 API 密钥">
在部署过程中,选择或创建一个 API 密钥。
</Step>
<Step title="Copy the API key">
Copy the API key to use in your TypeScript/JavaScript application.
<Step title="复制 API 密钥">
复制 API 密钥以在您的 TypeScript/JavaScript 应用程序中使用。
</Step>
</Steps>
<Callout type="warning">
Keep your API key secure and never commit it to version control. Use environment variables or secure configuration management.
请确保您的 API 密钥安全,切勿将其提交到版本控制中。使用环境变量或安全配置管理。
</Callout>
## Requirements
## 要求
- Node.js 16+
- TypeScript 5.0+ (for TypeScript projects)
- TypeScript 5.0+(适用于 TypeScript 项目)
## 许可证

View File

@@ -22,7 +22,210 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
</svg>`}
/>
## 概述
通用 Webhook 模块允许您接收来自任何外部服务的 webhook。这是一个灵活的触发器可以处理任何 JSON 负载,非常适合与没有专用 Sim 模块的服务集成。
## 基本用法
### 简单直通模式
在未定义输入格式的情况下webhook 会按原样传递整个请求正文:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
在下游模块中访问数据:
- `<webhook1.message>` → "测试 webhook 触发器"
- `<webhook1.data.key>` → "值"
### 结构化输入格式(可选)
定义输入模式以获取类型化字段,并启用高级功能,例如文件上传:
**输入格式配置:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhook 请求:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## 文件上传
### 支持的文件格式
webhook 支持两种文件输入格式:
#### 1. Base64 编码文件
用于直接上传文件内容:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **最大大小**:每个文件 20MB
- **格式**:带有 base64 编码的标准数据 URL
- **存储**:文件上传到安全的执行存储
#### 2. URL 引用
用于传递现有文件的 URL
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### 在下游模块中访问文件
文件被处理为 `UserFile` 对象,具有以下属性:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**在模块中访问:**
- `<webhook1.documents[0].url>` → 下载 URL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### 完整的文件上传示例
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## 身份验证
### 配置身份验证(可选)
在 webhook 配置中:
1. 启用“需要身份验证”
2. 设置一个密钥令牌
3. 选择头类型:
- **自定义头**`X-Sim-Secret: your-token`
- **授权 Bearer**`Authorization: Bearer your-token`
### 使用身份验证
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## 最佳实践
1. **使用输入格式定义结构**:当您知道预期的模式时,定义一个输入格式。这可以提供:
- 类型验证
- 编辑器中的更好自动完成
- 文件上传功能
2. **身份验证**:在生产环境的 webhook 中始终启用身份验证,以防止未经授权的访问。
3. **文件大小限制**:将文件保持在 20MB 以下。对于更大的文件,请使用 URL 引用。
4. **文件过期**:下载的文件具有 5 分钟的过期 URL。请及时处理或者如果需要更长时间请将其存储在其他地方。
5. **错误处理**Webhook 处理是异步的。请检查执行日志以获取错误信息。
6. **测试**:在部署之前,使用编辑器中的“测试 Webhook”按钮验证您的配置。
## 使用场景
- **表单提交**:接收带有文件上传的自定义表单数据
- **第三方集成**:与发送 webhook 的服务(如 Stripe、GitHub 等)连接
- **文档处理**:接受来自外部系统的文档进行处理
- **事件通知**:接收来自各种来源的事件数据
- **自定义 API**:为您的应用程序构建自定义 API 端点
## 注意事项
- 类别:`triggers`
- 类型:`generic_webhook`
- **文件支持**:通过输入格式配置可用
- **最大文件大小**:每个文件 20MB

View File

@@ -110,6 +110,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `chatId` | string | 是 | 要读取的聊天 ID |
| `includeAttachments` | boolean | 否 | 下载并将消息附件(托管内容)包含到存储中 |
#### 输出
@@ -117,11 +118,12 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| --------- | ---- | ----------- |
| `success` | boolean | Teams 聊天读取操作成功状态 |
| `messageCount` | number | 从聊天中检索到的消息数量 |
| `chatId` | string | 读取的聊天 ID |
| `chatId` | string | 读取的聊天 ID |
| `messages` | array | 聊天消息对象的数组 |
| `attachmentCount` | number | 找到的附件总数 |
| `attachmentTypes` | array | 找到的附件类型 |
| `content` | string | 聊天消息的格式化内容 |
| `attachments` | file[] | 为方便起见上传的附件(扁平化) |
### `microsoft_teams_write_chat`
@@ -155,19 +157,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| --------- | ---- | -------- | ----------- |
| `teamId` | string | 是 | 要读取的团队 ID |
| `channelId` | string | 是 | 要读取的频道 ID |
| `includeAttachments` | boolean | 否 | 下载并将消息附件(托管内容)包含到存储中 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | Teams 频道读取操作成功状态 |
| `messageCount` | number | 从频道检索到的消息数量 |
| `messageCount` | number | 从频道检索到的消息数量 |
| `teamId` | string | 读取的团队 ID |
| `channelId` | string | 读取的频道 ID |
| `messages` | array | 频道消息对象的数组 |
| `attachmentCount` | number | 找到的附件总数 |
| `attachmentTypes` | array | 找到的附件类型 |
| `content` | string | 频道消息的格式化内容 |
| `attachments` | file[] | 为方便起见上传的附件(扁平化) |
### `microsoft_teams_write_channel`

View File

@@ -202,7 +202,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `folder` | string | 否 | 要读取邮件的文件夹 ID默认收件箱 |
| `maxResults` | number | 否 | 要检索的最大邮件数量默认1最大10 |
| `maxResults` | number | 否 | 要检索的最大邮件数量默认1最大10 |
| `includeAttachments` | boolean | 否 | 下载并包含邮件附件 |
#### 输出
@@ -210,6 +211,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| --------- | ---- | ----------- |
| `message` | string | 成功或状态消息 |
| `results` | array | 邮件消息对象的数组 |
| `attachments` | file[] | 从所有邮件中提取的所有邮件附件 |
### `outlook_forward`

View File

@@ -0,0 +1,242 @@
---
title: Zep
description: 为 AI 代理提供长期记忆
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="zep"
color="#E8E8E8"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 233 196'
>
<path
d='m231.34,108.7l-1.48-1.55h-10.26l3.59-75.86-14.8-.45-2.77,49.31c-59.6-3.24-119.33-3.24-178.92-.02l-1.73-64.96-14.8.45,2.5,91.53H2.16l-1.41,1.47c-1.55,16.23-.66,32.68,2.26,48.89h10.83l.18,1.27c.67,19.34,16.1,34.68,35.9,34.68s44.86-.92,66.12-.92,46.56.92,65.95.92,35.19-15.29,35.9-34.61l.16-1.34h11.02c2.91-16.19,3.81-32.61,2.26-48.81Zm-158.23,58.01c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Zm86.51,0c-17.27,0-30.25-13.78-30.25-29.78s12.99-29.78,30.25-29.78,29.62,13.94,29.62,29.94-12.35,29.62-29.62,29.62Z'
fill='#FF1493'
/>
<polygon
points='111.77 22.4 93.39 49.97 93.52 50.48 185.88 38.51 190.95 27.68 114.32 36.55 117.7 31.48 117.7 31.47 138.38 .49 138.25 0 47.67 11.6 42.85 22.27 118.34 12.61 111.77 22.4'
fill='#FF1493'
/>
<path
d='m72.97,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
<path
d='m159.48,121.47c-8.67,0-15.73,6.93-15.73,15.46s7.06,15.46,15.73,15.46,15.37-6.75,15.37-15.37-6.75-15.55-15.37-15.55Z'
fill='#FF1493'
/>
</svg>`}
/>
## 使用说明
集成 Zep 以管理长期记忆。创建线程、添加消息、通过 AI 驱动的摘要和事实提取功能检索上下文。
## 工具
### `zep_create_thread`
在 Zep 中开始一个新的对话线程
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | 是 | 线程的唯一标识符 |
| `userId` | string | 是 | 与线程关联的用户 ID |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `threadId` | string | 线程 ID |
| `userId` | string | 用户 ID |
| `uuid` | string | 内部 UUID |
| `createdAt` | string | 创建时间戳 |
| `projectUuid` | string | 项目 UUID |
### `zep_get_threads`
列出所有对话线程
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `pageSize` | number | 否 | 每页检索的线程数量 |
| `pageNumber` | number | 否 | 分页的页码 |
| `orderBy` | string | 否 | 用于排序结果的字段 \(created_at, updated_at, user_id, thread_id\) |
| `asc` | boolean | 否 | 排序方向true 表示升序false 表示降序 |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `threads` | 数组 | 线程对象的数组 |
| `responseCount` | 数字 | 此响应中的线程数量 |
| `totalCount` | 数字 | 可用线程的总数 |
### `zep_delete_thread`
从 Zep 中删除会话线程
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `threadId` | 字符串 | 是 | 要删除的线程 ID |
| `apiKey` | 字符串 | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `deleted` | 布尔值 | 线程是否已被删除 |
### `zep_get_context`
从线程中检索用户上下文,支持摘要模式或基本模式
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `threadId` | 字符串 | 是 | 要获取上下文的线程 ID |
| `mode` | 字符串 | 否 | 上下文模式:"summary"(自然语言)或 "basic"(原始事实) |
| `minRating` | 数字 | 否 | 用于筛选相关事实的最低评分 |
| `apiKey` | 字符串 | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `context` | 字符串 | 上下文字符串(摘要或基本) |
| `facts` | 数组 | 提取的事实 |
| `entities` | 数组 | 提取的实体 |
| `summary` | 字符串 | 会话摘要 |
### `zep_get_messages`
从线程中检索消息
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | 是 | 要从中获取消息的线程 ID |
| `limit` | number | 否 | 要返回的最大消息数 |
| `cursor` | string | 否 | 用于分页的游标 |
| `lastn` | number | 否 | 要返回的最新消息数(覆盖限制和游标) |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `messages` | array | 消息对象数组 |
| `rowCount` | number | 此响应中的消息数 |
| `totalCount` | number | 线程中的消息总数 |
### `zep_add_messages`
向现有线程添加消息
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `threadId` | string | 是 | 要添加消息的线程 ID |
| `messages` | json | 是 | 包含角色和内容的消息对象数组 |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `context` | string | 添加消息后的更新上下文 |
| `messageIds` | array | 添加的消息 UUID 数组 |
| `threadId` | string | 线程 ID |
### `zep_add_user`
在 Zep 中创建新用户
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `userId` | string | 是 | 用户的唯一标识符 |
| `email` | string | 否 | 用户的电子邮件地址 |
| `firstName` | string | 否 | 用户的名字 |
| `lastName` | string | 否 | 用户的姓氏 |
| `metadata` | json | 否 | 作为 JSON 对象的附加元数据 |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `userId` | string | 用户 ID |
| `email` | string | 用户电子邮件 |
| `firstName` | string | 用户名字 |
| `lastName` | string | 用户姓氏 |
| `uuid` | string | 内部 UUID |
| `createdAt` | string | 创建时间戳 |
| `metadata` | object | 用户元数据 |
### `zep_get_user`
从 Zep 检索用户信息
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `userId` | string | 是 | 要检索的用户 ID |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `userId` | string | 用户 ID |
| `email` | string | 用户电子邮件 |
| `firstName` | string | 用户名字 |
| `lastName` | string | 用户姓氏 |
| `uuid` | string | 内部 UUID |
| `createdAt` | string | 创建时间戳 |
| `updatedAt` | string | 最后更新时间戳 |
| `metadata` | object | 用户元数据 |
### `zep_get_user_threads`
列出特定用户的所有会话线程
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `userId` | string | 是 | 要获取线程的用户 ID |
| `limit` | number | 否 | 要返回的最大线程数 |
| `apiKey` | string | 是 | 您的 Zep API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `threads` | array | 此用户的线程对象数组 |
| `userId` | string | 用户 ID |
## 注意
- 类别:`tools`
- 类型:`zep`

View File

@@ -11,8 +11,7 @@
"content/docs/[locale]/*.mdx",
"content/docs/[locale]/*/*.mdx",
"content/docs/[locale]/*/*/*.mdx"
],
"exclude": ["content/docs/[locale]/sdks/*.mdx"]
]
}
}
}

View File

@@ -1069,9 +1069,9 @@ checksums:
content/21: d80aeca589a53111bf1695cdd331cf5b
content/22: 7b2c4f76f939ba476f20884ac61751b9
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: d206d570561bd417cc86709da5cd0a3f
content/24: f61d9276ee4bd63f2ee11373ccd369f3
content/25: bcadfc362b69078beee0088e5936c98b
content/26: 77cd968476a6749228f609c8587b80dd
content/26: 743955528584fad4940470d1bc42c0d1
content/27: 7098784fe9ea35a3343e7c5cc6a34f59
content/28: 85608495eeeed8f3d33c1f0516435a07
content/29: 371d0e46b4bd2c23f559b8bc112f6955
@@ -1310,9 +1310,9 @@ checksums:
content/9: 66821a30fb30695c77e7b0a6337979c1
content/10: 2b79e754c09db25e8eba249a66f4a1ca
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: bbd02f60e378bf414d63c782ef4a7cde
content/12: 3b87d760806aebeb82653f4eed06e08f
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 127e8326cbcbe8516a4748db485e16c4
content/14: e5e1fa7bf66ae03d6ea900ea17ef28ae
content/15: 9a44ad2a020c00d5c50250ee0c543ae2
content/16: 3f013e8cf7537231614d510c9bcdf1b2
content/17: 371d0e46b4bd2c23f559b8bc112f6955
@@ -1322,9 +1322,9 @@ checksums:
content/21: 25d767f79c8b785d24657d66091e479b
content/22: 7cb6040a3113b9378333575292805e18
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: 1af1404c3595e86307f2f44f36e46c8a
content/24: 91affbbe15042adedd48b01483873129
content/25: bcadfc362b69078beee0088e5936c98b
content/26: 84d3a72b1119874d09537c524df4e2bc
content/26: 8941bd73040406ee68924d6e566efe1e
content/27: 89ae14a70cbc288e4f816b710c7ef48e
content/28: 311c355c6e43246e52112ee300df8db5
content/29: 371d0e46b4bd2c23f559b8bc112f6955
@@ -1976,8 +1976,49 @@ checksums:
meta/description: 13b4e5bceef6d1104e43d51d90fecf8e
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
content/1: a64277f09df51b99fcab31e92e106bed
content/2: b3f310d5ef115bea5a8b75bf25d7ea9a
content/3: 022b9e1e834f93bde611735a90c74ba8
content/2: 5e26527ccf8fddebe14c951e6c950b48
content/3: 18ab8a5892057b2629fdfa132c2bc99a
content/4: baefde094f2a3704e6963aa421acaba8
content/5: 21a69e345e1a48693ec398944754e999
content/6: 0c629dceccb0e3e2ba75dbb1a4757987
content/7: f3a38c0a13142335909263ee91899103
content/8: cba70dd7b00b275a91ded8755c3a0741
content/9: 782b550d15a06ff720e316057186022c
content/10: 75896132f96a46231588ee76b7a506dc
content/11: a9b57b3847a0b8d70d5315c0f96a9536
content/12: c9e7936c077be80272652b2dd30fd526
content/13: 6121e3dd3e9baebe39b10b59e2491fa6
content/14: 2c4c871ac88150beef559daa47957453
content/15: ee4064a3cf3cb394ba54cf208594890c
content/16: ec271420ee6f2ce91d8f900608b9261f
content/17: d0753484fe8f1c09124125d79dbd9beb
content/18: 8da01046db7127b6c454caa496b8b296
content/19: 5523f2e7b97e275e10cf55cf3f57afa7
content/20: b4857e265e096231a4f24be0d06987b6
content/21: d70affff7ccc15240cef59b8209f9e9c
content/22: 442299d79ced71d4bb59f4b0a46392a2
content/23: 08b6cfb90eb5a35b8511d88fe8c16fc3
content/24: b43a90f01b6192d918c3c1f2cddd2922
content/25: a63f15d6e9297c0731b40a8032be04c8
content/26: 16f4a97df43e4aaeff8614ad03bb4c6f
content/27: 7fd3613d3a651d0d5ce51d7f2049423d
content/28: d5ba126133858f23e7fc30e011c1e072
content/29: 4823775c5e1761754746a7bbd5486a1f
content/30: 6853158a0939b8ab539be1fda64c32da
content/31: 6d0006483a113fae930129b8daad43ad
content/32: a4a19b0692b9f0d6f6902e3dd65a887c
content/33: 2f430eafb54450f831b5acaecd97521f
content/34: b2a4a0c279f47d58a2456f25a1e1c6f9
content/35: 85c4d9ebf5a747f41505a57ff6ca3efa
content/36: 17033c46dca6954cfa35f364b9266cee
content/37: cc88c2e6e25b0ae8137daaf58e21a652
content/38: 1da265c4acca0e7a67efeec6884c08da
content/39: 4e062ddba9017e5918d98c3b71678129
content/40: 70df181932b7b36579ffc58bd22d3c53
content/41: 7a3be8a3771ee428ecf09008e42c0e2e
content/42: 9b4ff8b0793fc4337013bfcb316e2527
content/43: b3f310d5ef115bea5a8b75bf25d7ea9a
content/44: b2be17c101ec30b82d041616ab032a93
87beb559b312b9213347ee94e6506ae4:
meta/title: d8c4eebcc6c59235473743065c13300e
meta/description: 5e6724c34383ec8608d3713d0029c974
@@ -2397,72 +2438,33 @@ checksums:
content/110: 652b35852355ef0a9e0d7b634639cfc9
content/111: 7afd8cd395c8e63bb1ede3538307de54
content/112: 3304a33dfb626c6e2267c062e8956a9d
content/113: eea7828d6d712dc31d150bd438284b8a
content/114: 72a2b6306511832781715e503f0442d8
content/115: fe19a895e0b360386bfefc269ca596a5
content/116: d1bab8ec5a51a9da5464eb47e2a16b50
content/117: f4b91b5931dff5faaa26581272e51b61
content/118: 8f2d6066da0f1958aa55c6191f0a5be1
content/119: 2a1c4e0626d1a10a59ae80490435f1c6
content/120: a74834b7a623c3f5cc323d3ceeb5415f
content/121: f9da331cf341b7abe9fa4f1f021bb2ce
content/122: 006b642e4072cd86730b41503906c267
content/123: 4570f2c71f19c1a89aa15c62abea2a6b
content/124: bbb4874da742521d5e07d05e17056a3a
content/125: 35f33ebf126b4a967bccf057d990d8ea
content/126: 519f8d2ed7a62f11b58f924d05a1bdf4
content/127: 711f65ba5fbbc9204591770be00c0b76
content/128: d1bab8ec5a51a9da5464eb47e2a16b50
content/129: 9a2604ebbc848ffc92920e7ca9a1f5d5
content/130: 2d9792d07b8887ecb00b4bddaa77760d
content/131: 67c60dcbdcb96ec99abda80aed195a39
content/132: cd7923b20a896061bd8b437607a456ab
content/133: 6497acadfafd123e6501104bc339cb0b
content/134: 8d345f55098ed7fe8a79bfcd52780f56
content/135: 7f2808ae7cc39c74fa1ea2e023e16db8
content/136: 88f2cd0696cc8d78dc4b04549b26c13b
content/137: d1bab8ec5a51a9da5464eb47e2a16b50
content/138: 4760068730805bd746ad2b7abae14be7
content/139: 363a11a70ad2991f43e5c645343d24b1
content/140: 76193afa096b6017eb981461fbd0af36
content/141: 0e297ada3c58c15adbac6ede74fec86b
content/142: 08c8a11473671bde84d82b265f8f70cc
content/143: d1bab8ec5a51a9da5464eb47e2a16b50
content/144: ac1580a83d54361553916f1518ca3eff
content/145: a10c4dc3ab88bfa976836d5a2fd21168
content/146: e7ac414081f94d0ea1f36ef434392bb2
content/147: 5235c1c6fe2a3956c0b55c2d1827fb98
content/148: 074894d66efb4d36737a0735278e41cb
content/149: e5aa4e76b5d89aa3c2cda5255443eccd
content/150: b41c367dfb1527280df6c09bd32e626c
content/151: a5b2b0cf64941e8e962724a5728a5071
content/152: 08c8a11473671bde84d82b265f8f70cc
content/153: d1bab8ec5a51a9da5464eb47e2a16b50
content/154: da658275cc81a20f9cf7e4c66c7af1e3
content/155: d87f098de2239cceabdb02513ec754c0
content/156: 642e60a6fc14e6cca105684b0bc778fa
content/157: 41775a7801766af4164c8d93674e6cb2
content/158: 3afc03a5ab1dc9db2bfa092b0ac4826a
content/159: 18ddfcaf2be4a6f1d9819407dad9ce7c
content/160: 0d9a26fefe257a86593e24043e803357
content/161: 7ec562abd07f7db290d6f2accea4cd2d
content/162: ee8b87f59db1c578b95ad0198df084b7
content/163: 4603578d6b314b662f45564a34ca430d
content/164: cf4c97eb254d0bd6ea6633344621c2c2
content/165: 7b4640989fab002039936156f857eb21
content/166: 65ca9f08745b47b4cce8ea8247d043bf
content/167: 162b4180611ff0a53b782e4dc8109293
content/168: 6b367a189eb53cb198e3666023def89c
content/169: dbb2125cefcf618849600c1eccae8a64
content/170: 04eedda0da3767b06e6017c559e05414
content/171: 077ed20dc3e9d5664e20d2814825cbdf
content/172: a88260a5b5e23da73e4534376adeb193
content/173: e5e2329cdc226186fe9d44767528a4a0
content/174: 1773624e9ac3d5132b505894ef51977e
content/175: d62c9575cc66feec7589fba95c9f7aee
content/176: 7af652c5407ae7e156ab27b21a4f26d3
content/177: ecd571818ddf3d31b08b80a25958a662
content/178: 7dcdf2fbf3fce3f94987046506e12a9b
content/113: bf1afa789fdfa5815faaf43574341e90
content/114: 5f2fe55d098d4e4f438af595708b2280
content/115: 41b8f7cf8899a0e92e255a3f845f9584
content/116: 61ddd890032078ffd2da931b1d153b6d
content/117: 7873aa7487bc3e8a4826d65c1760a4a0
content/118: 98182d9aabe14d5bad43a5ee76a75eab
content/119: 2bdb01e4bcb08b1d99f192acf8e2fba7
content/120: 7079d9c00b1e1882c329b7e9b8f74552
content/121: 0f9d65eaf6e8de43c3d5fa7e62bc838d
content/122: 58c8e9d2d0ac37efd958203b8fbc8193
content/123: 7859d36a7a6d0122c0818b28ee29aa3e
content/124: ce185e7b041b8f95ebc11370d3e0aad9
content/125: 55c6b58dc7516918e8bfde837235ff52
content/126: 41c2bb95317d7c0421817a2b1a68cc09
content/127: 4c95f9fa55f698f220577380dff95011
content/128: 9ef273d776aada1b2cff3452f08ff985
content/129: 100e12673551d4ceb5b906b1b9c65059
content/130: ce253674cd7c49320203cda2bdd3685b
content/131: 8910afcea8c205a28256eb30de6a1f26
content/132: 4d7ad757d2c70fdff7834146d38dddd8
content/133: a88260a5b5e23da73e4534376adeb193
content/134: e5e2329cdc226186fe9d44767528a4a0
content/135: 1773624e9ac3d5132b505894ef51977e
content/136: d62c9575cc66feec7589fba95c9f7aee
content/137: 7af652c5407ae7e156ab27b21a4f26d3
content/138: ecd571818ddf3d31b08b80a25958a662
content/139: 7dcdf2fbf3fce3f94987046506e12a9b
27578f1315b6f1b7418d5e0d6042722e:
meta/title: 8c555594662512e95f28e20d3880f186
content/0: 9218a2e190598690d0fc5c27c30f01bb
@@ -3024,7 +3026,7 @@ checksums:
content/67: 0b22ed8a7e64005c666505c48e09f715
content/68: 494dcadaea5e62eddd599700511ecee5
content/69: 8332b16a0bf7a4c862f5104e9ffeb98d
content/70: 1de777052fc880c016398e0c1d464276
content/70: 90e2f984a874a8f954ddfd127ec8178a
0e322683b6d10e9fa8c9a17ff15a5fb1:
meta/title: a912b3c7fb996fefccb182cf5c4a3fbc
content/0: e1f8d4b13687e7d73b5b5fbb4cb6142d
@@ -4254,3 +4256,67 @@ checksums:
content/50: b2a4a0c279f47d58a2456f25a1e1c6f9
content/51: dd2fe5f38e6fadab2b5353cc6e8da57e
content/52: 604fec9d146658f2582bb53523378ac3
d29c2f5cb46e8d23d2ec6e814033ede8:
meta/title: 4e36553809660c7f65b68c72869e8813
meta/description: 0664282e49bfab44d249fb9b4e3e2be0
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
content/1: a1b88a777a1829218d892672b3d36009
content/2: 821e6394b0a953e2b0842b04ae8f3105
content/3: 90ec5fddbc461ffced3bc7d44319cf52
content/4: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
content/5: 5711a0ff79f0c42a843d81fe706df55b
content/6: 7cf359e27a894f26acc37d9dcff9d62b
content/7: 371d0e46b4bd2c23f559b8bc112f6955
content/8: b032ab99a64c0479121fa6a873430b6f
content/9: bcadfc362b69078beee0088e5936c98b
content/10: db2fbec7238480011503cc4d0705b56c
content/11: d5eaa5219d7e1a39c69748b5ebebd6a2
content/12: 7da2efbd5d91934427b86685efcaeed8
content/13: 371d0e46b4bd2c23f559b8bc112f6955
content/14: cbfc3c21364012d3af2b7967d72ff310
content/15: bcadfc362b69078beee0088e5936c98b
content/16: ddba5f8821ecc9755a6bed59ad599e2f
content/17: e2c01d982f4415f58b24860a62dfa257
content/18: b57d508facca5a3b25bd49cc4a94deb7
content/19: 371d0e46b4bd2c23f559b8bc112f6955
content/20: 695656d7f3da9ec4be0ddb0ab6340f0f
content/21: bcadfc362b69078beee0088e5936c98b
content/22: 33ab64ad090a5c41652a6381e64a8b29
content/23: 31010871ac088148188e433e71b528fd
content/24: ed4ba7f33c70c8c9c43f0cf5949a9e02
content/25: 371d0e46b4bd2c23f559b8bc112f6955
content/26: 1c1914a63d880607fa1f0ccaa47d6bdc
content/27: bcadfc362b69078beee0088e5936c98b
content/28: dfba53a44a72ddb05d937ac850642c32
content/29: 25c0139e6dbae0caf7045db76d678057
content/30: c11a5d76541d3111055068edb43d26af
content/31: 371d0e46b4bd2c23f559b8bc112f6955
content/32: f99a1195f3c8a932facc3878fd280f8c
content/33: bcadfc362b69078beee0088e5936c98b
content/34: 2bbc620c77f79f40e228bdb9f24c713e
content/35: 69c44fd6017f4b94f311bb9a15063cfa
content/36: 39cbf01ad9b1b7dcc3ec8fe6130a61ad
content/37: 371d0e46b4bd2c23f559b8bc112f6955
content/38: 722e870ff0fd01d4bf8abf17bfc56110
content/39: bcadfc362b69078beee0088e5936c98b
content/40: edf9a72a6b185e4dc921c84774bef409
content/41: ffae7ab7fc862449bf8c47818726e36d
content/42: 31375eb7944dce78bcba23c2e754f9e6
content/43: 371d0e46b4bd2c23f559b8bc112f6955
content/44: 563b7c2f4d5a654da1adf412665b9995
content/45: bcadfc362b69078beee0088e5936c98b
content/46: f41a8d427aa10bde30bc2f20ec2600a9
content/47: 489d9434245a2b07ed594438baac5067
content/48: ec8e3df9fda40ddfca53a1fc92584a0e
content/49: 371d0e46b4bd2c23f559b8bc112f6955
content/50: be0890e6d0e539576c2710db70f65bf9
content/51: bcadfc362b69078beee0088e5936c98b
content/52: ba68c28f140d47444ae79fb0e4ed5c8a
content/53: 1b93fcb390a8cb51777af3567601448e
content/54: 1160623d60e2189692c754b458c4fa30
content/55: 371d0e46b4bd2c23f559b8bc112f6955
content/56: 094163dad0578662e2a3197dbf665a4e
content/57: bcadfc362b69078beee0088e5936c98b
content/58: 74af50eceb2a5f514301c497a8e64030
content/59: b3f310d5ef115bea5a8b75bf25d7ea9a
content/60: 549a9bd7ff92264fbd18c9d6e616594b

View File

@@ -98,6 +98,7 @@ export async function POST(
const workflowResult = await db
.select({
isDeployed: workflow.isDeployed,
workspaceId: workflow.workspaceId,
})
.from(workflow)
.where(eq(workflow.id, deployment.workflowId))
@@ -123,11 +124,13 @@ export async function POST(
const { SSE_HEADERS } = await import('@/lib/utils')
const { createFilteredResult } = await import('@/app/api/workflows/[id]/execute/route')
// Generate executionId early so it can be used for file uploads and workflow execution
const executionId = crypto.randomUUID()
const workflowInput: any = { input, conversationId }
if (files && Array.isArray(files) && files.length > 0) {
logger.debug(`[${requestId}] Processing ${files.length} attached files`)
const executionId = crypto.randomUUID()
const executionContext = {
workspaceId: deployment.userId,
workflowId: deployment.workflowId,
@@ -144,7 +147,12 @@ export async function POST(
const stream = await createStreamingResponse({
requestId,
workflow: { id: deployment.workflowId, userId: deployment.userId, isDeployed: true },
workflow: {
id: deployment.workflowId,
userId: deployment.userId,
workspaceId: workflowResult[0].workspaceId,
isDeployed: true,
},
input: workflowInput,
executingUserId: deployment.userId,
streamConfig: {
@@ -153,6 +161,7 @@ export async function POST(
workflowTriggerType: 'chat',
},
createFilteredResult,
executionId,
})
const streamResponse = new NextResponse(stream, {

View File

@@ -3,10 +3,10 @@ import { chat, workflow } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { isDev } from '@/lib/environment'
import { processExecutionFiles } from '@/lib/execution/files'
import { createLogger } from '@/lib/logs/console/logger'
import { hasAdminPermission } from '@/lib/permissions/utils'
import { decryptSecret } from '@/lib/utils'
import { uploadExecutionFile } from '@/lib/workflows/execution-file-storage'
import type { UserFile } from '@/executor/types'
const logger = createLogger('ChatAuthUtils')
@@ -269,57 +269,20 @@ export async function validateChatAuth(
/**
* Process and upload chat files to execution storage
* Handles both base64 dataUrl format and direct URL pass-through
* Delegates to shared execution file processing logic
*/
export async function processChatFiles(
files: Array<{ dataUrl?: string; url?: string; name: string; type: string }>,
executionContext: { workspaceId: string; workflowId: string; executionId: string },
requestId: string
): Promise<UserFile[]> {
const uploadedFiles: UserFile[] = []
// Transform chat file format to shared execution file format
const transformedFiles = files.map((file) => ({
type: file.dataUrl ? 'file' : 'url',
data: file.dataUrl || file.url || '',
name: file.name,
mime: file.type,
}))
for (const file of files) {
try {
if (file.dataUrl) {
const dataUrlPrefix = 'data:'
const base64Prefix = ';base64,'
if (!file.dataUrl.startsWith(dataUrlPrefix)) {
logger.warn(`[${requestId}] Invalid dataUrl format for file: ${file.name}`)
continue
}
const base64Index = file.dataUrl.indexOf(base64Prefix)
if (base64Index === -1) {
logger.warn(
`[${requestId}] Invalid dataUrl format (no base64 marker) for file: ${file.name}`
)
continue
}
const mimeType = file.dataUrl.substring(dataUrlPrefix.length, base64Index)
const base64Data = file.dataUrl.substring(base64Index + base64Prefix.length)
const buffer = Buffer.from(base64Data, 'base64')
logger.debug(`[${requestId}] Uploading file to S3: ${file.name} (${buffer.length} bytes)`)
const userFile = await uploadExecutionFile(
executionContext,
buffer,
file.name,
mimeType || file.type
)
uploadedFiles.push(userFile)
logger.debug(`[${requestId}] Successfully uploaded ${file.name} with URL: ${userFile.url}`)
} else if (file.url) {
uploadedFiles.push(file as UserFile)
logger.debug(`[${requestId}] Using existing URL for file: ${file.name}`)
}
} catch (error) {
logger.error(`[${requestId}] Failed to process file ${file.name}:`, error)
throw new Error(`Failed to upload file: ${file.name}`)
}
}
return uploadedFiles
return processExecutionFiles(transformedFiles, executionContext, requestId)
}

View File

@@ -301,6 +301,7 @@ export async function GET(request: NextRequest) {
// Only process trace spans and detailed cost in full mode
let traceSpans = []
let finalOutput: any
let costSummary = (log.cost as any) || { total: 0 }
if (params.details === 'full' && log.executionData) {
@@ -316,6 +317,12 @@ export async function GET(request: NextRequest) {
log.cost && Object.keys(log.cost as any).length > 0
? (log.cost as any)
: extractCostSummary(blockExecutions)
// Include finalOutput if present on executionData
try {
const fo = (log.executionData as any)?.finalOutput
if (fo !== undefined) finalOutput = fo
} catch {}
}
const workflowSummary = {
@@ -346,6 +353,7 @@ export async function GET(request: NextRequest) {
totalDuration: log.totalDurationMs,
traceSpans,
blockExecutions,
finalOutput,
enhanced: true,
}
: undefined,

View File

@@ -11,6 +11,7 @@ import { checkServerSideUsageLimits } from '@/lib/billing'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { env } from '@/lib/env'
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { processExecutionFiles } from '@/lib/execution/files'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
@@ -23,11 +24,7 @@ import {
workflowHasResponseBlock,
} from '@/lib/workflows/utils'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
import {
createErrorResponse,
createSuccessResponse,
processApiWorkflowField,
} from '@/app/api/workflows/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { Executor } from '@/executor'
import type { ExecutionResult } from '@/executor/types'
import { Serializer } from '@/serializer'
@@ -124,10 +121,11 @@ export async function executeWorkflow(
onStream?: (streamingExec: any) => Promise<void> // Callback for streaming agent responses
onBlockComplete?: (blockId: string, output: any) => Promise<void> // Callback when any block completes
skipLoggingComplete?: boolean // When true, skip calling loggingSession.safeComplete (for streaming)
}
},
providedExecutionId?: string
): Promise<ExecutionResult> {
const workflowId = workflow.id
const executionId = uuidv4()
const executionId = providedExecutionId || uuidv4()
const executionKey = `${workflowId}:${requestId}`
@@ -577,6 +575,9 @@ export async function POST(
input: rawInput,
} = extractExecutionParams(request as NextRequest, parsedBody)
// Generate executionId early so it can be used for file uploads
const executionId = uuidv4()
let processedInput = rawInput
logger.info(`[${requestId}] Raw input received:`, JSON.stringify(rawInput, null, 2))
@@ -607,16 +608,18 @@ export async function POST(
const executionContext = {
workspaceId: validation.workflow.workspaceId,
workflowId,
executionId,
}
for (const fileField of fileFields) {
const fieldValue = rawInput[fileField.name]
if (fieldValue && typeof fieldValue === 'object') {
const uploadedFiles = await processApiWorkflowField(
const uploadedFiles = await processExecutionFiles(
fieldValue,
executionContext,
requestId
requestId,
isAsync
)
if (uploadedFiles.length > 0) {
@@ -769,6 +772,7 @@ export async function POST(
workflowTriggerType,
},
createFilteredResult,
executionId,
})
return new NextResponse(stream, {
@@ -782,7 +786,8 @@ export async function POST(
requestId,
input,
authenticatedUserId,
undefined
undefined,
executionId
)
const hasResponseBlock = workflowHasResponseBlock(result)

View File

@@ -1,14 +1,9 @@
import { NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { uploadExecutionFile } from '@/lib/workflows/execution-file-storage'
import type { UserFile } from '@/executor/types'
const logger = createLogger('WorkflowUtils')
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
export function createErrorResponse(error: string, status: number, code?: string) {
return NextResponse.json(
{
@@ -42,99 +37,3 @@ export async function verifyWorkspaceMembership(
return null
}
}
/**
* Process API workflow files - handles both base64 ('file' type) and URL pass-through ('url' type)
*/
export async function processApiWorkflowFiles(
file: { type: string; data: string; name: string; mime?: string },
executionContext: { workspaceId: string; workflowId: string; executionId: string },
requestId: string
): Promise<UserFile | null> {
if (file.type === 'file' && file.data && file.name) {
const dataUrlPrefix = 'data:'
const base64Prefix = ';base64,'
if (!file.data.startsWith(dataUrlPrefix)) {
logger.warn(`[${requestId}] Invalid data format for file: ${file.name}`)
return null
}
const base64Index = file.data.indexOf(base64Prefix)
if (base64Index === -1) {
logger.warn(`[${requestId}] Invalid data format (no base64 marker) for file: ${file.name}`)
return null
}
const mimeType = file.data.substring(dataUrlPrefix.length, base64Index)
const base64Data = file.data.substring(base64Index + base64Prefix.length)
const buffer = Buffer.from(base64Data, 'base64')
if (buffer.length > MAX_FILE_SIZE) {
const fileSizeMB = (buffer.length / (1024 * 1024)).toFixed(2)
throw new Error(
`File "${file.name}" exceeds the maximum size limit of 20MB (actual size: ${fileSizeMB}MB)`
)
}
logger.debug(`[${requestId}] Uploading file: ${file.name} (${buffer.length} bytes)`)
const userFile = await uploadExecutionFile(
executionContext,
buffer,
file.name,
mimeType || file.mime || 'application/octet-stream'
)
logger.debug(`[${requestId}] Successfully uploaded ${file.name}`)
return userFile
}
if (file.type === 'url' && file.data) {
return {
id: uuidv4(),
url: file.data,
name: file.name,
size: 0,
type: file.mime || 'application/octet-stream',
key: `url/${file.name}`,
uploadedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
}
}
return null
}
/**
* Process all files for a given field in the API workflow input
*/
export async function processApiWorkflowField(
fieldValue: any,
executionContext: { workspaceId: string; workflowId: string },
requestId: string
): Promise<UserFile[]> {
if (!fieldValue || typeof fieldValue !== 'object') {
return []
}
const files = Array.isArray(fieldValue) ? fieldValue : [fieldValue]
const uploadedFiles: UserFile[] = []
const executionId = uuidv4()
const fullContext = { ...executionContext, executionId }
for (const file of files) {
try {
const userFile = await processApiWorkflowFiles(file, fullContext, requestId)
if (userFile) {
uploadedFiles.push(userFile)
}
} catch (error) {
logger.error(`[${requestId}] Failed to process file ${file.name}:`, error)
throw new Error(`Failed to upload file: ${file.name}`)
}
}
return uploadedFiles
}

View File

@@ -1,305 +0,0 @@
import { db } from '@sim/db'
import { permissions, workflowExecutionLogs } from '@sim/db/schema'
import { and, desc, eq, gte, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('WorkflowExecutionDetailsAPI')
const QueryParamsSchema = z.object({
timeFilter: z.enum(['1h', '12h', '24h', '1w']).optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
triggers: z.string().optional(),
})
function getTimeRangeMs(filter: string): number {
switch (filter) {
case '1h':
return 60 * 60 * 1000
case '12h':
return 12 * 60 * 60 * 1000
case '24h':
return 24 * 60 * 60 * 1000
case '1w':
return 7 * 24 * 60 * 60 * 1000
default:
return 24 * 60 * 60 * 1000
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string; workflowId: string }> }
) {
const requestId = generateRequestId()
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized workflow details access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const { id: workspaceId, workflowId } = await params
const { searchParams } = new URL(request.url)
const queryParams = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
// Calculate time range - use custom times if provided, otherwise use timeFilter
let endTime: Date
let startTime: Date
if (queryParams.startTime && queryParams.endTime) {
startTime = new Date(queryParams.startTime)
endTime = new Date(queryParams.endTime)
} else {
endTime = new Date()
const timeRangeMs = getTimeRangeMs(queryParams.timeFilter || '24h')
startTime = new Date(endTime.getTime() - timeRangeMs)
}
const timeRangeMs = endTime.getTime() - startTime.getTime()
// Number of data points for the line charts
const dataPoints = 30
const segmentDurationMs = timeRangeMs / dataPoints
logger.debug(`[${requestId}] Fetching workflow details for ${workflowId}`)
// Check permissions
const [permission] = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId),
eq(permissions.userId, userId)
)
)
.limit(1)
if (!permission) {
logger.warn(`[${requestId}] User ${userId} has no permission for workspace ${workspaceId}`)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// Build conditions for log filtering
const logConditions = [
eq(workflowExecutionLogs.workflowId, workflowId),
gte(workflowExecutionLogs.startedAt, startTime),
]
// Add trigger filter if specified
if (queryParams.triggers) {
const triggerList = queryParams.triggers.split(',')
logConditions.push(inArray(workflowExecutionLogs.trigger, triggerList))
}
// Fetch all logs for this workflow in the time range
const logs = await db
.select({
id: workflowExecutionLogs.id,
executionId: workflowExecutionLogs.executionId,
level: workflowExecutionLogs.level,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
totalDurationMs: workflowExecutionLogs.totalDurationMs,
executionData: workflowExecutionLogs.executionData,
cost: workflowExecutionLogs.cost,
})
.from(workflowExecutionLogs)
.where(and(...logConditions))
.orderBy(desc(workflowExecutionLogs.startedAt))
.limit(50)
// Calculate metrics for each time segment
const errorRates: { timestamp: string; value: number }[] = []
const durations: { timestamp: string; value: number }[] = []
const executionCounts: { timestamp: string; value: number }[] = []
for (let i = 0; i < dataPoints; i++) {
const segmentStart = new Date(startTime.getTime() + i * segmentDurationMs)
const segmentEnd = new Date(startTime.getTime() + (i + 1) * segmentDurationMs)
// Filter logs for this segment
const segmentLogs = logs.filter((log) => {
const logTime = log.startedAt.getTime()
return logTime >= segmentStart.getTime() && logTime < segmentEnd.getTime()
})
const totalExecutions = segmentLogs.length
const errorExecutions = segmentLogs.filter((log) => log.level === 'error').length
const errorRate = totalExecutions > 0 ? (errorExecutions / totalExecutions) * 100 : 0
// Calculate average duration for this segment
const durationsInSegment = segmentLogs
.filter((log) => log.totalDurationMs !== null)
.map((log) => log.totalDurationMs!)
const avgDuration =
durationsInSegment.length > 0
? durationsInSegment.reduce((sum, d) => sum + d, 0) / durationsInSegment.length
: 0
errorRates.push({
timestamp: segmentStart.toISOString(),
value: errorRate,
})
durations.push({
timestamp: segmentStart.toISOString(),
value: avgDuration,
})
executionCounts.push({
timestamp: segmentStart.toISOString(),
value: totalExecutions,
})
}
// Helper function to recursively search for error in trace spans
const findErrorInSpans = (spans: any[]): string | null => {
for (const span of spans) {
if (span.status === 'error' && span.output?.error) {
return span.output.error
}
if (span.children && Array.isArray(span.children)) {
const childError = findErrorInSpans(span.children)
if (childError) return childError
}
}
return null
}
// Helper function to get all blocks from trace spans (flattened)
const flattenTraceSpans = (spans: any[]): any[] => {
const flattened: any[] = []
for (const span of spans) {
if (span.type !== 'workflow') {
flattened.push(span)
}
if (span.children && Array.isArray(span.children)) {
flattened.push(...flattenTraceSpans(span.children))
}
}
return flattened
}
// Format logs for response
const formattedLogs = logs.map((log) => {
const executionData = log.executionData as any
const triggerData = executionData?.trigger || {}
const traceSpans = executionData?.traceSpans || []
// Extract error message from trace spans
let errorMessage = null
if (log.level === 'error') {
errorMessage = findErrorInSpans(traceSpans)
// Fallback to executionData.errorDetails
if (!errorMessage) {
errorMessage = executionData?.errorDetails?.error || null
}
}
// Extract outputs from the last block in trace spans
let outputs = null
let cost = null
if (traceSpans.length > 0) {
// Flatten all blocks from trace spans
const allBlocks = flattenTraceSpans(traceSpans)
// Find the last successful block execution
const successBlocks = allBlocks.filter(
(span: any) =>
span.status !== 'error' && span.output && Object.keys(span.output).length > 0
)
if (successBlocks.length > 0) {
const lastBlock = successBlocks[successBlocks.length - 1]
const blockOutput = lastBlock.output || {}
// Clean up the output to show meaningful data
// Priority: content > result > data > the whole output object
if (blockOutput.content) {
outputs = { content: blockOutput.content }
} else if (blockOutput.result !== undefined) {
outputs = { result: blockOutput.result }
} else if (blockOutput.data !== undefined) {
outputs = { data: blockOutput.data }
} else {
// Filter out internal/metadata fields for cleaner display
const cleanOutput: any = {}
for (const [key, value] of Object.entries(blockOutput)) {
if (
![
'executionTime',
'tokens',
'model',
'cost',
'childTraceSpans',
'error',
'stackTrace',
].includes(key)
) {
cleanOutput[key] = value
}
}
if (Object.keys(cleanOutput).length > 0) {
outputs = cleanOutput
}
}
// Extract cost from the block output
if (blockOutput.cost) {
cost = blockOutput.cost
}
}
}
// Use the cost stored at the top-level in workflowExecutionLogs table
// This is the same cost shown in the logs page
const logCost = log.cost as any
return {
id: log.id,
executionId: log.executionId,
startedAt: log.startedAt.toISOString(),
level: log.level,
trigger: log.trigger,
triggerUserId: triggerData.userId || null,
triggerInputs: triggerData.inputs || triggerData.data || null,
outputs,
errorMessage,
duration: log.totalDurationMs,
cost: logCost
? {
input: logCost.input || 0,
output: logCost.output || 0,
total: logCost.total || 0,
}
: null,
}
})
logger.debug(`[${requestId}] Successfully calculated workflow details`)
logger.debug(`[${requestId}] Returning ${formattedLogs.length} execution logs`)
return NextResponse.json({
errorRates,
durations,
executionCounts,
logs: formattedLogs,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
})
} catch (error) {
logger.error(`[${requestId}] Error fetching workflow details:`, error)
return NextResponse.json({ error: 'Failed to fetch workflow details' }, { status: 500 })
}
}

View File

@@ -1,223 +0,0 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, eq, gte, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('ExecutionHistoryAPI')
const QueryParamsSchema = z.object({
timeFilter: z.enum(['1h', '12h', '24h', '1w']).optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
segments: z.coerce.number().min(1).max(200).default(120),
workflowIds: z.string().optional(),
folderIds: z.string().optional(),
triggers: z.string().optional(),
})
interface TimeSegment {
successRate: number
timestamp: string
hasExecutions: boolean
totalExecutions: number
successfulExecutions: number
}
interface WorkflowExecution {
workflowId: string
workflowName: string
segments: TimeSegment[]
overallSuccessRate: number
}
function getTimeRangeMs(filter: string): number {
switch (filter) {
case '1h':
return 60 * 60 * 1000
case '12h':
return 12 * 60 * 60 * 1000
case '24h':
return 24 * 60 * 60 * 1000
case '1w':
return 7 * 24 * 60 * 60 * 1000
default:
return 24 * 60 * 60 * 1000
}
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized execution history access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const { id: workspaceId } = await params
const { searchParams } = new URL(request.url)
const queryParams = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
// Calculate time range - use custom times if provided, otherwise use timeFilter
let endTime: Date
let startTime: Date
if (queryParams.startTime && queryParams.endTime) {
startTime = new Date(queryParams.startTime)
endTime = new Date(queryParams.endTime)
} else {
endTime = new Date()
const timeRangeMs = getTimeRangeMs(queryParams.timeFilter || '24h')
startTime = new Date(endTime.getTime() - timeRangeMs)
}
const timeRangeMs = endTime.getTime() - startTime.getTime()
const segmentDurationMs = timeRangeMs / queryParams.segments
logger.debug(`[${requestId}] Fetching execution history for workspace ${workspaceId}`)
logger.debug(
`[${requestId}] Time range: ${startTime.toISOString()} to ${endTime.toISOString()}`
)
logger.debug(
`[${requestId}] Segments: ${queryParams.segments}, duration: ${segmentDurationMs}ms`
)
// Check permissions
const [permission] = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId),
eq(permissions.userId, userId)
)
)
.limit(1)
if (!permission) {
logger.warn(`[${requestId}] User ${userId} has no permission for workspace ${workspaceId}`)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// Build workflow query conditions
const workflowConditions = [eq(workflow.workspaceId, workspaceId)]
// Apply workflow ID filter
if (queryParams.workflowIds) {
const workflowIdList = queryParams.workflowIds.split(',')
workflowConditions.push(inArray(workflow.id, workflowIdList))
}
// Apply folder ID filter
if (queryParams.folderIds) {
const folderIdList = queryParams.folderIds.split(',')
workflowConditions.push(inArray(workflow.folderId, folderIdList))
}
// Get all workflows in the workspace with optional filters
const workflows = await db
.select({
id: workflow.id,
name: workflow.name,
})
.from(workflow)
.where(and(...workflowConditions))
logger.debug(`[${requestId}] Found ${workflows.length} workflows`)
// Use Promise.all to fetch logs in parallel per workflow
// This is better than single query when workflows have 10k+ logs each
const workflowExecutions: WorkflowExecution[] = await Promise.all(
workflows.map(async (wf) => {
// Build conditions for log filtering
const logConditions = [
eq(workflowExecutionLogs.workflowId, wf.id),
gte(workflowExecutionLogs.startedAt, startTime),
]
// Add trigger filter if specified
if (queryParams.triggers) {
const triggerList = queryParams.triggers.split(',')
logConditions.push(inArray(workflowExecutionLogs.trigger, triggerList))
}
// Fetch logs for this workflow - runs in parallel with others
const logs = await db
.select({
id: workflowExecutionLogs.id,
level: workflowExecutionLogs.level,
startedAt: workflowExecutionLogs.startedAt,
})
.from(workflowExecutionLogs)
.where(and(...logConditions))
// Initialize segments with timestamps
const segments: TimeSegment[] = []
let totalSuccess = 0
let totalExecutions = 0
for (let i = 0; i < queryParams.segments; i++) {
const segmentStart = new Date(startTime.getTime() + i * segmentDurationMs)
const segmentEnd = new Date(startTime.getTime() + (i + 1) * segmentDurationMs)
// Count executions in this segment
const segmentLogs = logs.filter((log) => {
const logTime = log.startedAt.getTime()
return logTime >= segmentStart.getTime() && logTime < segmentEnd.getTime()
})
const segmentTotal = segmentLogs.length
const segmentErrors = segmentLogs.filter((log) => log.level === 'error').length
const segmentSuccess = segmentTotal - segmentErrors
// Calculate success rate (default to 100% if no executions in this segment)
const hasExecutions = segmentTotal > 0
const successRate = hasExecutions ? (segmentSuccess / segmentTotal) * 100 : 100
segments.push({
successRate,
timestamp: segmentStart.toISOString(),
hasExecutions,
totalExecutions: segmentTotal,
successfulExecutions: segmentSuccess,
})
totalExecutions += segmentTotal
totalSuccess += segmentSuccess
}
// Calculate overall success rate (percentage of non-errored executions)
const overallSuccessRate =
totalExecutions > 0 ? (totalSuccess / totalExecutions) * 100 : 100
return {
workflowId: wf.id,
workflowName: wf.name,
segments,
overallSuccessRate,
}
})
)
logger.debug(
`[${requestId}] Successfully calculated execution history for ${workflowExecutions.length} workflows`
)
return NextResponse.json({
workflows: workflowExecutions,
segments: queryParams.segments,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
})
} catch (error) {
logger.error(`[${requestId}] Error fetching execution history:`, error)
return NextResponse.json({ error: 'Failed to fetch execution history' }, { status: 500 })
}
}

View File

@@ -0,0 +1,174 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, eq, gte, inArray, lte } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('MetricsExecutionsAPI')
const QueryParamsSchema = z.object({
startTime: z.string().optional(),
endTime: z.string().optional(),
segments: z.coerce.number().min(1).max(200).default(72),
workflowIds: z.string().optional(),
folderIds: z.string().optional(),
triggers: z.string().optional(),
})
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id: workspaceId } = await params
const { searchParams } = new URL(request.url)
const qp = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const end = qp.endTime ? new Date(qp.endTime) : new Date()
const start = qp.startTime
? new Date(qp.startTime)
: new Date(end.getTime() - 24 * 60 * 60 * 1000)
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || start >= end) {
return NextResponse.json({ error: 'Invalid time range' }, { status: 400 })
}
const segments = qp.segments
const totalMs = Math.max(1, end.getTime() - start.getTime())
const segmentMs = Math.max(1, Math.floor(totalMs / Math.max(1, segments)))
const [permission] = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId),
eq(permissions.userId, userId)
)
)
.limit(1)
if (!permission) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const wfWhere = [eq(workflow.workspaceId, workspaceId)] as any[]
if (qp.folderIds) {
const folderList = qp.folderIds.split(',').filter(Boolean)
wfWhere.push(inArray(workflow.folderId, folderList))
}
if (qp.workflowIds) {
const wfList = qp.workflowIds.split(',').filter(Boolean)
wfWhere.push(inArray(workflow.id, wfList))
}
const workflows = await db
.select({ id: workflow.id, name: workflow.name })
.from(workflow)
.where(and(...wfWhere))
if (workflows.length === 0) {
return NextResponse.json({
workflows: [],
startTime: start.toISOString(),
endTime: end.toISOString(),
segmentMs,
})
}
const workflowIdList = workflows.map((w) => w.id)
const logWhere = [
inArray(workflowExecutionLogs.workflowId, workflowIdList),
gte(workflowExecutionLogs.startedAt, start),
lte(workflowExecutionLogs.startedAt, end),
] as any[]
if (qp.triggers) {
const t = qp.triggers.split(',').filter(Boolean)
logWhere.push(inArray(workflowExecutionLogs.trigger, t))
}
const logs = await db
.select({
workflowId: workflowExecutionLogs.workflowId,
level: workflowExecutionLogs.level,
startedAt: workflowExecutionLogs.startedAt,
totalDurationMs: workflowExecutionLogs.totalDurationMs,
})
.from(workflowExecutionLogs)
.where(and(...logWhere))
type Bucket = {
timestamp: string
totalExecutions: number
successfulExecutions: number
durations: number[]
}
const wfIdToBuckets = new Map<string, Bucket[]>()
for (const wf of workflows) {
const buckets: Bucket[] = Array.from({ length: segments }, (_, i) => ({
timestamp: new Date(start.getTime() + i * segmentMs).toISOString(),
totalExecutions: 0,
successfulExecutions: 0,
durations: [],
}))
wfIdToBuckets.set(wf.id, buckets)
}
for (const log of logs) {
const idx = Math.min(
segments - 1,
Math.max(0, Math.floor((log.startedAt.getTime() - start.getTime()) / segmentMs))
)
const buckets = wfIdToBuckets.get(log.workflowId)
if (!buckets) continue
const b = buckets[idx]
b.totalExecutions += 1
if ((log.level || '').toLowerCase() !== 'error') b.successfulExecutions += 1
if (typeof log.totalDurationMs === 'number') b.durations.push(log.totalDurationMs)
}
function percentile(arr: number[], p: number): number {
if (arr.length === 0) return 0
const sorted = [...arr].sort((a, b) => a - b)
const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * (sorted.length - 1)))
return sorted[idx]
}
const result = workflows.map((wf) => {
const buckets = wfIdToBuckets.get(wf.id) as Bucket[]
const segmentsOut = buckets.map((b) => {
const avg =
b.durations.length > 0
? Math.round(b.durations.reduce((s, d) => s + d, 0) / b.durations.length)
: 0
const p50 = percentile(b.durations, 50)
const p90 = percentile(b.durations, 90)
const p99 = percentile(b.durations, 99)
return {
timestamp: b.timestamp,
totalExecutions: b.totalExecutions,
successfulExecutions: b.successfulExecutions,
avgDurationMs: avg,
p50Ms: p50,
p90Ms: p90,
p99Ms: p99,
}
})
return { workflowId: wf.id, workflowName: wf.name, segments: segmentsOut }
})
return NextResponse.json({
workflows: result,
startTime: start.toISOString(),
endTime: end.toISOString(),
segmentMs,
})
} catch (error) {
logger.error('MetricsExecutionsAPI error', error)
return NextResponse.json({ error: 'Failed to compute metrics' }, { status: 500 })
}
}

View File

@@ -1,9 +1,10 @@
import type { ReactNode } from 'react'
import { Loader2, Play, RefreshCw, Search, Square } from 'lucide-react'
import { Loader2, RefreshCw, Search } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { soehne } from '@/app/fonts/soehne/soehne'
import Timeline from '@/app/workspace/[workspaceId]/logs/components/filters/components/timeline'
export function Controls({
@@ -32,7 +33,12 @@ export function Controls({
onExport?: () => void
}) {
return (
<div className='mb-8 flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-start'>
<div
className={cn(
'mb-8 flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-start',
soehne.className
)}
>
{searchComponent ? (
searchComponent
) : (
@@ -87,34 +93,32 @@ export function Controls({
<TooltipContent>{isRefetching ? 'Refreshing...' : 'Refresh'}</TooltipContent>
</Tooltip>
{showExport && viewMode !== 'dashboard' && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
onClick={onExport}
className='h-9 rounded-[11px] hover:bg-secondary'
aria-label='Export CSV'
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='ghost'
size='icon'
onClick={onExport}
className='h-9 rounded-[11px] hover:bg-secondary'
aria-label='Export CSV'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
className='h-5 w-5'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
className='h-5 w-5'
>
<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' />
<polyline points='7 10 12 15 17 10' />
<line x1='12' y1='15' x2='12' y2='3' />
</svg>
<span className='sr-only'>Export CSV</span>
</Button>
</TooltipTrigger>
<TooltipContent>Export CSV</TooltipContent>
</Tooltip>
)}
<path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' />
<polyline points='7 10 12 15 17 10' />
<line x1='12' y1='15' x2='12' y2='3' />
</svg>
<span className='sr-only'>Export CSV</span>
</Button>
</TooltipTrigger>
<TooltipContent>Export CSV</TooltipContent>
</Tooltip>
<div className='inline-flex h-9 items-center rounded-[11px] border bg-card p-1 shadow-sm'>
<Button
@@ -123,21 +127,13 @@ export function Controls({
onClick={() => setLive((v) => !v)}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
live ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground'
live
? 'bg-[var(--brand-primary-hex)] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)] hover:text-white hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={live}
>
{live ? (
<>
<Square className='mr-1.5 h-3 w-3 fill-current' />
Live
</>
) : (
<>
<Play className='mr-1.5 h-3 w-3' />
Live
</>
)}
Live
</Button>
</div>

View File

@@ -8,28 +8,28 @@ export interface AggregateMetrics {
export function KPIs({ aggregate }: { aggregate: AggregateMetrics }) {
return (
<div className='mb-5 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4'>
<div className='mb-2 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4'>
<div className='rounded-[12px] border bg-card p-4 shadow-sm'>
<div className='text-muted-foreground text-xs'>Total executions</div>
<div className='mt-1 font-semibold text-[22px] leading-6'>
<div className='mt-1 font-[440] text-[22px] leading-6'>
{aggregate.totalExecutions.toLocaleString()}
</div>
</div>
<div className='rounded-[12px] border bg-card p-4 shadow-sm'>
<div className='text-muted-foreground text-xs'>Success rate</div>
<div className='mt-1 font-semibold text-[22px] leading-6'>
<div className='mt-1 font-[440] text-[22px] leading-6'>
{aggregate.successRate.toFixed(1)}%
</div>
</div>
<div className='rounded-[12px] border bg-card p-4 shadow-sm'>
<div className='text-muted-foreground text-xs'>Failed executions</div>
<div className='mt-1 font-semibold text-[22px] leading-6'>
<div className='mt-1 font-[440] text-[22px] leading-6'>
{aggregate.failedExecutions.toLocaleString()}
</div>
</div>
<div className='rounded-[12px] border bg-card p-4 shadow-sm'>
<div className='text-muted-foreground text-xs'>Active workflows</div>
<div className='mt-1 font-semibold text-[22px] leading-6'>{aggregate.activeWorkflows}</div>
<div className='mt-1 font-[440] text-[22px] leading-6'>{aggregate.activeWorkflows}</div>
</div>
</div>
)

View File

@@ -1,30 +1,38 @@
import { useEffect, useRef, useState } from 'react'
import { TooltipProvider } from '@/components/ui/tooltip'
import { formatDate as formatDashboardDate } from '../../utils/format-date'
export interface LineChartPoint {
timestamp: string
value: number
}
export interface LineChartMultiSeries {
id?: string
label: string
color: string
data: LineChartPoint[]
dashed?: boolean
}
export function LineChart({
data,
label,
color,
unit,
series,
}: {
data: LineChartPoint[]
label: string
color: string
unit?: string
series?: LineChartMultiSeries[]
}) {
// Responsive sizing: chart fills its container width
const containerRef = useRef<HTMLDivElement | null>(null)
const [containerWidth, setContainerWidth] = useState<number>(420)
const width = containerWidth
const height = 176
// Add a touch more space below the axis so curves never visually clip it
const padding = { top: 18, right: 18, bottom: 32, left: 42 }
// Observe container width for responsiveness
const height = 166
const padding = { top: 16, right: 28, bottom: 26, left: 26 }
useEffect(() => {
if (!containerRef.current) return
const element = containerRef.current
@@ -36,7 +44,6 @@ export function LineChart({
}
})
ro.observe(element)
// Initialize once immediately
const rect = element.getBoundingClientRect()
if (rect?.width) setContainerWidth(Math.max(280, Math.floor(rect.width)))
return () => ro.disconnect()
@@ -45,6 +52,9 @@ export function LineChart({
const chartHeight = height - padding.top - padding.bottom
const [hoverIndex, setHoverIndex] = useState<number | null>(null)
const [isDark, setIsDark] = useState<boolean>(true)
const [hoverSeriesId, setHoverSeriesId] = useState<string | null>(null)
const [activeSeriesId, setActiveSeriesId] = useState<string | null>(null)
const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | null>(null)
useEffect(() => {
if (typeof window === 'undefined') return
@@ -67,26 +77,57 @@ export function LineChart({
)
}
// Ensure nice padding on the y-domain so the line never hugs the axes
const rawMax = Math.max(...data.map((d) => d.value), 1)
const rawMin = Math.min(...data.map((d) => d.value), 0)
const allSeries = (
Array.isArray(series) && series.length > 0
? [{ id: 'base', label, color, data }, ...series]
: [{ id: 'base', label, color, data }]
).map((s, idx) => ({ ...s, id: s.id || s.label || String(idx) }))
const flatValues = allSeries.flatMap((s) => s.data.map((d) => d.value))
const rawMax = Math.max(...flatValues, 1)
const rawMin = Math.min(...flatValues, 0)
const paddedMax = rawMax === 0 ? 1 : rawMax * 1.1
const paddedMin = Math.min(0, rawMin) // never below zero for our metrics
const maxValue = Math.ceil(paddedMax)
const minValue = Math.floor(paddedMin)
const paddedMin = Math.min(0, rawMin)
const unitSuffixPre = (unit || '').trim().toLowerCase()
let maxValue = Math.ceil(paddedMax)
let minValue = Math.floor(paddedMin)
if (unitSuffixPre === 'ms') {
maxValue = Math.max(1000, Math.ceil(paddedMax / 1000) * 1000)
minValue = 0
}
const valueRange = maxValue - minValue || 1
const yMin = padding.top + 3
const yMax = padding.top + chartHeight - 3
const scaledPoints = data.map((d, i) => {
const x = padding.left + (i / (data.length - 1 || 1)) * chartWidth
const usableW = Math.max(1, chartWidth)
const x = padding.left + (i / (data.length - 1 || 1)) * usableW
const rawY = padding.top + chartHeight - ((d.value - minValue) / valueRange) * chartHeight
// keep the line safely within the plotting area to avoid clipping behind the x-axis
const y = Math.max(yMin, Math.min(yMax, rawY))
return { x, y }
})
const scaledSeries = allSeries.map((s) => {
const pts = s.data.map((d, i) => {
const usableW = Math.max(1, chartWidth)
const x = padding.left + (i / (s.data.length - 1 || 1)) * usableW
const rawY = padding.top + chartHeight - ((d.value - minValue) / valueRange) * chartHeight
const y = Math.max(yMin, Math.min(yMax, rawY))
return { x, y }
})
return { ...s, pts }
})
const getSeriesById = (id?: string | null) => scaledSeries.find((s) => s.id === id)
const visibleSeries = activeSeriesId
? scaledSeries.filter((s) => s.id === activeSeriesId)
: scaledSeries
const orderedSeries = (() => {
if (!activeSeriesId) return visibleSeries
return visibleSeries
})()
const pathD = (() => {
if (scaledPoints.length <= 1) return ''
const p = scaledPoints
@@ -101,7 +142,6 @@ export function LineChart({
let cp1y = p1.y + ((p2.y - p0.y) / 6) * tension
const cp2x = p2.x - ((p3.x - p1.x) / 6) * tension
let cp2y = p2.y - ((p3.y - p1.y) / 6) * tension
// Clamp control points vertically to avoid bezier overshoot below the axis
cp1y = Math.max(yMin, Math.min(yMax, cp1y))
cp2y = Math.max(yMin, Math.min(yMax, cp2y))
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`
@@ -109,15 +149,88 @@ export function LineChart({
return d
})()
const getCompactDateLabel = (timestamp?: string) => {
if (!timestamp) return ''
try {
const f = formatDashboardDate(timestamp)
return `${f.compactDate} · ${f.compactTime}`
} catch (e) {
const d = new Date(timestamp)
if (Number.isNaN(d.getTime())) return ''
return d.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
}
}
const currentHoverDate =
hoverIndex !== null && data[hoverIndex] ? getCompactDateLabel(data[hoverIndex].timestamp) : ''
return (
<div ref={containerRef} className='w-full rounded-[11px] border bg-card p-4 shadow-sm'>
<h4 className='mb-3 font-medium text-foreground text-sm'>{label}</h4>
<div
ref={containerRef}
className='w-full overflow-hidden rounded-[11px] border bg-card p-4 shadow-sm'
>
<div className='mb-3 flex items-center justify-between'>
<div className='flex items-center gap-3'>
<h4 className='font-medium text-foreground text-sm'>{label}</h4>
{allSeries.length > 1 && (
<div className='flex items-center gap-2'>
{scaledSeries.slice(1).map((s) => {
const isActive = activeSeriesId ? activeSeriesId === s.id : true
const isHovered = hoverSeriesId === s.id
const dimmed = activeSeriesId ? !isActive : false
return (
<button
key={`legend-${s.id}`}
type='button'
aria-pressed={activeSeriesId === s.id}
aria-label={`Toggle ${s.label}`}
className='inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px]'
style={{
color: s.color,
opacity: dimmed ? 0.4 : isHovered ? 1 : 0.9,
border: '1px solid hsl(var(--border))',
background: 'transparent',
}}
onMouseEnter={() => setHoverSeriesId(s.id || null)}
onMouseLeave={() => setHoverSeriesId((prev) => (prev === s.id ? null : prev))}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveSeriesId((prev) => (prev === s.id ? null : s.id || null))
}
}}
onClick={() =>
setActiveSeriesId((prev) => (prev === s.id ? null : s.id || null))
}
>
<span
aria-hidden='true'
className='inline-block h-[6px] w-[6px] rounded-full'
style={{ backgroundColor: s.color }}
/>
<span style={{ color: 'hsl(var(--muted-foreground))' }}>{s.label}</span>
</button>
)
})}
</div>
)}
</div>
{currentHoverDate ? (
<div className='text-[10px] text-muted-foreground'>{currentHoverDate}</div>
) : null}
</div>
<TooltipProvider delayDuration={0}>
<div className='relative' style={{ width, height }}>
<svg
width={width}
height={height}
className='overflow-visible'
className='overflow-hidden'
onMouseMove={(e) => {
if (scaledPoints.length === 0) return
const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect()
@@ -126,8 +239,29 @@ export function LineChart({
const ratio = (clamped - padding.left) / (chartWidth || 1)
const i = Math.round(ratio * (scaledPoints.length - 1))
setHoverIndex(i)
setHoverPos({ x: clamped, y: e.clientY - rect.top })
const cursorY = e.clientY - rect.top
if (activeSeriesId) {
setHoverSeriesId(activeSeriesId)
} else {
let best: { id: string | null; dy: number } = {
id: null,
dy: Number.POSITIVE_INFINITY,
}
for (const s of scaledSeries.slice(1)) {
const pt = s.pts[i]
if (!pt) continue
const dy = Math.abs(pt.y - cursorY)
if (dy < best.dy) best = { id: s.id || null, dy }
}
setHoverSeriesId(best.dy <= 12 ? best.id : null)
}
}}
onMouseLeave={() => {
setHoverIndex(null)
setHoverPos(null)
setHoverSeriesId(null)
}}
onMouseLeave={() => setHoverIndex(null)}
>
<defs>
<linearGradient id={`area-${label.replace(/\s+/g, '-')}`} x1='0' x2='0' y1='0' y2='1'>
@@ -138,7 +272,7 @@ export function LineChart({
<rect
x={padding.left}
y={yMin}
width={chartWidth}
width={Math.max(1, chartWidth)}
height={chartHeight - (yMin - padding.top) * 2}
rx='2'
/>
@@ -156,7 +290,7 @@ export function LineChart({
{[0.25, 0.5, 0.75].map((p) => (
<line
key={p}
key={`${label}-grid-${p}`}
x1={padding.left}
y1={padding.top + chartHeight * p}
x2={width - padding.right}
@@ -169,7 +303,7 @@ export function LineChart({
{/* axis baseline is drawn last (after line) to visually mask any overshoot */}
{scaledPoints.length > 1 && (
{!activeSeriesId && scaledPoints.length > 1 && (
<path
d={`${pathD} L ${scaledPoints[scaledPoints.length - 1].x} ${height - padding.bottom} L ${scaledPoints[0].x} ${height - padding.bottom} Z`}
fill={`url(#area-${label.replace(/\s+/g, '-')})`}
@@ -178,58 +312,146 @@ export function LineChart({
/>
)}
{scaledPoints.length > 1 ? (
<path
d={pathD}
fill='none'
stroke={color}
strokeWidth={isDark ? 1.75 : 2.25}
strokeLinecap='round'
clipPath={`url(#clip-${label.replace(/\s+/g, '-')})`}
style={{ mixBlendMode: isDark ? 'screen' : 'normal' }}
/>
) : (
// Single-point series: show a dot so the value doesn't "disappear"
<circle cx={scaledPoints[0].x} cy={scaledPoints[0].y} r='3' fill={color} />
)}
{orderedSeries.map((s, idx) => {
const isActive = activeSeriesId ? activeSeriesId === s.id : true
const isHovered = hoverSeriesId ? hoverSeriesId === s.id : false
const baseOpacity = isActive ? 1 : 0.12
const strokeOpacity = isHovered ? 1 : baseOpacity
const sw = (() => {
switch ((s.id || '').toLowerCase()) {
case 'p50':
return isDark ? 1.5 : 1.7
case 'p90':
return isDark ? 1.9 : 2.1
case 'p99':
return isDark ? 2.3 : 2.5
default:
return isDark ? 1.7 : 2.0
}
})()
if (s.pts.length <= 1) {
return (
<circle
key={`pt-${idx}`}
cx={s.pts[0]?.x}
cy={s.pts[0]?.y}
r='3'
fill={s.color}
opacity={strokeOpacity}
/>
)
}
const p = (() => {
const p = s.pts
const tension = 0.2
let d = `M ${p[0].x} ${p[0].y}`
for (let i = 0; i < p.length - 1; i++) {
const p0 = p[i - 1] || p[i]
const p1 = p[i]
const p2 = p[i + 1]
const p3 = p[i + 2] || p[i + 1]
const cp1x = p1.x + ((p2.x - p0.x) / 6) * tension
let cp1y = p1.y + ((p2.y - p0.y) / 6) * tension
const cp2x = p2.x - ((p3.x - p1.x) / 6) * tension
let cp2y = p2.y - ((p3.y - p1.y) / 6) * tension
cp1y = Math.max(yMin, Math.min(yMax, cp1y))
cp2y = Math.max(yMin, Math.min(yMax, cp2y))
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`
}
return d
})()
return (
<path
key={`series-${idx}`}
d={p}
fill='none'
stroke={s.color}
strokeWidth={sw}
strokeLinecap='round'
clipPath={`url(#clip-${label.replace(/\s+/g, '-')})`}
style={{ cursor: 'pointer', mixBlendMode: isDark ? 'screen' : 'normal' }}
strokeDasharray={s.dashed ? '5 4' : undefined}
opacity={strokeOpacity}
onClick={() => setActiveSeriesId((prev) => (prev === s.id ? null : s.id || null))}
/>
)
})}
{hoverIndex !== null && scaledPoints[hoverIndex] && scaledPoints.length > 1 && (
<g pointerEvents='none' clipPath={`url(#clip-${label.replace(/\s+/g, '-')})`}>
<line
x1={scaledPoints[hoverIndex].x}
y1={padding.top}
x2={scaledPoints[hoverIndex].x}
y2={height - padding.bottom}
stroke={color}
strokeOpacity='0.35'
strokeDasharray='3 3'
/>
<circle
cx={scaledPoints[hoverIndex].x}
cy={scaledPoints[hoverIndex].y}
r='3'
fill={color}
/>
</g>
)}
{hoverIndex !== null &&
scaledPoints[hoverIndex] &&
scaledPoints.length > 1 &&
(() => {
const guideSeries =
getSeriesById(activeSeriesId) || getSeriesById(hoverSeriesId) || scaledSeries[0]
const active = guideSeries
const pt = active.pts[hoverIndex] || scaledPoints[hoverIndex]
return (
<g pointerEvents='none' clipPath={`url(#clip-${label.replace(/\s+/g, '-')})`}>
<line
x1={pt.x}
y1={padding.top}
x2={pt.x}
y2={height - padding.bottom}
stroke={active.color}
strokeOpacity='0.35'
strokeDasharray='3 3'
/>
{activeSeriesId &&
(() => {
const s = getSeriesById(activeSeriesId)
const spt = s?.pts?.[hoverIndex]
if (!s || !spt) return null
return <circle cx={spt.x} cy={spt.y} r='3' fill={s.color} />
})()}
</g>
)
})()}
{(() => {
if (data.length < 2) return null
const idx = [0, Math.floor(data.length / 2), data.length - 1]
const usableW = Math.max(1, chartWidth)
const firstTs = new Date(data[0].timestamp)
const lastTs = new Date(data[data.length - 1].timestamp)
const spanMs = Math.abs(lastTs.getTime() - firstTs.getTime())
const approxLabelWidth = 64
const desired = Math.min(8, Math.max(3, Math.floor(usableW / approxLabelWidth)))
const rawIdx = Array.from({ length: desired }, (_, i) =>
Math.round((i * (data.length - 1)) / Math.max(1, desired - 1))
)
const seen = new Set<number>()
const idx = rawIdx.filter((i) => {
if (seen.has(i)) return false
seen.add(i)
return true
})
const formatTick = (d: Date) => {
if (spanMs <= 36 * 60 * 60 * 1000) {
return d.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
}
if (spanMs <= 90 * 24 * 60 * 60 * 1000) {
return d.toLocaleString('en-US', { month: 'short', day: 'numeric' })
}
return d.toLocaleString('en-US', { month: 'short', year: 'numeric' })
}
return idx.map((i) => {
const x = padding.left + (i / (data.length - 1 || 1)) * chartWidth
const x = padding.left + (i / (data.length - 1 || 1)) * usableW
const tsSource = data[i]?.timestamp
if (!tsSource) return null
const ts = new Date(tsSource)
const labelStr = Number.isNaN(ts.getTime())
? ''
: ts.toLocaleString('en-US', { month: 'short', day: 'numeric' })
const labelStr = Number.isNaN(ts.getTime()) ? '' : formatTick(ts)
return (
<text
key={i}
key={`${label}-x-axis-${i}`}
x={x}
y={height - padding.bottom + 14}
fontSize='10'
fontSize='9'
textAnchor='middle'
fill='hsl(var(--muted-foreground))'
>
@@ -239,26 +461,41 @@ export function LineChart({
})
})()}
<text
x={padding.left - 10}
y={padding.top}
textAnchor='end'
fontSize='10'
fill='hsl(var(--muted-foreground))'
>
{maxValue}
{unit}
</text>
<text
x={padding.left - 10}
y={height - padding.bottom}
textAnchor='end'
fontSize='10'
fill='hsl(var(--muted-foreground))'
>
{minValue}
{unit}
</text>
{(() => {
const unitSuffix = (unit || '').trim()
const showInTicks = unitSuffix === '%'
const fmtCompact = (v: number) =>
new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
})
.format(v)
.toLowerCase()
return (
<>
<text
x={padding.left - 8}
y={padding.top}
textAnchor='end'
fontSize='9'
fill='hsl(var(--muted-foreground))'
>
{fmtCompact(maxValue)}
{showInTicks ? unit : ''}
</text>
<text
x={padding.left - 8}
y={height - padding.bottom}
textAnchor='end'
fontSize='9'
fill='hsl(var(--muted-foreground))'
>
{fmtCompact(minValue)}
{showInTicks ? unit : ''}
</text>
</>
)
})()}
<line
x1={padding.left}
@@ -270,32 +507,76 @@ export function LineChart({
/>
</svg>
{/* No end labels to keep the chart clean and avoid edge overlap */}
{hoverIndex !== null &&
scaledPoints[hoverIndex] &&
(() => {
const pt = scaledPoints[hoverIndex]
const val = data[hoverIndex]?.value
let formatted = ''
if (typeof val === 'number' && Number.isFinite(val)) {
const active =
getSeriesById(activeSeriesId) || getSeriesById(hoverSeriesId) || scaledSeries[0]
const pt = active.pts[hoverIndex] || scaledPoints[hoverIndex]
const toDisplay = activeSeriesId
? [getSeriesById(activeSeriesId)!]
: scaledSeries.length > 1
? scaledSeries.slice(1)
: [scaledSeries[0]]
const fmt = (v?: number) => {
if (typeof v !== 'number' || !Number.isFinite(v)) return '—'
const u = unit || ''
if (u.includes('%')) {
formatted = `${val.toFixed(1)}%`
} else if (u.toLowerCase().includes('ms')) {
formatted = `${Math.round(val)}ms`
} else if (u.toLowerCase().includes('exec')) {
formatted = `${Math.round(val)}${u}` // keep label like " execs"
} else {
formatted = `${Math.round(val)}${u}`
}
if (u.includes('%')) return `${v.toFixed(1)}%`
if (u.toLowerCase().includes('ms')) return `${Math.round(v)}ms`
if (u.toLowerCase().includes('exec')) return `${Math.round(v)}`
return `${Math.round(v)}${u}`
}
const left = Math.min(Math.max(pt.x + 8, padding.left), width - padding.right - 60)
const top = Math.min(Math.max(pt.y - 26, padding.top), height - padding.bottom - 18)
const longest = toDisplay.reduce((m, s) => {
const seriesIndex = allSeries.findIndex((x) => x.id === s.id)
const v = allSeries[seriesIndex]?.data?.[hoverIndex]?.value
const valueStr = fmt(v)
const labelStr = s.label || String(s.id || '')
const len = `${labelStr} ${valueStr}`.length
return Math.max(m, len)
}, 0)
const tooltipMaxW = Math.min(220, Math.max(80, 7 * longest + 24))
const anchorX = hoverPos?.x ?? pt.x
const margin = 10
const preferRight = anchorX + margin + tooltipMaxW <= width - padding.right
const left = preferRight
? Math.max(
padding.left,
Math.min(anchorX + margin, width - padding.right - tooltipMaxW)
)
: Math.max(
padding.left,
Math.min(anchorX - margin - tooltipMaxW, width - padding.right - tooltipMaxW)
)
const anchorY = hoverPos?.y ?? pt.y
const top = Math.min(
Math.max(anchorY - 26, padding.top),
height - padding.bottom - 18
)
return (
<div
className='pointer-events-none absolute rounded-md bg-background/80 px-2 py-1 font-medium text-[11px] shadow-sm ring-1 ring-border backdrop-blur'
style={{ left, top }}
>
{formatted}
{toDisplay.map((s) => {
const seriesIndex = allSeries.findIndex((x) => x.id === s.id)
const val = allSeries[seriesIndex]?.data?.[hoverIndex]?.value
return (
<div key={`tt-${s.id}`} className='flex items-center gap-1'>
<span
className='inline-block h-[6px] w-[6px] rounded-full'
style={{ backgroundColor: s.color }}
/>
<span style={{ color: 'hsl(var(--muted-foreground))' }}>
{s.label || s.id}
</span>
<span>{fmt(val)}</span>
</div>
)
})}
</div>
)
})()}

View File

@@ -1,5 +1,4 @@
import type React from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { memo, useMemo, useState } from 'react'
export interface StatusBarSegment {
successRate: number
@@ -15,6 +14,7 @@ export function StatusBar({
onSegmentClick,
workflowId,
segmentDurationMs,
preferBelow = false,
}: {
segments: StatusBarSegment[]
selectedSegmentIndices: number[] | null
@@ -26,117 +26,94 @@ export function StatusBar({
) => void
workflowId: string
segmentDurationMs: number
preferBelow?: boolean
}) {
const [hoverIndex, setHoverIndex] = useState<number | null>(null)
const labels = useMemo(() => {
return segments.map((segment) => {
const start = new Date(segment.timestamp)
const end = new Date(start.getTime() + (segmentDurationMs || 0))
const rangeLabel = Number.isNaN(start.getTime())
? ''
: `${start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric' })} ${end.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}`
return {
rangeLabel,
successLabel: `${segment.successRate.toFixed(1)}%`,
countsLabel: `${segment.successfulExecutions ?? 0}/${segment.totalExecutions ?? 0} succeeded`,
}
})
}, [segments, segmentDurationMs])
return (
<TooltipProvider delayDuration={0}>
<div className='flex select-none items-stretch gap-[2px]'>
<div className='relative'>
<div
className='flex select-none items-stretch gap-[2px]'
onMouseLeave={() => setHoverIndex(null)}
>
{segments.map((segment, i) => {
let color: string
let tooltipContent: React.ReactNode
const isSelected = Array.isArray(selectedSegmentIndices)
? selectedSegmentIndices.includes(i)
: false
let color: string
if (!segment.hasExecutions) {
color = 'bg-gray-300/60 dark:bg-gray-500/40'
} else if (segment.successRate === 100) {
color = 'bg-emerald-400/90'
} else if (segment.successRate >= 95) {
color = 'bg-amber-400/90'
} else {
if (segment.successRate === 100) {
color = 'bg-emerald-400/90'
} else if (segment.successRate >= 95) {
color = 'bg-amber-400/90'
} else {
color = 'bg-red-400/90'
}
const start = new Date(segment.timestamp)
const end = new Date(start.getTime() + (segmentDurationMs || 0))
const rangeLabel = Number.isNaN(start.getTime())
? ''
: `${start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric' })} ${end.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}`
tooltipContent = (
<div className='text-center'>
<div className='font-semibold'>{segment.successRate.toFixed(1)}%</div>
<div className='mt-1 text-xs'>
{segment.successfulExecutions ?? 0}/{segment.totalExecutions ?? 0} succeeded
</div>
{rangeLabel && (
<div className='mt-1 text-[11px] text-muted-foreground'>{rangeLabel}</div>
)}
</div>
)
}
// For empty segments: show a minimal tooltip with just the time range
if (!segment.hasExecutions) {
const start = new Date(segment.timestamp)
const end = new Date(start.getTime() + (segmentDurationMs || 0))
const rangeLabel = Number.isNaN(start.getTime())
? ''
: `${start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric' })} ${end.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}`
return (
<Tooltip key={i}>
<TooltipTrigger asChild>
<div
className={`h-6 flex-1 rounded-[3px] ${color} cursor-pointer transition-[opacity,transform] hover:opacity-90 ${
isSelected
? 'relative z-10 ring-2 ring-primary ring-offset-1'
: 'relative z-0'
}`}
aria-label={`Segment ${i + 1}`}
onClick={(e) => {
e.stopPropagation()
const mode = e.shiftKey
? 'range'
: e.metaKey || e.ctrlKey
? 'toggle'
: 'single'
onSegmentClick(workflowId, i, segment.timestamp, mode)
}}
onMouseDown={(e) => {
// Avoid selecting surrounding text when shift-clicking
e.preventDefault()
}}
/>
</TooltipTrigger>
<TooltipContent side='top' className='select-none px-3 py-2'>
{rangeLabel && (
<div className='text-[11px] text-muted-foreground'>{rangeLabel}</div>
)}
</TooltipContent>
</Tooltip>
)
color = 'bg-red-400/90'
}
return (
<Tooltip key={i}>
<TooltipTrigger asChild>
<div
className={`h-6 flex-1 rounded-[3px] ${color} cursor-pointer transition-[opacity,transform] hover:opacity-90 ${
isSelected ? 'relative z-10 ring-2 ring-primary ring-offset-1' : 'relative z-0'
}`}
aria-label={`Segment ${i + 1}`}
onMouseDown={(e) => {
// Avoid selecting surrounding text when shift-clicking
e.preventDefault()
}}
onClick={(e) => {
e.stopPropagation()
const mode = e.shiftKey ? 'range' : e.metaKey || e.ctrlKey ? 'toggle' : 'single'
onSegmentClick(workflowId, i, segment.timestamp, mode)
}}
/>
</TooltipTrigger>
<TooltipContent side='top' className='select-none px-3 py-2'>
{tooltipContent}
</TooltipContent>
</Tooltip>
<div
key={i}
className={`h-6 flex-1 rounded-[3px] ${color} cursor-pointer transition-[opacity,transform] hover:opacity-90 ${
isSelected ? 'relative z-10 ring-2 ring-primary ring-offset-1' : 'relative z-0'
}`}
aria-label={`Segment ${i + 1}`}
onMouseEnter={() => setHoverIndex(i)}
onMouseDown={(e) => {
e.preventDefault()
}}
onClick={(e) => {
e.stopPropagation()
const mode = e.shiftKey ? 'range' : e.metaKey || e.ctrlKey ? 'toggle' : 'single'
onSegmentClick(workflowId, i, segment.timestamp, mode)
}}
/>
)
})}
</div>
</TooltipProvider>
{hoverIndex !== null && segments[hoverIndex] && (
<div
className={`-translate-x-1/2 pointer-events-none absolute z-20 w-max whitespace-nowrap rounded-md bg-background/90 px-2 py-1 text-center text-[11px] shadow-sm ring-1 ring-border backdrop-blur ${
preferBelow ? '' : '-translate-y-full'
}`}
style={{
left: `${((hoverIndex + 0.5) / (segments.length || 1)) * 100}%`,
top: preferBelow ? '100%' : 0,
marginTop: preferBelow ? 8 : -8,
}}
>
{segments[hoverIndex].hasExecutions ? (
<div>
<div className='font-semibold'>{labels[hoverIndex].successLabel}</div>
<div className='text-muted-foreground'>{labels[hoverIndex].countsLabel}</div>
{labels[hoverIndex].rangeLabel && (
<div className='mt-0.5 text-muted-foreground'>{labels[hoverIndex].rangeLabel}</div>
)}
</div>
) : (
<div className='text-muted-foreground'>{labels[hoverIndex].rangeLabel}</div>
)}
</div>
)}
</div>
)
}
export default StatusBar
export default memo(StatusBar)

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Info, Loader2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { cn } from '@/lib/utils'
@@ -32,6 +32,9 @@ export interface ExecutionLogItem {
export interface WorkflowDetailsData {
errorRates: LineChartPoint[]
durations?: LineChartPoint[]
durationP50?: LineChartPoint[]
durationP90?: LineChartPoint[]
durationP99?: LineChartPoint[]
executionCounts: LineChartPoint[]
logs: ExecutionLogItem[]
allLogs: ExecutionLogItem[]
@@ -47,6 +50,9 @@ export function WorkflowDetails({
selectedSegment,
clearSegmentSelection,
formatCost,
onLoadMore,
hasMore,
isLoadingMore,
}: {
workspaceId: string
expandedWorkflowId: string
@@ -57,6 +63,9 @@ export function WorkflowDetails({
selectedSegment: { timestamp: string; totalExecutions: number } | null
clearSegmentSelection: () => void
formatCost: (n: number) => string
onLoadMore?: () => void
hasMore?: boolean
isLoadingMore?: boolean
}) {
const router = useRouter()
const { workflows } = useWorkflowRegistry()
@@ -65,9 +74,49 @@ export function WorkflowDetails({
[workflows, expandedWorkflowId]
)
const [expandedRowId, setExpandedRowId] = useState<string | null>(null)
const listRef = useRef<HTMLDivElement | null>(null)
const loaderRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const rootEl = listRef.current
const sentinel = loaderRef.current
if (!rootEl || !sentinel || !onLoadMore || !hasMore) return
let ticking = false
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0]
if (entry?.isIntersecting && hasMore && !ticking && !isLoadingMore) {
ticking = true
setTimeout(() => {
onLoadMore()
ticking = false
}, 50)
}
},
{ root: rootEl, threshold: 0.1, rootMargin: '200px 0px 0px 0px' }
)
observer.observe(sentinel)
return () => observer.disconnect()
}, [onLoadMore, hasMore, isLoadingMore])
// Fallback: if IntersectionObserver fails (older browsers), use scroll position
useEffect(() => {
const el = listRef.current
if (!el || !onLoadMore || !hasMore) return
const onScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el
const pct = (scrollTop / Math.max(1, scrollHeight - clientHeight)) * 100
if (pct > 80 && !isLoadingMore) onLoadMore()
}
el.addEventListener('scroll', onScroll)
return () => el.removeEventListener('scroll', onScroll)
}, [onLoadMore, hasMore, isLoadingMore])
return (
<div className='mt-5 overflow-hidden rounded-[11px] border bg-card shadow-sm'>
<div className='mt-1 overflow-hidden rounded-[11px] border bg-card shadow-sm'>
<div className='border-b bg-muted/30 px-4 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
@@ -79,7 +128,7 @@ export function WorkflowDetails({
className='h-[14px] w-[14px] flex-shrink-0 rounded'
style={{ backgroundColor: workflowColor }}
/>
<span className='font-semibold text-sm tracking-tight group-hover:text-primary'>
<span className='font-[480] text-sm tracking-tight group-hover:text-primary dark:font-[560]'>
{workflowName}
</span>
</button>
@@ -87,17 +136,15 @@ export function WorkflowDetails({
<div className='flex items-center gap-2'>
<div className='inline-flex h-7 items-center gap-2 rounded-[10px] border px-2.5'>
<span className='text-[11px] text-muted-foreground'>Executions</span>
<span className='font-semibold text-sm leading-none'>{overview.total}</span>
<span className='font-[500] text-sm leading-none'>{overview.total}</span>
</div>
<div className='inline-flex h-7 items-center gap-2 rounded-[10px] border px-2.5'>
<span className='text-[11px] text-muted-foreground'>Success</span>
<span className='font-semibold text-sm leading-none'>
{overview.rate.toFixed(1)}%
</span>
<span className='font-[500] text-sm leading-none'>{overview.rate.toFixed(1)}%</span>
</div>
<div className='inline-flex h-7 items-center gap-2 rounded-[10px] border px-2.5'>
<span className='text-[11px] text-muted-foreground'>Failures</span>
<span className='font-semibold text-sm leading-none'>{overview.failures}</span>
<span className='font-[500] text-sm leading-none'>{overview.failures}</span>
</div>
</div>
</div>
@@ -123,9 +170,9 @@ export function WorkflowDetails({
})
: 'Selected segment'
return (
<div className='mb-4 flex items-center justify-between rounded-lg border border-primary/30 bg-primary/10 px-4 py-2.5 text-foreground text-sm'>
<div className='mb-4 flex items-center justify-between rounded-[10px] border bg-muted/30 px-3 py-2 text-[13px] text-foreground'>
<div className='flex items-center gap-2'>
<div className='h-2 w-2 animate-pulse rounded-full bg-primary ring-2 ring-primary/40' />
<div className='h-1.5 w-1.5 rounded-full bg-primary ring-2 ring-primary/30' />
<span className='font-medium'>
Filtered to {tsLabel}
{selectedSegmentIndex.length > 1
@@ -137,7 +184,7 @@ export function WorkflowDetails({
</div>
<button
onClick={clearSegmentSelection}
className='rounded px-2 py-1 text-foreground text-xs hover:bg-primary/20 focus:outline-none focus:ring-2 focus:ring-primary/50'
className='rounded px-2 py-1 text-foreground text-xs hover:bg-muted focus:outline-none focus:ring-2 focus:ring-primary/40'
>
Clear filter
</button>
@@ -151,7 +198,7 @@ export function WorkflowDetails({
? 'md:grid-cols-2 xl:grid-cols-4'
: 'md:grid-cols-2 xl:grid-cols-3'
return (
<div className={`mb-4 grid grid-cols-1 gap-4 ${gridCols}`}>
<div className={`mb-3 grid grid-cols-1 gap-3 ${gridCols}`}>
<LineChart
data={details.errorRates}
label='Error Rate'
@@ -164,13 +211,42 @@ export function WorkflowDetails({
label='Workflow Duration'
color='#3b82f6'
unit='ms'
series={
[
details.durationP50
? {
id: 'p50',
label: 'p50',
color: '#60A5FA',
data: details.durationP50,
dashed: true,
}
: undefined,
details.durationP90
? {
id: 'p90',
label: 'p90',
color: '#3B82F6',
data: details.durationP90,
}
: undefined,
details.durationP99
? {
id: 'p99',
label: 'p99',
color: '#1D4ED8',
data: details.durationP99,
}
: undefined,
].filter(Boolean) as any
}
/>
)}
<LineChart
data={details.executionCounts}
label='Usage'
label='Executions'
color='#10b981'
unit=' execs'
unit='execs'
/>
{(() => {
const failures = details.errorRates.map((e, i) => ({
@@ -188,13 +264,13 @@ export function WorkflowDetails({
<div>
<div className='border-border border-b'>
<div className='grid min-w-[980px] grid-cols-[140px_90px_90px_90px_180px_1fr_100px] gap-2 px-2 pb-3 md:gap-3 lg:min-w-0 lg:gap-4'>
<div className='font-[480] font-sans text-[13px] text-muted-foreground leading-normal'>
<div className='font-[460] font-sans text-[13px] text-muted-foreground leading-normal'>
Time
</div>
<div className='font-[480] font-sans text-[13px] text-muted-foreground leading-normal'>
<div className='font-[460] font-sans text-[13px] text-muted-foreground leading-normal'>
Status
</div>
<div className='font-[480] font-sans text-[13px] text-muted-foreground leading-normal'>
<div className='font-[460] font-sans text-[13px] text-muted-foreground leading-normal'>
Trigger
</div>
<div className='font-[480] font-sans text-[13px] text-muted-foreground leading-normal'>
@@ -214,7 +290,7 @@ export function WorkflowDetails({
</div>
</div>
<div className='flex-1 overflow-auto' style={{ maxHeight: '400px' }}>
<div ref={listRef} className='flex-1 overflow-auto' style={{ maxHeight: '400px' }}>
<div className='pb-4'>
{(() => {
const logsToDisplay = details.logs
@@ -261,7 +337,7 @@ export function WorkflowDetails({
</span>
<span
style={{ marginLeft: '8px' }}
className='hidden font-medium sm:inline'
className='hidden font-[400] sm:inline'
>
{formattedDate.compactTime}
</span>
@@ -271,7 +347,7 @@ export function WorkflowDetails({
<div>
<div
className={cn(
'inline-flex items-center rounded-[8px] px-[6px] py-[2px] font-medium text-xs transition-all duration-200 lg:px-[8px]',
'inline-flex items-center rounded-[8px] px-[6px] py-[2px] font-[400] text-xs transition-all duration-200 lg:px-[8px]',
log.level === 'error'
? 'bg-red-500 text-white'
: 'bg-secondary text-card-foreground'
@@ -285,7 +361,7 @@ export function WorkflowDetails({
{log.trigger ? (
<div
className={cn(
'inline-flex items-center rounded-[8px] px-[6px] py-[2px] font-medium text-xs transition-all duration-200 lg:px-[8px]',
'inline-flex items-center rounded-[8px] px-[6px] py-[2px] font-[400] text-xs transition-all duration-200 lg:px-[8px]',
log.trigger.toLowerCase() === 'manual'
? 'bg-secondary text-card-foreground'
: 'text-white'
@@ -304,7 +380,7 @@ export function WorkflowDetails({
</div>
<div>
<div className='font-medium text-muted-foreground text-xs'>
<div className='font-[400] text-muted-foreground text-xs'>
{log.cost && log.cost.total > 0 ? formatCost(log.cost.total) : '—'}
</div>
</div>
@@ -361,6 +437,21 @@ export function WorkflowDetails({
)
})
})()}
{/* Bottom loading / sentinel */}
{hasMore && (
<div className='flex items-center justify-center py-3 text-muted-foreground'>
<div ref={loaderRef} className='flex items-center gap-2'>
{isLoadingMore ? (
<>
<Loader2 className='h-4 w-4 animate-spin' />
<span className='text-sm'>Loading more</span>
</>
) : (
<span className='text-sm'>Scroll to load more</span>
)}
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { memo, useMemo } from 'react'
import { ScrollArea } from '@/components/ui/scroll-area'
import StatusBar, {
type StatusBarSegment,
@@ -48,39 +48,7 @@ export function WorkflowsList({
return `${mins} minute${mins !== 1 ? 's' : ''}`
}, [segmentDurationMs])
const Axis = () => {
if (!filteredExecutions.length || !segmentsCount || !segmentDurationMs) return null
const firstTs = filteredExecutions[0]?.segments?.[0]?.timestamp
if (!firstTs) return null
const start = new Date(firstTs)
if (Number.isNaN(start.getTime())) return null
const totalMs = segmentsCount * segmentDurationMs
const end = new Date(start.getTime() + totalMs)
const midMs = start.getTime() + totalMs / 2
// Avoid duplicate labels by shifting mid tick slightly if it rounds identical to start/end
const mid = new Date(midMs + 60 * 1000)
const useDates = totalMs >= 24 * 60 * 60 * 1000
const fmt = (d: Date) => {
if (useDates) return d.toLocaleString('en-US', { month: 'short', day: 'numeric' })
return d.toLocaleString('en-US', { hour: 'numeric' })
}
return (
<div className='relative px-3 pt-2 pb-1'>
<div className='mr-[80px] ml-[224px]'>
<div className='relative h-4'>
<div className='-z-10 -translate-y-1/2 absolute inset-x-0 top-1/2 h-px bg-border' />
<div className='flex justify-between text-[10px] text-muted-foreground'>
<span>{fmt(start)}</span>
<span>{fmt(mid)}</span>
<span className='text-right'>{fmt(end)}</span>
</div>
</div>
</div>
</div>
)
}
// Date axis above the status bars intentionally removed for a cleaner, denser layout
function DynamicLegend() {
return (
@@ -92,12 +60,12 @@ export function WorkflowsList({
return (
<div
className='overflow-hidden rounded-lg border bg-card shadow-sm'
style={{ maxHeight: '380px', display: 'flex', flexDirection: 'column' }}
style={{ height: '380px', display: 'flex', flexDirection: 'column' }}
>
<div className='flex-shrink-0 border-b bg-muted/30 px-4 py-2.5'>
<div className='flex-shrink-0 border-b bg-muted/30 px-4 py-2'>
<div className='flex items-center justify-between'>
<div>
<h3 className='font-medium text-sm'>Workflows</h3>
<h3 className='font-[480] text-sm'>Workflows</h3>
<DynamicLegend />
</div>
<span className='text-muted-foreground text-xs'>
@@ -107,15 +75,15 @@ export function WorkflowsList({
</span>
</div>
</div>
<Axis />
<ScrollArea className='flex-1' style={{ height: 'calc(350px - 41px)' }}>
{/* Axis removed */}
<ScrollArea className='min-h-0 flex-1 overflow-auto'>
<div className='space-y-1 p-3'>
{filteredExecutions.length === 0 ? (
<div className='py-8 text-center text-muted-foreground text-sm'>
No workflows found matching "{searchQuery}"
</div>
) : (
filteredExecutions.map((workflow) => {
filteredExecutions.map((workflow, idx) => {
const isSelected = expandedWorkflowId === workflow.workflowId
return (
@@ -134,7 +102,9 @@ export function WorkflowsList({
backgroundColor: workflows[workflow.workflowId]?.color || '#64748b',
}}
/>
<h3 className='truncate font-medium text-sm'>{workflow.workflowName}</h3>
<h3 className='truncate font-[460] text-sm dark:font-medium'>
{workflow.workflowName}
</h3>
</div>
</div>
@@ -145,11 +115,12 @@ export function WorkflowsList({
onSegmentClick={onSegmentClick as any}
workflowId={workflow.workflowId}
segmentDurationMs={segmentDurationMs}
preferBelow={idx < 2}
/>
</div>
<div className='w-16 flex-shrink-0 text-right'>
<span className='font-medium text-muted-foreground text-sm'>
<span className='font-[460] text-muted-foreground text-sm'>
{workflow.overallSuccessRate.toFixed(1)}%
</span>
</div>
@@ -163,4 +134,4 @@ export function WorkflowsList({
)
}
export default WorkflowsList
export default memo(WorkflowsList)

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Check, ChevronDown } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
@@ -16,6 +16,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { createLogger } from '@/lib/logs/console/logger'
import {
commandListClass,
dropdownContentClass,
filterButtonClass,
folderDropdownListStyle,
} from '@/app/workspace/[workspaceId]/logs/components/filters/components/shared'
import { useFolderStore } from '@/stores/folders/store'
import { useFilterStore } from '@/stores/logs/filters/store'
@@ -29,6 +35,7 @@ interface FolderOption {
}
export default function FolderFilter() {
const triggerRef = useRef<HTMLButtonElement | null>(null)
const { folderIds, toggleFolderId, setFolderIds } = useFilterStore()
const { getFolderTree, getFolderPath, fetchFolders } = useFolderStore()
const params = useParams()
@@ -104,22 +111,21 @@ export default function FolderFilter() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<Button ref={triggerRef} variant='outline' size='sm' className={filterButtonClass}>
{loading ? 'Loading folders...' : getSelectedFoldersText()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-[200px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
side='bottom'
avoidCollisions={false}
sideOffset={4}
className={dropdownContentClass}
>
<Command>
<CommandInput placeholder='Search folders...' onValueChange={(v) => setSearch(v)} />
<CommandList>
<CommandList className={commandListClass} style={folderDropdownListStyle}>
<CommandEmpty>{loading ? 'Loading folders...' : 'No folders found.'}</CommandEmpty>
<CommandGroup>
<CommandItem

View File

@@ -0,0 +1,31 @@
export const filterButtonClass =
'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
export const dropdownContentClass =
'w-[200px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
export const commandListClass = 'overflow-y-auto overflow-x-hidden'
export const workflowDropdownListStyle = {
maxHeight: '14rem',
overflowY: 'auto',
overflowX: 'hidden',
} as const
export const folderDropdownListStyle = {
maxHeight: '10rem',
overflowY: 'auto',
overflowX: 'hidden',
} as const
export const triggerDropdownListStyle = {
maxHeight: '7.5rem',
overflowY: 'auto',
overflowX: 'hidden',
} as const
export const timelineDropdownListStyle = {
maxHeight: '9rem',
overflowY: 'auto',
overflowX: 'hidden',
} as const

View File

@@ -7,10 +7,20 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
commandListClass,
dropdownContentClass,
filterButtonClass,
timelineDropdownListStyle,
} from '@/app/workspace/[workspaceId]/logs/components/filters/components/shared'
import { useFilterStore } from '@/stores/logs/filters/store'
import type { TimeRange } from '@/stores/logs/filters/types'
export default function Timeline() {
type TimelineProps = {
variant?: 'default' | 'header'
}
export default function Timeline({ variant = 'default' }: TimelineProps = {}) {
const { timeRange, setTimeRange } = useFilterStore()
const specificTimeRanges: TimeRange[] = [
'Past 30 minutes',
@@ -27,47 +37,48 @@ export default function Timeline() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<Button variant='outline' size='sm' className={filterButtonClass}>
{timeRange}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align={variant === 'header' ? 'end' : 'start'}
side='bottom'
align='end'
sideOffset={6}
collisionPadding={8}
className='w-[220px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
avoidCollisions={false}
sideOffset={4}
className={dropdownContentClass}
>
<DropdownMenuItem
key='all'
onSelect={() => {
setTimeRange('All time')
}}
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
<div
className={`${commandListClass} py-1`}
style={variant === 'header' ? undefined : timelineDropdownListStyle}
>
<span>All time</span>
{timeRange === 'All time' && <Check className='h-4 w-4 text-muted-foreground' />}
</DropdownMenuItem>
<DropdownMenuSeparator />
{specificTimeRanges.map((range) => (
<DropdownMenuItem
key={range}
key='all'
onSelect={() => {
setTimeRange(range)
setTimeRange('All time')
}}
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<span>{range}</span>
{timeRange === range && <Check className='h-4 w-4 text-muted-foreground' />}
<span>All time</span>
{timeRange === 'All time' && <Check className='h-4 w-4 text-muted-foreground' />}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
{specificTimeRanges.map((range) => (
<DropdownMenuItem
key={range}
onSelect={() => {
setTimeRange(range)
}}
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<span>{range}</span>
{timeRange === range && <Check className='h-4 w-4 text-muted-foreground' />}
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
)

View File

@@ -1,17 +1,32 @@
import { useMemo, useRef, useState } from 'react'
import { Check, ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
commandListClass,
dropdownContentClass,
filterButtonClass,
triggerDropdownListStyle,
} from '@/app/workspace/[workspaceId]/logs/components/filters/components/shared'
import { useFilterStore } from '@/stores/logs/filters/store'
import type { TriggerType } from '@/stores/logs/filters/types'
export default function Trigger() {
const { triggers, toggleTrigger, setTriggers } = useFilterStore()
const [search, setSearch] = useState('')
const triggerRef = useRef<HTMLButtonElement | null>(null)
const triggerOptions: { value: TriggerType; label: string; color?: string }[] = [
{ value: 'manual', label: 'Manual', color: 'bg-gray-500' },
{ value: 'api', label: 'API', color: 'bg-blue-500' },
@@ -43,53 +58,60 @@ export default function Trigger() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<Button ref={triggerRef} variant='outline' size='sm' className={filterButtonClass}>
{getSelectedTriggersText()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
side='bottom'
avoidCollisions={false}
sideOffset={4}
className={dropdownContentClass}
>
<DropdownMenuItem
key='all'
onSelect={(e) => {
e.preventDefault()
clearSelections()
}}
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<span>All triggers</span>
{triggers.length === 0 && <Check className='h-4 w-4 text-muted-foreground' />}
</DropdownMenuItem>
<DropdownMenuSeparator />
{triggerOptions.map((triggerItem) => (
<DropdownMenuItem
key={triggerItem.value}
onSelect={(e) => {
e.preventDefault()
toggleTrigger(triggerItem.value)
}}
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
>
<div className='flex items-center'>
{triggerItem.color && (
<div className={`mr-2 h-2 w-2 rounded-full ${triggerItem.color}`} />
)}
{triggerItem.label}
</div>
{isTriggerSelected(triggerItem.value) && (
<Check className='h-4 w-4 text-muted-foreground' />
)}
</DropdownMenuItem>
))}
<Command>
<CommandInput placeholder='Search triggers...' onValueChange={(v) => setSearch(v)} />
<CommandList className={commandListClass} style={triggerDropdownListStyle}>
<CommandEmpty>No triggers found.</CommandEmpty>
<CommandGroup>
<CommandItem
value='all-triggers'
onSelect={() => clearSelections()}
className='cursor-pointer'
>
<span>All triggers</span>
{triggers.length === 0 && (
<Check className='ml-auto h-4 w-4 text-muted-foreground' />
)}
</CommandItem>
{useMemo(() => {
const q = search.trim().toLowerCase()
const filtered = q
? triggerOptions.filter((t) => t.label.toLowerCase().includes(q))
: triggerOptions
return filtered.map((triggerItem) => (
<CommandItem
key={triggerItem.value}
value={triggerItem.label}
onSelect={() => toggleTrigger(triggerItem.value)}
className='cursor-pointer'
>
<div className='flex items-center'>
{triggerItem.color && (
<div className={`mr-2 h-2 w-2 rounded-full ${triggerItem.color}`} />
)}
{triggerItem.label}
</div>
{isTriggerSelected(triggerItem.value) && (
<Check className='ml-auto h-4 w-4 text-muted-foreground' />
)}
</CommandItem>
))
}, [search, triggers])}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuContent>
</DropdownMenu>
)

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Check, ChevronDown } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
@@ -16,6 +16,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { createLogger } from '@/lib/logs/console/logger'
import {
commandListClass,
dropdownContentClass,
filterButtonClass,
workflowDropdownListStyle,
} from '@/app/workspace/[workspaceId]/logs/components/filters/components/shared'
import { useFilterStore } from '@/stores/logs/filters/store'
const logger = createLogger('LogsWorkflowFilter')
@@ -27,6 +33,7 @@ interface WorkflowOption {
}
export default function Workflow() {
const triggerRef = useRef<HTMLButtonElement | null>(null)
const { workflowIds, toggleWorkflowId, setWorkflowIds, folderIds } = useFilterStore()
const params = useParams()
const workspaceId = params?.workspaceId as string | undefined
@@ -84,22 +91,21 @@ export default function Workflow() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='sm'
className='w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
>
<Button ref={triggerRef} variant='outline' size='sm' className={filterButtonClass}>
{loading ? 'Loading workflows...' : getSelectedWorkflowsText()}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-[180px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
side='bottom'
avoidCollisions={false}
sideOffset={4}
className={dropdownContentClass}
>
<Command>
<CommandInput placeholder='Search workflows...' onValueChange={(v) => setSearch(v)} />
<CommandList>
<CommandList className={commandListClass} style={workflowDropdownListStyle}>
<CommandEmpty>{loading ? 'Loading workflows...' : 'No workflows found.'}</CommandEmpty>
<CommandGroup>
<CommandItem

View File

@@ -184,8 +184,8 @@ export function Sidebar({
hasPrev = false,
}: LogSidebarProps) {
const MIN_WIDTH = 400
const DEFAULT_WIDTH = 600
const EXPANDED_WIDTH = 800
const DEFAULT_WIDTH = 720
const EXPANDED_WIDTH = 900
const [width, setWidth] = useState(DEFAULT_WIDTH) // Start with default width
const [isDragging, setIsDragging] = useState(false)
@@ -325,7 +325,7 @@ export function Sidebar({
return (
<div
className={`fixed top-[148px] right-4 bottom-4 transform rounded-[14px] border bg-card shadow-xs ${
className={`fixed top-24 right-4 bottom-4 transform rounded-[14px] border bg-card shadow-xs ${
isOpen ? 'translate-x-0' : 'translate-x-[calc(100%+1rem)]'
} ${isDragging ? '' : 'transition-all duration-300 ease-in-out'} z-50 flex flex-col`}
style={{ width: `${width}px`, minWidth: `${MIN_WIDTH}px` }}
@@ -354,7 +354,7 @@ export function Sidebar({
<ChevronUp className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent side='bottom'>Previous log ()</TooltipContent>
<TooltipContent side='bottom'>Previous log</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -372,7 +372,7 @@ export function Sidebar({
<ChevronDown className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent side='bottom'>Next log ()</TooltipContent>
<TooltipContent side='bottom'>Next log</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -52,6 +52,7 @@ export function TraceSpanItem({
workflowStartTime,
onToggle,
expandedSpans,
hoveredPercent,
forwardHover,
gapBeforeMs = 0,
gapBeforePercent = 0,
@@ -609,6 +610,12 @@ export function TraceSpanItem({
)
})
})()}
{hoveredPercent != null && (
<div
className='pointer-events-none absolute top-[-10px] bottom-[-10px] w-px bg-black/30 dark:bg-white/45'
style={{ left: `${Math.max(0, Math.min(100, hoveredPercent))}%`, zIndex: 12 }}
/>
)}
<div className='absolute inset-x-0 inset-y-[-12px] cursor-crosshair' />
</div>
</div>

View File

@@ -275,20 +275,14 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
)
})}
{/* Global crosshair spanning all rows with visible time label */}
{/* Time label for hover (keep top label, row lines render per-row) */}
{hoveredPercent !== null && hoveredX !== null && (
<>
<div
className='pointer-events-none absolute inset-y-0 w-px bg-black/30 dark:bg-white/45'
style={{ left: hoveredX, zIndex: 20 }}
/>
<div
className='-translate-x-1/2 pointer-events-none absolute top-1 rounded bg-popover px-1.5 py-0.5 text-[10px] text-foreground shadow'
style={{ left: hoveredX, zIndex: 20 }}
>
{formatDurationDisplay(Math.max(0, (hoveredWorkflowMs || 0) - workflowStartTime))}
</div>
</>
<div
className='-translate-x-1/2 pointer-events-none absolute top-1 rounded bg-popover px-1.5 py-0.5 text-[10px] text-foreground shadow'
style={{ left: hoveredX, zIndex: 20 }}
>
{formatDurationDisplay(Math.max(0, (hoveredWorkflowMs || 0) - workflowStartTime))}
</div>
)}
{/* Hover capture area - aligned to timeline bars, not extending to edge */}

View File

@@ -363,6 +363,12 @@ export default function Logs() {
const fetchLogs = useCallback(async (pageNum: number, append = false) => {
try {
// Don't fetch if workspaceId is not set
const { workspaceId: storeWorkspaceId } = useFilterStore.getState()
if (!storeWorkspaceId) {
return
}
if (pageNum === 1) {
setLoading(true)
} else {
@@ -497,6 +503,11 @@ export default function Logs() {
return
}
// Don't fetch if workspaceId is not set yet
if (!workspaceId) {
return
}
setPage(1)
setHasMore(true)
@@ -613,7 +624,11 @@ export default function Logs() {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isFetchingMore) {
const e = entries[0]
if (!e?.isIntersecting) return
const { scrollTop, scrollHeight, clientHeight } = scrollContainer
const pct = (scrollTop / (scrollHeight - clientHeight)) * 100
if (pct > 70 && !isFetchingMore) {
loadMoreLogs()
}
},

View File

@@ -135,8 +135,6 @@ export function FieldFormat({
if (!inputEl) return
const current = localValues[field.id] ?? inputEl.value ?? ''
const trimmed = current.trim()
if (!trimmed) return
updateField(field.id, 'value', current)
}

View File

@@ -5,6 +5,7 @@ import { eq, sql } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import { checkServerSideUsageLimits } from '@/lib/billing'
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { processExecutionFiles } from '@/lib/execution/files'
import { IdempotencyService, webhookIdempotency } from '@/lib/idempotency'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
@@ -410,6 +411,53 @@ async function executeWebhookJobInternal(
}
}
// Process generic webhook files based on inputFormat
if (input && payload.provider === 'generic' && payload.blockId && blocks[payload.blockId]) {
try {
const triggerBlock = blocks[payload.blockId]
if (triggerBlock?.subBlocks?.inputFormat?.value) {
const inputFormat = triggerBlock.subBlocks.inputFormat.value as unknown as Array<{
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
}>
logger.debug(`[${requestId}] Processing generic webhook files from inputFormat`)
const fileFields = inputFormat.filter((field) => field.type === 'files')
if (fileFields.length > 0 && typeof input === 'object' && input !== null) {
const executionContext = {
workspaceId: workspaceId || '',
workflowId: payload.workflowId,
executionId,
}
for (const fileField of fileFields) {
const fieldValue = input[fileField.name]
if (fieldValue && typeof fieldValue === 'object') {
const uploadedFiles = await processExecutionFiles(
fieldValue,
executionContext,
requestId
)
if (uploadedFiles.length > 0) {
input[fileField.name] = uploadedFiles
logger.info(
`[${requestId}] Successfully processed ${uploadedFiles.length} file(s) for field: ${fileField.name}`
)
}
}
}
}
}
} catch (error) {
logger.error(`[${requestId}] Error processing generic webhook files:`, error)
// Continue without processing files rather than failing execution
}
}
// Create executor and execute
const executor = new Executor({
workflow: serializedWorkflow,

View File

@@ -28,6 +28,15 @@ export const GenericWebhookBlock: BlockConfig = {
triggerProvider: 'generic',
availableTriggers: ['generic_webhook'],
},
// Optional input format for structured data including files
{
id: 'inputFormat',
title: 'Input Format',
type: 'input-format',
layout: 'full',
description:
'Define the expected JSON input schema for this webhook (optional). Use type "files" for file uploads.',
},
],
tools: {

View File

@@ -0,0 +1,106 @@
import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger'
import { uploadExecutionFile } from '@/lib/workflows/execution-file-storage'
import type { UserFile } from '@/executor/types'
const logger = createLogger('ExecutionFiles')
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
/**
* Process a single file for workflow execution - handles both base64 ('file' type) and URL pass-through ('url' type)
*/
export async function processExecutionFile(
file: { type: string; data: string; name: string; mime?: string },
executionContext: { workspaceId: string; workflowId: string; executionId: string },
requestId: string,
isAsync?: boolean
): Promise<UserFile | null> {
if (file.type === 'file' && file.data && file.name) {
const dataUrlPrefix = 'data:'
const base64Prefix = ';base64,'
if (!file.data.startsWith(dataUrlPrefix)) {
logger.warn(`[${requestId}] Invalid data format for file: ${file.name}`)
return null
}
const base64Index = file.data.indexOf(base64Prefix)
if (base64Index === -1) {
logger.warn(`[${requestId}] Invalid data format (no base64 marker) for file: ${file.name}`)
return null
}
const mimeType = file.data.substring(dataUrlPrefix.length, base64Index)
const base64Data = file.data.substring(base64Index + base64Prefix.length)
const buffer = Buffer.from(base64Data, 'base64')
if (buffer.length > MAX_FILE_SIZE) {
const fileSizeMB = (buffer.length / (1024 * 1024)).toFixed(2)
throw new Error(
`File "${file.name}" exceeds the maximum size limit of 20MB (actual size: ${fileSizeMB}MB)`
)
}
logger.debug(`[${requestId}] Uploading file: ${file.name} (${buffer.length} bytes)`)
const userFile = await uploadExecutionFile(
executionContext,
buffer,
file.name,
mimeType || file.mime || 'application/octet-stream',
isAsync
)
logger.debug(`[${requestId}] Successfully uploaded ${file.name}`)
return userFile
}
if (file.type === 'url' && file.data) {
return {
id: uuidv4(),
url: file.data,
name: file.name,
size: 0,
type: file.mime || 'application/octet-stream',
key: `url/${file.name}`,
uploadedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
}
}
return null
}
/**
* Process all files for a given field in workflow execution input
*/
export async function processExecutionFiles(
fieldValue: any,
executionContext: { workspaceId: string; workflowId: string; executionId: string },
requestId: string,
isAsync?: boolean
): Promise<UserFile[]> {
if (!fieldValue || typeof fieldValue !== 'object') {
return []
}
const files = Array.isArray(fieldValue) ? fieldValue : [fieldValue]
const uploadedFiles: UserFile[] = []
const fullContext = { ...executionContext }
for (const file of files) {
try {
const userFile = await processExecutionFile(file, fullContext, requestId, isAsync)
if (userFile) {
uploadedFiles.push(userFile)
}
} catch (error) {
logger.error(`[${requestId}] Failed to process file ${file.name}:`, error)
throw new Error(`Failed to upload file: ${file.name}`)
}
}
return uploadedFiles
}

View File

@@ -87,9 +87,17 @@ export function getBlockOutputs(
}
if (Array.isArray(inputFormatValue)) {
// For API and Input triggers, only use inputFormat fields
if (blockType === 'api_trigger' || blockType === 'input_trigger') {
outputs = {} // Clear all default outputs
// For API, Input triggers, and Generic Webhook, use inputFormat fields
if (
blockType === 'api_trigger' ||
blockType === 'input_trigger' ||
blockType === 'generic_webhook'
) {
// For generic_webhook, only clear outputs if inputFormat has fields
// Otherwise keep the default outputs (pass-through body)
if (inputFormatValue.length > 0 || blockType !== 'generic_webhook') {
outputs = {} // Clear all default outputs
}
// Add each field from inputFormat as an output at root level
inputFormatValue.forEach((field: { name?: string; type?: string }) => {

View File

@@ -36,7 +36,8 @@ export async function uploadExecutionFile(
context: ExecutionContext,
fileBuffer: Buffer,
fileName: string,
contentType: string
contentType: string,
isAsync?: boolean
): Promise<UserFile> {
logger.info(`Uploading execution file: ${fileName} for execution ${context.executionId}`)
logger.debug(`File upload context:`, {
@@ -53,6 +54,9 @@ export async function uploadExecutionFile(
logger.info(`Generated storage key: "${storageKey}" for file: ${fileName}`)
// Use 10-minute expiration for async executions, 5 minutes for sync
const urlExpirationSeconds = isAsync ? 10 * 60 : 5 * 60
try {
let fileInfo: any
let directUrl: string | undefined
@@ -78,16 +82,18 @@ export async function uploadExecutionFile(
logger.info(`Original storage key was: "${storageKey}"`)
logger.info(`Keys match: ${fileInfo.key === storageKey}`)
// Generate presigned URL for execution (5 minutes)
// Generate presigned URL for execution (5 or 10 minutes)
try {
logger.info(`Generating presigned URL with key: "${fileInfo.key}"`)
logger.info(
`Generating presigned URL with key: "${fileInfo.key}" (expiration: ${urlExpirationSeconds / 60} minutes)`
)
directUrl = await getPresignedUrlWithConfig(
fileInfo.key, // Use the actual uploaded key
{
bucket: S3_EXECUTION_FILES_CONFIG.bucket,
region: S3_EXECUTION_FILES_CONFIG.region,
},
5 * 60 // 5 minutes
urlExpirationSeconds
)
logger.info(`Generated presigned URL: ${directUrl}`)
} catch (error) {
@@ -102,7 +108,7 @@ export async function uploadExecutionFile(
containerName: BLOB_EXECUTION_FILES_CONFIG.containerName,
})
// Generate presigned URL for execution (5 minutes)
// Generate presigned URL for execution (5 or 10 minutes)
try {
directUrl = await getBlobPresignedUrlWithConfig(
fileInfo.key, // Use the actual uploaded key
@@ -112,7 +118,7 @@ export async function uploadExecutionFile(
connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString,
containerName: BLOB_EXECUTION_FILES_CONFIG.containerName,
},
5 * 60 // 5 minutes
urlExpirationSeconds
)
} catch (error) {
logger.warn(`Failed to generate Blob presigned URL for ${fileName}:`, error)
@@ -126,7 +132,7 @@ export async function uploadExecutionFile(
name: fileName,
size: fileBuffer.length,
type: contentType,
url: directUrl || `/api/files/serve/${fileInfo.key}`, // Use 5-minute presigned URL, fallback to serve path
url: directUrl || `/api/files/serve/${fileInfo.key}`, // Use presigned URL (5 or 10 min), fallback to serve path
key: fileInfo.key, // Use the actual uploaded key from S3/Blob
uploadedAt: new Date().toISOString(),
expiresAt: getFileExpirationDate(),

View File

@@ -16,18 +16,26 @@ export interface StreamingConfig {
export interface StreamingResponseOptions {
requestId: string
workflow: { id: string; userId: string; isDeployed?: boolean }
workflow: { id: string; userId: string; workspaceId?: string | null; isDeployed?: boolean }
input: any
executingUserId: string
streamConfig: StreamingConfig
createFilteredResult: (result: ExecutionResult) => any
executionId?: string
}
export async function createStreamingResponse(
options: StreamingResponseOptions
): Promise<ReadableStream> {
const { requestId, workflow, input, executingUserId, streamConfig, createFilteredResult } =
options
const {
requestId,
workflow,
input,
executingUserId,
streamConfig,
createFilteredResult,
executionId,
} = options
const { executeWorkflow, createFilteredResult: defaultFilteredResult } = await import(
'@/app/api/workflows/[id]/execute/route'
@@ -115,15 +123,22 @@ export async function createStreamingResponse(
}
}
const result = await executeWorkflow(workflow, requestId, input, executingUserId, {
enabled: true,
selectedOutputs: streamConfig.selectedOutputs,
isSecureMode: streamConfig.isSecureMode,
workflowTriggerType: streamConfig.workflowTriggerType,
onStream: onStreamCallback,
onBlockComplete: onBlockCompleteCallback,
skipLoggingComplete: true, // We'll complete logging after tokenization
})
const result = await executeWorkflow(
workflow,
requestId,
input,
executingUserId,
{
enabled: true,
selectedOutputs: streamConfig.selectedOutputs,
isSecureMode: streamConfig.isSecureMode,
workflowTriggerType: streamConfig.workflowTriggerType,
onStream: onStreamCallback,
onBlockComplete: onBlockCompleteCallback,
skipLoggingComplete: true, // We'll complete logging after tokenization
},
executionId
)
if (result.logs && streamedContent.size > 0) {
result.logs = result.logs.map((log: any) => {

View File

@@ -206,6 +206,23 @@ function sanitizeToolsForComparison(tools: any[] | undefined): any[] {
})
}
/**
* Sanitize inputFormat array by removing test-only value fields
* @param inputFormat - The inputFormat array to sanitize
* @returns A sanitized inputFormat array without test values
*/
function sanitizeInputFormatForComparison(inputFormat: any[] | undefined): any[] {
if (!Array.isArray(inputFormat)) {
return []
}
return inputFormat.map((field) => {
// Remove test-only field: value (used only for manual testing)
const { value, collapsed, ...cleanField } = field
return cleanField
})
}
/**
* Normalize a value for consistent comparison by sorting object keys
* @param value - The value to normalize
@@ -363,6 +380,16 @@ export function hasWorkflowChanged(
deployedValue = sanitizeToolsForComparison(deployedValue)
}
// Special handling for 'inputFormat' subBlock - sanitize UI-only fields (collapsed state)
if (
subBlockId === 'inputFormat' &&
Array.isArray(currentValue) &&
Array.isArray(deployedValue)
) {
currentValue = sanitizeInputFormatForComparison(currentValue)
deployedValue = sanitizeInputFormatForComparison(deployedValue)
}
// For string values, compare directly to catch even small text changes
if (typeof currentValue === 'string' && typeof deployedValue === 'string') {
if (currentValue !== deployedValue) {

View File

@@ -15,10 +15,12 @@ const updateURL = (params: URLSearchParams) => {
window.history.replaceState({}, '', url)
}
const DEFAULT_TIME_RANGE: TimeRange = 'Past 12 hours'
const DEFAULT_TIME_RANGE: TimeRange = 'All time'
const parseTimeRangeFromURL = (value: string | null): TimeRange => {
switch (value) {
case 'all-time':
return 'All time'
case 'past-30-minutes':
return 'Past 30 minutes'
case 'past-hour':