mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-13 08:57:55 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da091dfe8a | ||
|
|
29c7827d6f | ||
|
|
22f9d6e2df | ||
|
|
0cb615428d | ||
|
|
74576ec921 | ||
|
|
67e681dd7c | ||
|
|
4b05da31e0 | ||
|
|
82fa4e8bbb | ||
|
|
4cd790b200 | ||
|
|
b7e0b42d48 | ||
|
|
c6ef5785c8 |
34
.gitattributes
vendored
Normal file
34
.gitattributes
vendored
Normal 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
|
||||
12
.github/workflows/i18n.yml
vendored
12
.github/workflows/i18n.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 failed → Retry 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 Fehler → Wiederholen 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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
242
apps/docs/content/docs/de/tools/zep.mdx
Normal file
242
apps/docs/content/docs/de/tools/zep.mdx
Normal 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`
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>`}
|
||||
/>
|
||||
|
||||
@@ -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álido → Solicitar 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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
242
apps/docs/content/docs/es/tools/zep.mdx
Normal file
242
apps/docs/content/docs/es/tools/zep.mdx
Normal 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`
|
||||
@@ -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> : vé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
|
||||
### Dé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>`
|
||||
- Ré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
|
||||
- **Dé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")
|
||||
**Dé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 invalide → Demander 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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
242
apps/docs/content/docs/fr/tools/zep.mdx
Normal file
242
apps/docs/content/docs/fr/tools/zep.mdx
Normal 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`
|
||||
@@ -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-4o、Claude 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番号、国民保険番号
|
||||
- **スペイン**: NIF、NIE、CIF
|
||||
- **イタリア**: 納税者番号、運転免許証、VAT番号
|
||||
- **ポーランド**: PESEL、NIP、REGON
|
||||
- **シンガポール**: NRIC/FIN、UEN
|
||||
- **オーストラリア**: ABN、ACN、TFN、メディケア
|
||||
- **インド**: Aadhaar、PAN、パスポート、有権者番号
|
||||
- **モード**:
|
||||
- **検出**: 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マスキング
|
||||
- GDPR、HIPAAなどのプライバシー規制への準拠
|
||||
- 処理前のユーザー入力のサニタイズ
|
||||
|
||||
## 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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 Events(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';
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
242
apps/docs/content/docs/ja/tools/zep.mdx
Normal file
242
apps/docs/content/docs/ja/tools/zep.mdx
Normal 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`
|
||||
@@ -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-4o、Claude 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 编号、国家保险号码
|
||||
- **西班牙**:NIF、NIE、CIF
|
||||
- **意大利**:税号、驾驶执照、增值税号
|
||||
- **波兰**:PESEL、NIP、REGON
|
||||
- **新加坡**:NRIC/FIN、UEN
|
||||
- **澳大利亚**:ABN、ACN、TFN、Medicare
|
||||
- **印度**:Aadhaar、PAN、护照、选民编号
|
||||
- **模式:**
|
||||
- **检测**:仅识别 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 进行掩码
|
||||
- 符合 GDPR、HIPAA 和其他隐私法规
|
||||
- 在处理之前清理用户输入
|
||||
|
||||
## 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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 项目)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
242
apps/docs/content/docs/zh/tools/zep.mdx
Normal file
242
apps/docs/content/docs/zh/tools/zep.mdx
Normal 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`
|
||||
@@ -11,8 +11,7 @@
|
||||
"content/docs/[locale]/*.mdx",
|
||||
"content/docs/[locale]/*/*.mdx",
|
||||
"content/docs/[locale]/*/*/*.mdx"
|
||||
],
|
||||
"exclude": ["content/docs/[locale]/sdks/*.mdx"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
174
apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts
Normal file
174
apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
106
apps/sim/lib/execution/files.ts
Normal file
106
apps/sim/lib/execution/files.ts
Normal 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
|
||||
}
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user