mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
v0.5.41: memory fixes, copilot improvements, knowledgebase improvements, LLM providers standardization
This commit is contained in:
@@ -258,8 +258,7 @@ export async function generateMetadata(props: {
|
||||
const baseUrl = 'https://docs.sim.ai'
|
||||
const fullUrl = `${baseUrl}${page.url}`
|
||||
|
||||
const description = data.description || ''
|
||||
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}&category=DOCUMENTATION${description ? `&description=${encodeURIComponent(description)}` : ''}`
|
||||
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}`
|
||||
|
||||
return {
|
||||
title: data.title,
|
||||
|
||||
@@ -39,12 +39,10 @@ async function loadGoogleFont(font: string, weights: string, text: string): Prom
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const title = searchParams.get('title') || 'Documentation'
|
||||
const category = searchParams.get('category') || 'DOCUMENTATION'
|
||||
const description = searchParams.get('description') || ''
|
||||
|
||||
const baseUrl = new URL(request.url).origin
|
||||
|
||||
const allText = `${title}${category}${description}docs.sim.ai`
|
||||
const allText = `${title}docs.sim.ai`
|
||||
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)
|
||||
|
||||
return new ImageResponse(
|
||||
@@ -59,7 +57,7 @@ export async function GET(request: NextRequest) {
|
||||
fontFamily: 'Geist',
|
||||
}}
|
||||
>
|
||||
{/* Base gradient layer - very subtle purple tint across the entire image */}
|
||||
{/* Base gradient layer - subtle purple tint across the entire image */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -114,56 +112,25 @@ export async function GET(request: NextRequest) {
|
||||
{/* Logo */}
|
||||
<img src={`${baseUrl}/static/logo.png`} alt='sim' height={32} />
|
||||
|
||||
{/* Category + Title + Description */}
|
||||
<div
|
||||
{/* Title */}
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
fontSize: getTitleFontSize(title),
|
||||
fontWeight: 600,
|
||||
color: '#ffffff',
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: '#802fff',
|
||||
letterSpacing: '0.02em',
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: getTitleFontSize(title),
|
||||
fontWeight: 600,
|
||||
color: '#ffffff',
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{description && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 18,
|
||||
fontWeight: 400,
|
||||
color: '#a1a1aa',
|
||||
lineHeight: 1.4,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{description.length > 100 ? `${description.slice(0, 100)}...` : description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
{/* Footer */}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
color: '#52525b',
|
||||
color: '#71717a',
|
||||
}}
|
||||
>
|
||||
docs.sim.ai
|
||||
|
||||
@@ -58,7 +58,7 @@ export const metadata = {
|
||||
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation&category=DOCUMENTATION',
|
||||
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Sim Documentation',
|
||||
@@ -72,7 +72,7 @@ export const metadata = {
|
||||
'Comprehensive documentation for Sim - the visual workflow builder for AI applications.',
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation&category=DOCUMENTATION'],
|
||||
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
|
||||
@@ -49,40 +49,40 @@ Die Modellaufschlüsselung zeigt:
|
||||
|
||||
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
|
||||
<Tab>
|
||||
**Gehostete Modelle** - Sim stellt API-Schlüssel mit einem 2,5-fachen Preismultiplikator bereit:
|
||||
**Gehostete Modelle** - Sim stellt API-Schlüssel mit einem 2-fachen Preismultiplikator bereit:
|
||||
|
||||
**OpenAI**
|
||||
| Modell | Basispreis (Eingabe/Ausgabe) | Gehosteter Preis (Eingabe/Ausgabe) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-5.1 | $1,25 / $10,00 | $3,13 / $25,00 |
|
||||
| GPT-5 | $1,25 / $10,00 | $3,13 / $25,00 |
|
||||
| GPT-5 Mini | $0,25 / $2,00 | $0,63 / $5,00 |
|
||||
| GPT-5 Nano | $0,05 / $0,40 | $0,13 / $1,00 |
|
||||
| GPT-4o | $2,50 / $10,00 | $6,25 / $25,00 |
|
||||
| GPT-4.1 | $2,00 / $8,00 | $5,00 / $20,00 |
|
||||
| GPT-4.1 Mini | $0,40 / $1,60 | $1,00 / $4,00 |
|
||||
| GPT-4.1 Nano | $0,10 / $0,40 | $0,25 / $1,00 |
|
||||
| o1 | $15,00 / $60,00 | $37,50 / $150,00 |
|
||||
| o3 | $2,00 / $8,00 | $5,00 / $20,00 |
|
||||
| o4 Mini | $1,10 / $4,40 | $2,75 / $11,00 |
|
||||
| GPT-5.1 | 1,25 $ / 10,00 $ | 2,50 $ / 20,00 $ |
|
||||
| GPT-5 | 1,25 $ / 10,00 $ | 2,50 $ / 20,00 $ |
|
||||
| GPT-5 Mini | 0,25 $ / 2,00 $ | 0,50 $ / 4,00 $ |
|
||||
| GPT-5 Nano | 0,05 $ / 0,40 $ | 0,10 $ / 0,80 $ |
|
||||
| GPT-4o | 2,50 $ / 10,00 $ | 5,00 $ / 20,00 $ |
|
||||
| GPT-4.1 | 2,00 $ / 8,00 $ | 4,00 $ / 16,00 $ |
|
||||
| GPT-4.1 Mini | 0,40 $ / 1,60 $ | 0,80 $ / 3,20 $ |
|
||||
| GPT-4.1 Nano | 0,10 $ / 0,40 $ | 0,20 $ / 0,80 $ |
|
||||
| o1 | 15,00 $ / 60,00 $ | 30,00 $ / 120,00 $ |
|
||||
| o3 | 2,00 $ / 8,00 $ | 4,00 $ / 16,00 $ |
|
||||
| o4 Mini | 1,10 $ / 4,40 $ | 2,20 $ / 8,80 $ |
|
||||
|
||||
**Anthropic**
|
||||
| Modell | Basispreis (Eingabe/Ausgabe) | Gehosteter Preis (Eingabe/Ausgabe) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Claude Opus 4.5 | $5,00 / $25,00 | $12,50 / $62,50 |
|
||||
| Claude Opus 4.1 | $15,00 / $75,00 | $37,50 / $187,50 |
|
||||
| Claude Sonnet 4.5 | $3,00 / $15,00 | $7,50 / $37,50 |
|
||||
| Claude Sonnet 4.0 | $3,00 / $15,00 | $7,50 / $37,50 |
|
||||
| Claude Haiku 4.5 | $1,00 / $5,00 | $2,50 / $12,50 |
|
||||
| Claude Opus 4.5 | 5,00 $ / 25,00 $ | 10,00 $ / 50,00 $ |
|
||||
| Claude Opus 4.1 | 15,00 $ / 75,00 $ | 30,00 $ / 150,00 $ |
|
||||
| Claude Sonnet 4.5 | 3,00 $ / 15,00 $ | 6,00 $ / 30,00 $ |
|
||||
| Claude Sonnet 4.0 | 3,00 $ / 15,00 $ | 6,00 $ / 30,00 $ |
|
||||
| Claude Haiku 4.5 | 1,00 $ / 5,00 $ | 2,00 $ / 10,00 $ |
|
||||
|
||||
**Google**
|
||||
| Modell | Basispreis (Eingabe/Ausgabe) | Gehosteter Preis (Eingabe/Ausgabe) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Gemini 3 Pro Preview | $2,00 / $12,00 | $5,00 / $30,00 |
|
||||
| Gemini 2.5 Pro | $0,15 / $0,60 | $0,38 / $1,50 |
|
||||
| Gemini 2.5 Flash | $0,15 / $0,60 | $0,38 / $1,50 |
|
||||
| Gemini 3 Pro Preview | 2,00 $ / 12,00 $ | 4,00 $ / 24,00 $ |
|
||||
| Gemini 2.5 Pro | 1,25 $ / 10,00 $ | 2,50 $ / 20,00 $ |
|
||||
| Gemini 2.5 Flash | 0,30 $ / 2,50 $ | 0,60 $ / 5,00 $ |
|
||||
|
||||
*Der 2,5-fache Multiplikator deckt Infrastruktur- und API-Verwaltungskosten ab.*
|
||||
*Der 2x-Multiplikator deckt Infrastruktur- und API-Verwaltungskosten ab.*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
@@ -185,11 +185,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
||||
|
||||
Verschiedene Abonnementpläne haben unterschiedliche Nutzungslimits:
|
||||
|
||||
| Plan | Monatliches Nutzungslimit | Rate-Limits (pro Minute) |
|
||||
| Plan | Monatliches Nutzungslimit | Ratenlimits (pro Minute) |
|
||||
|------|-------------------|-------------------------|
|
||||
| **Free** | $10 | 5 sync, 10 async |
|
||||
| **Pro** | $100 | 10 sync, 50 async |
|
||||
| **Team** | $500 (gepoolt) | 50 sync, 100 async |
|
||||
| **Free** | 20 $ | 5 synchron, 10 asynchron |
|
||||
| **Pro** | 100 $ | 10 synchron, 50 asynchron |
|
||||
| **Team** | 500 $ (gepoolt) | 50 synchron, 100 asynchron |
|
||||
| **Enterprise** | Individuell | Individuell |
|
||||
|
||||
## Abrechnungsmodell
|
||||
|
||||
@@ -35,81 +35,87 @@ Sobald Ihre Dokumente verarbeitet sind, können Sie die einzelnen Chunks anzeige
|
||||
<Image src="/static/knowledgebase/knowledgebase.png" alt="Dokumentchunk-Ansicht mit verarbeiteten Inhalten" width={800} height={500} />
|
||||
|
||||
### Chunk-Konfiguration
|
||||
- **Standardgröße der Chunks**: 1.024 Zeichen
|
||||
- **Konfigurierbarer Bereich**: 100-4.000 Zeichen pro Chunk
|
||||
- **Intelligente Überlappung**: Standardmäßig 200 Zeichen zur Kontexterhaltung
|
||||
- **Hierarchische Aufteilung**: Respektiert Dokumentstruktur (Abschnitte, Absätze, Sätze)
|
||||
|
||||
### Bearbeitungsfunktionen
|
||||
Beim Erstellen einer Wissensdatenbank können Sie konfigurieren, wie Dokumente in Chunks aufgeteilt werden:
|
||||
|
||||
| Einstellung | Einheit | Standard | Bereich | Beschreibung |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **Maximale Chunk-Größe** | Tokens | 1.024 | 100-4.000 | Maximale Größe jedes Chunks (1 Token ≈ 4 Zeichen) |
|
||||
| **Minimale Chunk-Größe** | Zeichen | 1 | 1-2.000 | Minimale Chunk-Größe, um winzige Fragmente zu vermeiden |
|
||||
| **Überlappung** | Zeichen | 200 | 0-500 | Kontextüberlappung zwischen aufeinanderfolgenden Chunks |
|
||||
|
||||
- **Hierarchische Aufteilung**: Berücksichtigt die Dokumentstruktur (Abschnitte, Absätze, Sätze)
|
||||
|
||||
### Bearbeitungsmöglichkeiten
|
||||
- **Chunk-Inhalt bearbeiten**: Textinhalt einzelner Chunks ändern
|
||||
- **Chunk-Grenzen anpassen**: Chunks bei Bedarf zusammenführen oder teilen
|
||||
- **Chunk-Grenzen anpassen**: Chunks nach Bedarf zusammenführen oder aufteilen
|
||||
- **Metadaten hinzufügen**: Chunks mit zusätzlichem Kontext anreichern
|
||||
- **Massenoperationen**: Effiziente Verwaltung mehrerer Chunks
|
||||
- **Massenoperationen**: Mehrere Chunks effizient verwalten
|
||||
|
||||
## Erweiterte PDF-Verarbeitung
|
||||
|
||||
Für PDF-Dokumente bietet Sim erweiterte Verarbeitungsfunktionen:
|
||||
|
||||
### OCR-Unterstützung
|
||||
Bei Konfiguration mit Azure oder [Mistral OCR](https://docs.mistral.ai/ocr/):
|
||||
Wenn mit Azure oder [Mistral OCR](https://docs.mistral.ai/ocr/) konfiguriert:
|
||||
- **Verarbeitung gescannter Dokumente**: Text aus bildbasierten PDFs extrahieren
|
||||
- **Umgang mit gemischten Inhalten**: Verarbeitung von PDFs mit Text und Bildern
|
||||
- **Verarbeitung gemischter Inhalte**: PDFs mit Text und Bildern verarbeiten
|
||||
- **Hohe Genauigkeit**: Fortschrittliche KI-Modelle gewährleisten präzise Textextraktion
|
||||
|
||||
## Verwendung des Wissensblocks in Workflows
|
||||
## Verwendung des Knowledge-Blocks in Workflows
|
||||
|
||||
Sobald Ihre Dokumente verarbeitet sind, können Sie sie in Ihren KI-Workflows über den Wissensblock nutzen. Dies ermöglicht Retrieval-Augmented Generation (RAG), wodurch Ihre KI-Agenten auf Ihre Dokumentinhalte zugreifen und darüber nachdenken können, um genauere, kontextbezogene Antworten zu liefern.
|
||||
Sobald Ihre Dokumente verarbeitet sind, können Sie sie in Ihren KI-Workflows über den Knowledge-Block verwenden. Dies ermöglicht Retrieval-Augmented Generation (RAG), wodurch Ihre KI-Agenten auf Ihre Dokumentinhalte zugreifen und darüber nachdenken können, um genauere, kontextbezogene Antworten zu liefern.
|
||||
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="Verwendung des Wissensblocks in Workflows" width={800} height={500} />
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="Verwendung des Knowledge-Blocks in Workflows" width={800} height={500} />
|
||||
|
||||
### Funktionen des Wissensblocks
|
||||
- **Semantische Suche**: Relevante Inhalte mit natürlichsprachlichen Abfragen finden
|
||||
- **Kontextintegration**: Automatisches Einbinden relevanter Chunks in Agenten-Prompts
|
||||
- **Dynamischer Abruf**: Suche erfolgt in Echtzeit während der Workflow-Ausführung
|
||||
- **Relevanzbewertung**: Ergebnisse nach semantischer Ähnlichkeit geordnet
|
||||
### Knowledge-Block-Funktionen
|
||||
- **Semantische Suche**: Relevante Inhalte mithilfe natürlichsprachlicher Abfragen finden
|
||||
- **Kontextintegration**: Relevante Chunks automatisch in Agenten-Prompts einbinden
|
||||
- **Dynamisches Abrufen**: Suche erfolgt in Echtzeit während der Workflow-Ausführung
|
||||
- **Relevanz-Bewertung**: Ergebnisse nach semantischer Ähnlichkeit sortiert
|
||||
|
||||
### Integrationsoptionen
|
||||
- **System-Prompts**: Kontext für Ihre KI-Agenten bereitstellen
|
||||
- **Dynamischer Kontext**: Suche und Einbindung relevanter Informationen während Gesprächen
|
||||
- **Dokumentübergreifende Suche**: Abfrage über Ihre gesamte Wissensdatenbank
|
||||
- **Gefilterte Suche**: Kombination mit Tags für präzisen Inhaltsabruf
|
||||
- **System-Prompts**: Stellen Sie Ihren KI-Agenten Kontext bereit
|
||||
- **Dynamischer Kontext**: Suchen und fügen Sie relevante Informationen während Konversationen hinzu
|
||||
- **Multi-Dokument-Suche**: Durchsuchen Sie Ihre gesamte Wissensdatenbank
|
||||
- **Gefilterte Suche**: Kombinieren Sie mit Tags für präzises Abrufen von Inhalten
|
||||
|
||||
## Vektorsuchtechnologie
|
||||
## Vektor-Suchtechnologie
|
||||
|
||||
Sim verwendet Vektorsuche, die von [pgvector](https://github.com/pgvector/pgvector) unterstützt wird, um die Bedeutung und den Kontext Ihrer Inhalte zu verstehen:
|
||||
|
||||
### Semantisches Verständnis
|
||||
- **Kontextuelle Suche**: Findet relevante Inhalte, auch wenn exakte Schlüsselwörter nicht übereinstimmen
|
||||
- **Konzeptbasierte Abfrage**: Versteht Beziehungen zwischen Ideen
|
||||
- **Konzeptbasiertes Abrufen**: Versteht Beziehungen zwischen Ideen
|
||||
- **Mehrsprachige Unterstützung**: Funktioniert über verschiedene Sprachen hinweg
|
||||
- **Synonymerkennung**: Findet verwandte Begriffe und Konzepte
|
||||
|
||||
### Suchfunktionen
|
||||
- **Natürlichsprachige Abfragen**: Stellen Sie Fragen in natürlicher Sprache
|
||||
- **Natürlichsprachige Abfragen**: Stellen Sie Fragen in einfachem Deutsch
|
||||
- **Ähnlichkeitssuche**: Finden Sie konzeptionell ähnliche Inhalte
|
||||
- **Hybridsuche**: Kombiniert Vektor- und traditionelle Schlüsselwortsuche
|
||||
- **Konfigurierbare Ergebnisse**: Steuern Sie die Anzahl und den Relevanz-Schwellenwert der Ergebnisse
|
||||
- **Hybride Suche**: Kombiniert Vektor- und traditionelle Schlüsselwortsuche
|
||||
- **Konfigurierbare Ergebnisse**: Steuern Sie die Anzahl und Relevanzschwelle der Ergebnisse
|
||||
|
||||
## Dokumentenverwaltung
|
||||
|
||||
### Organisationsfunktionen
|
||||
- **Massenupload**: Laden Sie mehrere Dateien gleichzeitig über die asynchrone API hoch
|
||||
- **Verarbeitungsstatus**: Echtzeit-Updates zum Dokumentenverarbeitungsprozess
|
||||
- **Suchen und Filtern**: Finden Sie Dokumente schnell in großen Sammlungen
|
||||
- **Massen-Upload**: Laden Sie mehrere Dateien gleichzeitig über die asynchrone API hoch
|
||||
- **Verarbeitungsstatus**: Echtzeit-Updates zur Dokumentenverarbeitung
|
||||
- **Suchen und filtern**: Finden Sie Dokumente schnell in großen Sammlungen
|
||||
- **Metadaten-Tracking**: Automatische Erfassung von Dateiinformationen und Verarbeitungsdetails
|
||||
|
||||
### Sicherheit und Datenschutz
|
||||
- **Sichere Speicherung**: Dokumente werden mit Sicherheit auf Unternehmensniveau gespeichert
|
||||
- **Zugriffskontrolle**: Workspace-basierte Berechtigungen
|
||||
- **Verarbeitungsisolierung**: Jeder Workspace hat eine isolierte Dokumentenverarbeitung
|
||||
- **Verarbeitungsisolierung**: Jeder Workspace hat isolierte Dokumentenverarbeitung
|
||||
- **Datenaufbewahrung**: Konfigurieren Sie Richtlinien zur Dokumentenaufbewahrung
|
||||
|
||||
## Erste Schritte
|
||||
|
||||
1. **Navigieren Sie zu Ihrer Wissensdatenbank**: Zugriff über Ihre Workspace-Seitenleiste
|
||||
2. **Dokumente hochladen**: Drag & Drop oder wählen Sie Dateien zum Hochladen aus
|
||||
3. **Verarbeitung überwachen**: Beobachten Sie, wie Dokumente verarbeitet und in Chunks aufgeteilt werden
|
||||
4. **Chunks erkunden**: Sehen und bearbeiten Sie die verarbeiteten Inhalte
|
||||
5. **Zu Workflows hinzufügen**: Verwenden Sie den Wissensblock, um ihn in Ihre KI-Agenten zu integrieren
|
||||
2. **Dokumente hochladen**: Ziehen und ablegen oder Dateien zum Hochladen auswählen
|
||||
3. **Verarbeitung überwachen**: Beobachten Sie, wie Dokumente verarbeitet und in Abschnitte unterteilt werden
|
||||
4. **Abschnitte erkunden**: Zeigen Sie die verarbeiteten Inhalte an und bearbeiten Sie sie
|
||||
5. **Zu Workflows hinzufügen**: Verwenden Sie den Knowledge-Block, um mit Ihren KI-Agenten zu integrieren
|
||||
|
||||
Die Wissensdatenbank verwandelt Ihre statischen Dokumente in eine intelligente, durchsuchbare Ressource, die Ihre KI-Workflows für fundiertere und kontextbezogenere Antworten nutzen können.
|
||||
Die Wissensdatenbank verwandelt Ihre statischen Dokumente in eine intelligente, durchsuchbare Ressource, die Ihre KI-Workflows für fundiertere und kontextbezogene Antworten nutzen können.
|
||||
@@ -38,6 +38,7 @@ Erstellen Sie einen neuen Kontakt in Intercom mit E-Mail, external_id oder Rolle
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `role` | string | Nein | Die Rolle des Kontakts. Akzeptiert 'user' oder 'lead'. Standardmäßig 'lead', wenn nicht angegeben. |
|
||||
| `email` | string | Nein | Die E-Mail-Adresse des Kontakts |
|
||||
| `external_id` | string | Nein | Eine eindeutige Kennung für den Kontakt, die vom Client bereitgestellt wird |
|
||||
| `phone` | string | Nein | Die Telefonnummer des Kontakts |
|
||||
@@ -45,9 +46,10 @@ Erstellen Sie einen neuen Kontakt in Intercom mit E-Mail, external_id oder Rolle
|
||||
| `avatar` | string | Nein | Eine Avatar-Bild-URL für den Kontakt |
|
||||
| `signed_up_at` | number | Nein | Der Zeitpunkt der Registrierung des Benutzers als Unix-Zeitstempel |
|
||||
| `last_seen_at` | number | Nein | Der Zeitpunkt, zu dem der Benutzer zuletzt gesehen wurde, als Unix-Zeitstempel |
|
||||
| `owner_id` | string | Nein | Die ID eines Administrators, dem die Kontoinhaberschaft des Kontakts zugewiesen wurde |
|
||||
| `unsubscribed_from_emails` | boolean | Nein | Ob der Kontakt E-Mails abbestellt hat |
|
||||
| `custom_attributes` | string | Nein | Benutzerdefinierte Attribute als JSON-Objekt \(z.B. \{"attribute_name": "value"\}\) |
|
||||
| `owner_id` | string | Nein | Die ID eines Administrators, dem die Kontoverantwortung für den Kontakt zugewiesen wurde |
|
||||
| `unsubscribed_from_emails` | boolean | Nein | Ob der Kontakt von E-Mails abgemeldet ist |
|
||||
| `custom_attributes` | string | Nein | Benutzerdefinierte Attribute als JSON-Objekt \(z. B. \{"attribute_name": "value"\}\) |
|
||||
| `company_id` | string | Nein | Unternehmens-ID, mit der der Kontakt bei der Erstellung verknüpft werden soll |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -81,16 +83,19 @@ Einen bestehenden Kontakt in Intercom aktualisieren
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | Ja | Zu aktualisierende Kontakt-ID |
|
||||
| `contactId` | string | Ja | Kontakt-ID, die aktualisiert werden soll |
|
||||
| `role` | string | Nein | Die Rolle des Kontakts. Akzeptiert 'user' oder 'lead'. |
|
||||
| `external_id` | string | Nein | Eine eindeutige Kennung für den Kontakt, die vom Client bereitgestellt wird |
|
||||
| `email` | string | Nein | Die E-Mail-Adresse des Kontakts |
|
||||
| `phone` | string | Nein | Die Telefonnummer des Kontakts |
|
||||
| `name` | string | Nein | Der Name des Kontakts |
|
||||
| `avatar` | string | Nein | Eine Avatar-Bild-URL für den Kontakt |
|
||||
| `signed_up_at` | number | Nein | Der Zeitpunkt der Registrierung des Benutzers als Unix-Timestamp |
|
||||
| `last_seen_at` | number | Nein | Der Zeitpunkt, zu dem der Benutzer zuletzt gesehen wurde, als Unix-Timestamp |
|
||||
| `owner_id` | string | Nein | Die ID eines Administrators, dem die Kontoeigentümerschaft des Kontakts zugewiesen wurde |
|
||||
| `unsubscribed_from_emails` | boolean | Nein | Ob der Kontakt E-Mails abbestellt hat |
|
||||
| `custom_attributes` | string | Nein | Benutzerdefinierte Attribute als JSON-Objekt \(z.B. \{"attribut_name": "wert"\}\) |
|
||||
| `signed_up_at` | number | Nein | Der Zeitpunkt der Registrierung des Benutzers als Unix-Zeitstempel |
|
||||
| `last_seen_at` | number | Nein | Der Zeitpunkt, zu dem der Benutzer zuletzt gesehen wurde, als Unix-Zeitstempel |
|
||||
| `owner_id` | string | Nein | Die ID eines Administrators, dem die Kontoverantwortung für den Kontakt zugewiesen wurde |
|
||||
| `unsubscribed_from_emails` | boolean | Nein | Ob der Kontakt von E-Mails abgemeldet ist |
|
||||
| `custom_attributes` | string | Nein | Benutzerdefinierte Attribute als JSON-Objekt \(z. B. \{"attribute_name": "value"\}\) |
|
||||
| `company_id` | string | Nein | Unternehmens-ID, mit der der Kontakt verknüpft werden soll |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -125,9 +130,11 @@ Suche nach Kontakten in Intercom mit einer Abfrage
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Ja | Suchabfrage (z.B., \{"field":"email","operator":"=","value":"user@example.com"\}) |
|
||||
| `query` | string | Ja | Suchabfrage (z. B. \{"field":"email","operator":"=","value":"user@example.com"\}) |
|
||||
| `per_page` | number | Nein | Anzahl der Ergebnisse pro Seite (max: 150) |
|
||||
| `starting_after` | string | Nein | Cursor für Paginierung |
|
||||
| `sort_field` | string | Nein | Feld zum Sortieren (z. B. "name", "created_at", "last_seen_at") |
|
||||
| `sort_order` | string | Nein | Sortierreihenfolge: "ascending" oder "descending" |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -163,12 +170,13 @@ Ein Unternehmen in Intercom erstellen oder aktualisieren
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `company_id` | string | Ja | Ihre eindeutige Kennung für das Unternehmen |
|
||||
| `name` | string | Nein | Der Name des Unternehmens |
|
||||
| `website` | string | Nein | Die Unternehmenswebsite |
|
||||
| `plan` | string | Nein | Der Unternehmensplan |
|
||||
| `website` | string | Nein | Die Website des Unternehmens |
|
||||
| `plan` | string | Nein | Der Name des Unternehmensplans |
|
||||
| `size` | number | Nein | Die Anzahl der Mitarbeiter im Unternehmen |
|
||||
| `industry` | string | Nein | Die Branche, in der das Unternehmen tätig ist |
|
||||
| `monthly_spend` | number | Nein | Wie viel Umsatz das Unternehmen für Ihr Geschäft generiert. Hinweis: Dieses Feld kürzt Dezimalzahlen auf ganze Zahlen \(z.B. wird aus 155,98 die Zahl 155\) |
|
||||
| `monthly_spend` | number | Nein | Wie viel Umsatz das Unternehmen für Ihr Geschäft generiert. Hinweis: Dieses Feld rundet Dezimalzahlen auf ganze Zahlen ab (z. B. wird 155,98 zu 155) |
|
||||
| `custom_attributes` | string | Nein | Benutzerdefinierte Attribute als JSON-Objekt |
|
||||
| `remote_created_at` | number | Nein | Der Zeitpunkt, zu dem das Unternehmen von Ihnen erstellt wurde, als Unix-Zeitstempel |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -204,6 +212,7 @@ Listet alle Unternehmen von Intercom mit Paginierungsunterstützung auf. Hinweis
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | Nein | Anzahl der Ergebnisse pro Seite |
|
||||
| `page` | number | Nein | Seitennummer |
|
||||
| `starting_after` | string | Nein | Cursor für Paginierung (bevorzugt gegenüber seitenbasierter Paginierung) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -221,7 +230,8 @@ Eine einzelne Konversation anhand der ID von Intercom abrufen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Ja | Konversations-ID zum Abrufen |
|
||||
| `display_as` | string | Nein | Auf "plaintext" setzen, um Nachrichten im Klartext abzurufen |
|
||||
| `display_as` | string | Nein | Auf "plaintext" setzen, um Nachrichten als reinen Text abzurufen |
|
||||
| `include_translations` | boolean | Nein | Wenn true, werden Konversationsteile in die erkannte Sprache der Konversation übersetzt |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -238,8 +248,10 @@ Alle Konversationen von Intercom mit Paginierungsunterstützung auflisten
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | Nein | Anzahl der Ergebnisse pro Seite \(max: 150\) |
|
||||
| `per_page` | number | Nein | Anzahl der Ergebnisse pro Seite (max: 150) |
|
||||
| `starting_after` | string | Nein | Cursor für Paginierung |
|
||||
| `sort` | string | Nein | Feld zum Sortieren (z. B. "waiting_since", "updated_at", "created_at") |
|
||||
| `order` | string | Nein | Sortierreihenfolge: "asc" (aufsteigend) oder "desc" (absteigend) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -256,11 +268,12 @@ Als Administrator auf eine Konversation in Intercom antworten
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Ja | Konversations-ID, auf die geantwortet werden soll |
|
||||
| `conversationId` | string | Ja | Konversations-ID zum Antworten |
|
||||
| `message_type` | string | Ja | Nachrichtentyp: "comment" oder "note" |
|
||||
| `body` | string | Ja | Der Textinhalt der Antwort |
|
||||
| `admin_id` | string | Nein | Die ID des Administrators, der die Antwort verfasst. Wenn nicht angegeben, wird ein Standard-Administrator \(Operator/Fin\) verwendet. |
|
||||
| `attachment_urls` | string | Nein | Kommagetrennte Liste von Bild-URLs \(max. 10\) |
|
||||
| `admin_id` | string | Nein | Die ID des Administrators, der die Antwort verfasst. Falls nicht angegeben, wird ein Standard-Administrator (Operator/Fin) verwendet. |
|
||||
| `attachment_urls` | string | Nein | Kommagetrennte Liste von Bild-URLs (max. 10) |
|
||||
| `created_at` | number | Nein | Unix-Zeitstempel für den Zeitpunkt der Erstellung der Antwort. Falls nicht angegeben, wird die aktuelle Zeit verwendet. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -278,8 +291,10 @@ Nach Konversationen in Intercom mit einer Abfrage suchen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Ja | Suchabfrage als JSON-Objekt |
|
||||
| `per_page` | number | Nein | Anzahl der Ergebnisse pro Seite (max: 150) |
|
||||
| `per_page` | number | Nein | Anzahl der Ergebnisse pro Seite \(max: 150\) |
|
||||
| `starting_after` | string | Nein | Cursor für Paginierung |
|
||||
| `sort_field` | string | Nein | Feld, nach dem sortiert werden soll \(z. B. "created_at", "updated_at"\) |
|
||||
| `sort_order` | string | Nein | Sortierreihenfolge: "ascending" oder "descending" |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -297,8 +312,12 @@ Ein neues Ticket in Intercom erstellen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `ticket_type_id` | string | Ja | Die ID des Ticket-Typs |
|
||||
| `contacts` | string | Ja | JSON-Array von Kontakt-Identifikatoren (z.B. \{"id": "contact_id"\}) |
|
||||
| `contacts` | string | Ja | JSON-Array von Kontaktkennungen \(z. B. \[\{"id": "contact_id"\}\]\) |
|
||||
| `ticket_attributes` | string | Ja | JSON-Objekt mit Ticket-Attributen einschließlich _default_title_ und _default_description_ |
|
||||
| `company_id` | string | Nein | Unternehmens-ID, mit der das Ticket verknüpft werden soll |
|
||||
| `created_at` | number | Nein | Unix-Zeitstempel für den Zeitpunkt der Ticket-Erstellung. Wenn nicht angegeben, wird die aktuelle Zeit verwendet. |
|
||||
| `conversation_to_link_id` | string | Nein | ID einer vorhandenen Konversation, die mit diesem Ticket verknüpft werden soll |
|
||||
| `disable_notifications` | boolean | Nein | Wenn true, werden Benachrichtigungen bei der Ticket-Erstellung unterdrückt |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -330,15 +349,17 @@ Eine neue vom Administrator initiierte Nachricht in Intercom erstellen und sende
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Erforderlich | Beschreibung |
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `message_type` | string | Ja | Nachrichtentyp: "inapp" oder "email" |
|
||||
| `message_type` | string | Ja | Nachrichtentyp: "inapp" für In-App-Nachrichten oder "email" für E-Mail-Nachrichten |
|
||||
| `template` | string | Ja | Nachrichtenvorlagenstil: "plain" für einfachen Text oder "personal" für personalisierten Stil |
|
||||
| `subject` | string | Nein | Der Betreff der Nachricht \(für E-Mail-Typ\) |
|
||||
| `body` | string | Ja | Der Inhalt der Nachricht |
|
||||
| `from_type` | string | Ja | Absendertyp: "admin" |
|
||||
| `from_id` | string | Ja | Die ID des Administrators, der die Nachricht sendet |
|
||||
| `to_type` | string | Ja | Empfängertyp: "contact" |
|
||||
| `to_id` | string | Ja | Die ID des Kontakts, der die Nachricht empfängt |
|
||||
| `created_at` | number | Nein | Unix-Zeitstempel für den Zeitpunkt der Nachrichtenerstellung. Wenn nicht angegeben, wird die aktuelle Zeit verwendet. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -10,55 +10,52 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
color="#F64F9E"
|
||||
/>
|
||||
|
||||
## Gebrauchsanweisung
|
||||
## Nutzungsanweisungen
|
||||
|
||||
Memory in den Workflow integrieren. Kann Erinnerungen hinzufügen, eine Erinnerung abrufen, alle Erinnerungen abrufen und Erinnerungen löschen.
|
||||
Integrieren Sie Memory in den Workflow. Kann Erinnerungen hinzufügen, abrufen, alle Erinnerungen abrufen und Erinnerungen löschen.
|
||||
|
||||
## Tools
|
||||
|
||||
### `memory_add`
|
||||
|
||||
Füge eine neue Erinnerung zur Datenbank hinzu oder ergänze bestehende Erinnerungen mit derselben ID.
|
||||
Fügen Sie eine neue Erinnerung zur Datenbank hinzu oder hängen Sie sie an eine bestehende Erinnerung mit derselben ID an.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z.B. user-123, session-abc). Wenn bereits eine Erinnerung mit dieser conversationId für diesen Block existiert, wird die neue Nachricht angehängt. |
|
||||
| `id` | string | Nein | Legacy-Parameter für die Konversationskennung. Verwenden Sie stattdessen conversationId. Für Abwärtskompatibilität bereitgestellt. |
|
||||
| `role` | string | Ja | Rolle für Agent-Erinnerung (user, assistant oder system) |
|
||||
| `conversationId` | string | Nein | Konversationskennung \(z. B. user-123, session-abc\). Wenn bereits eine Erinnerung mit dieser conversationId existiert, wird die neue Nachricht an diese angehängt. |
|
||||
| `id` | string | Nein | Legacy-Parameter für Konversationskennung. Verwenden Sie stattdessen conversationId. Wird aus Gründen der Abwärtskompatibilität bereitgestellt. |
|
||||
| `role` | string | Ja | Rolle für Agent-Erinnerung \(user, assistant oder system\) |
|
||||
| `content` | string | Ja | Inhalt für Agent-Erinnerung |
|
||||
| `blockId` | string | Nein | Optionale Block-ID. Wenn nicht angegeben, wird die aktuelle Block-ID aus dem Ausführungskontext verwendet oder standardmäßig "default" gesetzt. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob der Speicher erfolgreich hinzugefügt wurde |
|
||||
| `memories` | array | Array von Speicherobjekten einschließlich des neuen oder aktualisierten Speichers |
|
||||
| `success` | boolean | Ob die Erinnerung erfolgreich hinzugefügt wurde |
|
||||
| `memories` | array | Array von Erinnerungsobjekten einschließlich der neuen oder aktualisierten Erinnerung |
|
||||
| `error` | string | Fehlermeldung, falls der Vorgang fehlgeschlagen ist |
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Erinnerungen nach conversationId, blockId, blockName oder einer Kombination abrufen. Gibt alle übereinstimmenden Erinnerungen zurück.
|
||||
Erinnerung nach conversationId abrufen. Gibt übereinstimmende Erinnerungen zurück.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z.B. user-123, session-abc). Wenn allein angegeben, werden alle Erinnerungen für diese Konversation über alle Blöcke hinweg zurückgegeben. |
|
||||
| `id` | string | Nein | Legacy-Parameter für die Konversationskennung. Verwenden Sie stattdessen conversationId. Für Abwärtskompatibilität bereitgestellt. |
|
||||
| `blockId` | string | Nein | Block-Kennung. Wenn allein angegeben, werden alle Erinnerungen für diesen Block über alle Konversationen hinweg zurückgegeben. Wenn mit conversationId angegeben, werden Erinnerungen für diese spezifische Konversation in diesem Block zurückgegeben. |
|
||||
| `blockName` | string | Nein | Blockname. Alternative zu blockId. Wenn allein angegeben, werden alle Erinnerungen für Blöcke mit diesem Namen zurückgegeben. Wenn mit conversationId angegeben, werden Erinnerungen für diese Konversation in Blöcken mit diesem Namen zurückgegeben. |
|
||||
| `conversationId` | string | Nein | Konversationskennung \(z. B. user-123, session-abc\). Gibt Erinnerungen für diese Konversation zurück. |
|
||||
| `id` | string | Nein | Legacy-Parameter für Konversationskennung. Verwenden Sie stattdessen conversationId. Wird aus Gründen der Abwärtskompatibilität bereitgestellt. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob die Erinnerung erfolgreich abgerufen wurde |
|
||||
| `memories` | array | Array von Speicherobjekten mit conversationId, blockId, blockName und data-Feldern |
|
||||
| `success` | boolean | Ob der Speicher erfolgreich abgerufen wurde |
|
||||
| `memories` | array | Array von Speicherobjekten mit conversationId- und data-Feldern |
|
||||
| `message` | string | Erfolgs- oder Fehlermeldung |
|
||||
| `error` | string | Fehlermeldung, wenn der Vorgang fehlgeschlagen ist |
|
||||
| `error` | string | Fehlermeldung, falls fehlgeschlagen |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -73,31 +70,29 @@ Alle Speicher aus der Datenbank abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob alle Erinnerungen erfolgreich abgerufen wurden |
|
||||
| `memories` | array | Array aller Speicherobjekte mit key, conversationId, blockId, blockName und data-Feldern |
|
||||
| `success` | boolean | Ob alle Speicher erfolgreich abgerufen wurden |
|
||||
| `memories` | array | Array aller Speicherobjekte mit key-, conversationId- und data-Feldern |
|
||||
| `message` | string | Erfolgs- oder Fehlermeldung |
|
||||
| `error` | string | Fehlermeldung, wenn der Vorgang fehlgeschlagen ist |
|
||||
| `error` | string | Fehlermeldung, falls fehlgeschlagen |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Löschen von Erinnerungen nach conversationId, blockId, blockName oder einer Kombination davon. Unterstützt Massenlöschung.
|
||||
Speicher nach conversationId löschen.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z.B. user-123, session-abc). Wenn allein angegeben, werden alle Erinnerungen für diese Konversation über alle Blöcke hinweg gelöscht. |
|
||||
| `id` | string | Nein | Legacy-Parameter für die Konversationskennung. Verwenden Sie stattdessen conversationId. Für Abwärtskompatibilität bereitgestellt. |
|
||||
| `blockId` | string | Nein | Block-Kennung. Wenn allein angegeben, werden alle Erinnerungen für diesen Block über alle Konversationen hinweg gelöscht. Wenn mit conversationId angegeben, werden Erinnerungen für diese spezifische Konversation in diesem Block gelöscht. |
|
||||
| `blockName` | string | Nein | Blockname. Alternative zu blockId. Wenn allein angegeben, werden alle Erinnerungen für Blöcke mit diesem Namen gelöscht. Wenn mit conversationId angegeben, werden Erinnerungen für diese Konversation in Blöcken mit diesem Namen gelöscht. |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z. B. user-123, session-abc). Löscht alle Speicher für diese Konversation. |
|
||||
| `id` | string | Nein | Legacy-Parameter für Konversationskennung. Verwenden Sie stattdessen conversationId. Wird aus Gründen der Abwärtskompatibilität bereitgestellt. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob die Erinnerung erfolgreich gelöscht wurde |
|
||||
| `success` | boolean | Ob der Speicher erfolgreich gelöscht wurde |
|
||||
| `message` | string | Erfolgs- oder Fehlermeldung |
|
||||
| `error` | string | Fehlermeldung, wenn der Vorgang fehlgeschlagen ist |
|
||||
| `error` | string | Fehlermeldung, falls fehlgeschlagen |
|
||||
|
||||
## Hinweise
|
||||
|
||||
|
||||
@@ -47,12 +47,13 @@ Daten aus einer Supabase-Tabelle abfragen
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z. B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der abzufragenden Supabase-Tabelle |
|
||||
| `filter` | string | Nein | PostgREST-Filter \(z.B. "id=eq.123"\) |
|
||||
| `orderBy` | string | Nein | Spalte zum Sortieren \(fügen Sie DESC für absteigend hinzu\) |
|
||||
| `schema` | string | Nein | Datenbankschema für die Abfrage \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Nein | PostgREST-Filter \(z. B. "id=eq.123"\) |
|
||||
| `orderBy` | string | Nein | Spalte zum Sortieren \(fügen Sie DESC für absteigende Sortierung hinzu\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Zeilen |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service-Rolle-Secret-Schlüssel |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -69,8 +70,9 @@ Daten in eine Supabase-Tabelle einfügen
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z. B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle, in die Daten eingefügt werden sollen |
|
||||
| `schema` | string | Nein | Datenbankschema für das Einfügen \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `data` | array | Ja | Die einzufügenden Daten \(Array von Objekten oder ein einzelnes Objekt\) |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
@@ -89,10 +91,11 @@ Eine einzelne Zeile aus einer Supabase-Tabelle basierend auf Filterkriterien abr
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID (z.B. jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle für die Abfrage |
|
||||
| `filter` | string | Ja | PostgREST-Filter zum Finden der spezifischen Zeile (z.B. "id=eq.123") |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service-Role-Secret-Key |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z. B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der abzufragenden Supabase-Tabelle |
|
||||
| `schema` | string | Nein | Datenbankschema für die Abfrage \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Ja | PostgREST-Filter zum Auffinden der spezifischen Zeile \(z. B. "id=eq.123"\) |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -109,9 +112,10 @@ Zeilen in einer Supabase-Tabelle basierend auf Filterkriterien aktualisieren
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID (z.B. jdrkgepadsdopsntdlom) |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der zu aktualisierenden Supabase-Tabelle |
|
||||
| `filter` | string | Ja | PostgREST-Filter zur Identifizierung der zu aktualisierenden Zeilen (z.B. "id=eq.123") |
|
||||
| `schema` | string | Nein | Datenbankschema für die Aktualisierung \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Ja | PostgREST-Filter zur Identifizierung der zu aktualisierenden Zeilen \(z.B. "id=eq.123"\) |
|
||||
| `data` | object | Ja | Daten, die in den übereinstimmenden Zeilen aktualisiert werden sollen |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
@@ -130,9 +134,10 @@ Zeilen aus einer Supabase-Tabelle basierend auf Filterkriterien löschen
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID (z.B. jdrkgepadsdopsntdlom) |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle, aus der gelöscht werden soll |
|
||||
| `filter` | string | Ja | PostgREST-Filter zur Identifizierung der zu löschenden Zeilen (z.B. "id=eq.123") |
|
||||
| `schema` | string | Nein | Datenbankschema für die Löschung \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Ja | PostgREST-Filter zur Identifizierung der zu löschenden Zeilen \(z.B. "id=eq.123"\) |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
#### Ausgabe
|
||||
@@ -151,8 +156,9 @@ Daten in eine Supabase-Tabelle einfügen oder aktualisieren (Upsert-Operation)
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle, in die Daten upsertet werden sollen |
|
||||
| `data` | array | Ja | Die zu upsertenden Daten \(einfügen oder aktualisieren\) - Array von Objekten oder ein einzelnes Objekt |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle, in die Daten eingefügt oder aktualisiert werden sollen |
|
||||
| `schema` | string | Nein | Datenbankschema für Upsert \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `data` | array | Ja | Die Daten für Upsert \(Einfügen oder Aktualisieren\) - Array von Objekten oder ein einzelnes Objekt |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
#### Ausgabe
|
||||
@@ -171,7 +177,8 @@ Zeilen in einer Supabase-Tabelle zählen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle, deren Zeilen gezählt werden sollen |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle, aus der Zeilen gezählt werden sollen |
|
||||
| `schema` | string | Nein | Datenbankschema zum Zählen \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Nein | PostgREST-Filter \(z.B. "status=eq.active"\) |
|
||||
| `countType` | string | Nein | Zähltyp: exact, planned oder estimated \(Standard: exact\) |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
@@ -192,7 +199,8 @@ Volltextsuche in einer Supabase-Tabelle durchführen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der Supabase-Tabelle für die Suche |
|
||||
| `table` | string | Ja | Der Name der zu durchsuchenden Supabase-Tabelle |
|
||||
| `schema` | string | Nein | Datenbankschema zum Durchsuchen \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `column` | string | Ja | Die Spalte, in der gesucht werden soll |
|
||||
| `query` | string | Ja | Die Suchanfrage |
|
||||
| `searchType` | string | Nein | Suchtyp: plain, phrase oder websearch \(Standard: websearch\) |
|
||||
|
||||
@@ -48,40 +48,40 @@ The model breakdown shows:
|
||||
|
||||
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
|
||||
<Tab>
|
||||
**Hosted Models** - Sim provides API keys with a 2.5x pricing multiplier:
|
||||
**Hosted Models** - Sim provides API keys with a 2x pricing multiplier:
|
||||
|
||||
**OpenAI**
|
||||
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-5.1 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.63 / $5.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.13 / $1.00 |
|
||||
| GPT-4o | $2.50 / $10.00 | $6.25 / $25.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $1.00 / $4.00 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.25 / $1.00 |
|
||||
| o1 | $15.00 / $60.00 | $37.50 / $150.00 |
|
||||
| o3 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.75 / $11.00 |
|
||||
| GPT-5.1 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.50 / $4.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.10 / $0.80 |
|
||||
| GPT-4o | $2.50 / $10.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $0.80 / $3.20 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.20 / $0.80 |
|
||||
| o1 | $15.00 / $60.00 | $30.00 / $120.00 |
|
||||
| o3 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.20 / $8.80 |
|
||||
|
||||
**Anthropic**
|
||||
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $12.50 / $62.50 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $37.50 / $187.50 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.50 / $12.50 |
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $10.00 / $50.00 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $30.00 / $150.00 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.00 / $10.00 |
|
||||
|
||||
**Google**
|
||||
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $5.00 / $30.00 |
|
||||
| Gemini 2.5 Pro | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 2.5 Flash | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $4.00 / $24.00 |
|
||||
| Gemini 2.5 Pro | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| Gemini 2.5 Flash | $0.30 / $2.50 | $0.60 / $5.00 |
|
||||
|
||||
*The 2.5x multiplier covers infrastructure and API management costs.*
|
||||
*The 2x multiplier covers infrastructure and API management costs.*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
@@ -183,7 +183,7 @@ Different subscription plans have different usage limits:
|
||||
|
||||
| Plan | Monthly Usage Limit | Rate Limits (per minute) |
|
||||
|------|-------------------|-------------------------|
|
||||
| **Free** | $10 | 5 sync, 10 async |
|
||||
| **Free** | $20 | 5 sync, 10 async |
|
||||
| **Pro** | $100 | 10 sync, 50 async |
|
||||
| **Team** | $500 (pooled) | 50 sync, 100 async |
|
||||
| **Enterprise** | Custom | Custom |
|
||||
|
||||
@@ -34,9 +34,15 @@ Once your documents are processed, you can view and edit the individual chunks.
|
||||
<Image src="/static/knowledgebase/knowledgebase.png" alt="Document chunks view showing processed content" width={800} height={500} />
|
||||
|
||||
### Chunk Configuration
|
||||
- **Default chunk size**: 1,024 characters
|
||||
- **Configurable range**: 100-4,000 characters per chunk
|
||||
- **Smart overlap**: 200 characters by default for context preservation
|
||||
|
||||
When creating a knowledge base, you can configure how documents are split into chunks:
|
||||
|
||||
| Setting | Unit | Default | Range | Description |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **Max Chunk Size** | tokens | 1,024 | 100-4,000 | Maximum size of each chunk (1 token ≈ 4 characters) |
|
||||
| **Min Chunk Size** | characters | 1 | 1-2,000 | Minimum chunk size to avoid tiny fragments |
|
||||
| **Overlap** | characters | 200 | 0-500 | Context overlap between consecutive chunks |
|
||||
|
||||
- **Hierarchical splitting**: Respects document structure (sections, paragraphs, sentences)
|
||||
|
||||
### Editing Capabilities
|
||||
|
||||
@@ -41,6 +41,7 @@ Create a new contact in Intercom with email, external_id, or role
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `role` | string | No | The role of the contact. Accepts 'user' or 'lead'. Defaults to 'lead' if not specified. |
|
||||
| `email` | string | No | The contact's email address |
|
||||
| `external_id` | string | No | A unique identifier for the contact provided by the client |
|
||||
| `phone` | string | No | The contact's phone number |
|
||||
@@ -51,6 +52,7 @@ Create a new contact in Intercom with email, external_id, or role
|
||||
| `owner_id` | string | No | The id of an admin that has been assigned account ownership of the contact |
|
||||
| `unsubscribed_from_emails` | boolean | No | Whether the contact is unsubscribed from emails |
|
||||
| `custom_attributes` | string | No | Custom attributes as JSON object \(e.g., \{"attribute_name": "value"\}\) |
|
||||
| `company_id` | string | No | Company ID to associate the contact with during creation |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -85,6 +87,8 @@ Update an existing contact in Intercom
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | Yes | Contact ID to update |
|
||||
| `role` | string | No | The role of the contact. Accepts 'user' or 'lead'. |
|
||||
| `external_id` | string | No | A unique identifier for the contact provided by the client |
|
||||
| `email` | string | No | The contact's email address |
|
||||
| `phone` | string | No | The contact's phone number |
|
||||
| `name` | string | No | The contact's name |
|
||||
@@ -94,6 +98,7 @@ Update an existing contact in Intercom
|
||||
| `owner_id` | string | No | The id of an admin that has been assigned account ownership of the contact |
|
||||
| `unsubscribed_from_emails` | boolean | No | Whether the contact is unsubscribed from emails |
|
||||
| `custom_attributes` | string | No | Custom attributes as JSON object \(e.g., \{"attribute_name": "value"\}\) |
|
||||
| `company_id` | string | No | Company ID to associate the contact with |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -131,6 +136,8 @@ Search for contacts in Intercom using a query
|
||||
| `query` | string | Yes | Search query \(e.g., \{"field":"email","operator":"=","value":"user@example.com"\}\) |
|
||||
| `per_page` | number | No | Number of results per page \(max: 150\) |
|
||||
| `starting_after` | string | No | Cursor for pagination |
|
||||
| `sort_field` | string | No | Field to sort by \(e.g., "name", "created_at", "last_seen_at"\) |
|
||||
| `sort_order` | string | No | Sort order: "ascending" or "descending" |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -172,6 +179,7 @@ Create or update a company in Intercom
|
||||
| `industry` | string | No | The industry the company operates in |
|
||||
| `monthly_spend` | number | No | How much revenue the company generates for your business. Note: This field truncates floats to whole integers \(e.g., 155.98 becomes 155\) |
|
||||
| `custom_attributes` | string | No | Custom attributes as JSON object |
|
||||
| `remote_created_at` | number | No | The time the company was created by you as a Unix timestamp |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -207,6 +215,7 @@ List all companies from Intercom with pagination support. Note: This endpoint ha
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | No | Number of results per page |
|
||||
| `page` | number | No | Page number |
|
||||
| `starting_after` | string | No | Cursor for pagination \(preferred over page-based pagination\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -225,6 +234,7 @@ Retrieve a single conversation by ID from Intercom
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Yes | Conversation ID to retrieve |
|
||||
| `display_as` | string | No | Set to "plaintext" to retrieve messages in plain text |
|
||||
| `include_translations` | boolean | No | When true, conversation parts will be translated to the detected language of the conversation |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -243,6 +253,8 @@ List all conversations from Intercom with pagination support
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | No | Number of results per page \(max: 150\) |
|
||||
| `starting_after` | string | No | Cursor for pagination |
|
||||
| `sort` | string | No | Field to sort by \(e.g., "waiting_since", "updated_at", "created_at"\) |
|
||||
| `order` | string | No | Sort order: "asc" \(ascending\) or "desc" \(descending\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -264,6 +276,7 @@ Reply to a conversation as an admin in Intercom
|
||||
| `body` | string | Yes | The text body of the reply |
|
||||
| `admin_id` | string | No | The ID of the admin authoring the reply. If not provided, a default admin \(Operator/Fin\) will be used. |
|
||||
| `attachment_urls` | string | No | Comma-separated list of image URLs \(max 10\) |
|
||||
| `created_at` | number | No | Unix timestamp for when the reply was created. If not provided, current time is used. |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -283,6 +296,8 @@ Search for conversations in Intercom using a query
|
||||
| `query` | string | Yes | Search query as JSON object |
|
||||
| `per_page` | number | No | Number of results per page \(max: 150\) |
|
||||
| `starting_after` | string | No | Cursor for pagination |
|
||||
| `sort_field` | string | No | Field to sort by \(e.g., "created_at", "updated_at"\) |
|
||||
| `sort_order` | string | No | Sort order: "ascending" or "descending" |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -302,6 +317,10 @@ Create a new ticket in Intercom
|
||||
| `ticket_type_id` | string | Yes | The ID of the ticket type |
|
||||
| `contacts` | string | Yes | JSON array of contact identifiers \(e.g., \[\{"id": "contact_id"\}\]\) |
|
||||
| `ticket_attributes` | string | Yes | JSON object with ticket attributes including _default_title_ and _default_description_ |
|
||||
| `company_id` | string | No | Company ID to associate the ticket with |
|
||||
| `created_at` | number | No | Unix timestamp for when the ticket was created. If not provided, current time is used. |
|
||||
| `conversation_to_link_id` | string | No | ID of an existing conversation to link to this ticket |
|
||||
| `disable_notifications` | boolean | No | When true, suppresses notifications when the ticket is created |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -335,13 +354,15 @@ Create and send a new admin-initiated message in Intercom
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `message_type` | string | Yes | Message type: "inapp" or "email" |
|
||||
| `message_type` | string | Yes | Message type: "inapp" for in-app messages or "email" for email messages |
|
||||
| `template` | string | Yes | Message template style: "plain" for plain text or "personal" for personalized style |
|
||||
| `subject` | string | No | The subject of the message \(for email type\) |
|
||||
| `body` | string | Yes | The body of the message |
|
||||
| `from_type` | string | Yes | Sender type: "admin" |
|
||||
| `from_id` | string | Yes | The ID of the admin sending the message |
|
||||
| `to_type` | string | Yes | Recipient type: "contact" |
|
||||
| `to_id` | string | Yes | The ID of the contact receiving the message |
|
||||
| `created_at` | number | No | Unix timestamp for when the message was created. If not provided, current time is used. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -26,11 +26,10 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If a memory with this conversationId already exists for this block, the new message will be appended to it. |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If a memory with this conversationId already exists, the new message will be appended to it. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `role` | string | Yes | Role for agent memory \(user, assistant, or system\) |
|
||||
| `content` | string | Yes | Content for agent memory |
|
||||
| `blockId` | string | No | Optional block ID. If not provided, uses the current block ID from execution context, or defaults to "default". |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -42,23 +41,21 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Retrieve memory by conversationId, blockId, blockName, or a combination. Returns all matching memories.
|
||||
Retrieve memory by conversationId. Returns matching memories.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If provided alone, returns all memories for this conversation across all blocks. |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). Returns memories for this conversation. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `blockId` | string | No | Block identifier. If provided alone, returns all memories for this block across all conversations. If provided with conversationId, returns memories for that specific conversation in this block. |
|
||||
| `blockName` | string | No | Block name. Alternative to blockId. If provided alone, returns all memories for blocks with this name. If provided with conversationId, returns memories for that conversation in blocks with this name. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the memory was retrieved successfully |
|
||||
| `memories` | array | Array of memory objects with conversationId, blockId, blockName, and data fields |
|
||||
| `memories` | array | Array of memory objects with conversationId and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
@@ -76,22 +73,20 @@ Retrieve all memories from the database
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether all memories were retrieved successfully |
|
||||
| `memories` | array | Array of all memory objects with key, conversationId, blockId, blockName, and data fields |
|
||||
| `memories` | array | Array of all memory objects with key, conversationId, and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Delete memories by conversationId, blockId, blockName, or a combination. Supports bulk deletion.
|
||||
Delete memories by conversationId.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If provided alone, deletes all memories for this conversation across all blocks. |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). Deletes all memories for this conversation. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `blockId` | string | No | Block identifier. If provided alone, deletes all memories for this block across all conversations. If provided with conversationId, deletes memories for that specific conversation in this block. |
|
||||
| `blockName` | string | No | Block name. Alternative to blockId. If provided alone, deletes all memories for blocks with this name. If provided with conversationId, deletes memories for that conversation in blocks with this name. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ Query data from a Supabase table
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `schema` | string | No | Database schema to query from \(default: public\). Use this to access tables in other schemas. |
|
||||
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
|
||||
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
|
||||
| `limit` | number | No | Maximum number of rows to return |
|
||||
@@ -74,6 +75,7 @@ Insert data into a Supabase table
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to insert data into |
|
||||
| `schema` | string | No | Database schema to insert into \(default: public\). Use this to access tables in other schemas. |
|
||||
| `data` | array | Yes | The data to insert \(array of objects or a single object\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
@@ -94,6 +96,7 @@ Get a single row from a Supabase table based on filter criteria
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `schema` | string | No | Database schema to query from \(default: public\). Use this to access tables in other schemas. |
|
||||
| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., "id=eq.123"\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
@@ -114,6 +117,7 @@ Update rows in a Supabase table based on filter criteria
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to update |
|
||||
| `schema` | string | No | Database schema to update in \(default: public\). Use this to access tables in other schemas. |
|
||||
| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., "id=eq.123"\) |
|
||||
| `data` | object | Yes | Data to update in the matching rows |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
@@ -135,6 +139,7 @@ Delete rows from a Supabase table based on filter criteria
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to delete from |
|
||||
| `schema` | string | No | Database schema to delete from \(default: public\). Use this to access tables in other schemas. |
|
||||
| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., "id=eq.123"\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
@@ -155,6 +160,7 @@ Insert or update data in a Supabase table (upsert operation)
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to upsert data into |
|
||||
| `schema` | string | No | Database schema to upsert into \(default: public\). Use this to access tables in other schemas. |
|
||||
| `data` | array | Yes | The data to upsert \(insert or update\) - array of objects or a single object |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
@@ -175,6 +181,7 @@ Count rows in a Supabase table
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to count rows from |
|
||||
| `schema` | string | No | Database schema to count from \(default: public\). Use this to access tables in other schemas. |
|
||||
| `filter` | string | No | PostgREST filter \(e.g., "status=eq.active"\) |
|
||||
| `countType` | string | No | Count type: exact, planned, or estimated \(default: exact\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
@@ -196,6 +203,7 @@ Perform full-text search on a Supabase table
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to search |
|
||||
| `schema` | string | No | Database schema to search in \(default: public\). Use this to access tables in other schemas. |
|
||||
| `column` | string | Yes | The column to search in |
|
||||
| `query` | string | Yes | The search query |
|
||||
| `searchType` | string | No | Search type: plain, phrase, or websearch \(default: websearch\) |
|
||||
|
||||
@@ -47,42 +47,42 @@ El desglose del modelo muestra:
|
||||
|
||||
## Opciones de precios
|
||||
|
||||
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
|
||||
<Tabs items={['Modelos alojados', 'Trae tu propia clave API']}>
|
||||
<Tab>
|
||||
**Modelos alojados** - Sim proporciona claves API con un multiplicador de precio de 2.5x:
|
||||
**Modelos alojados** - Sim proporciona claves API con un multiplicador de precio de 2x:
|
||||
|
||||
**OpenAI**
|
||||
| Modelo | Precio base (Entrada/Salida) | Precio alojado (Entrada/Salida) |
|
||||
| Modelo | Precio base (entrada/salida) | Precio alojado (entrada/salida) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-5.1 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.63 / $5.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.13 / $1.00 |
|
||||
| GPT-4o | $2.50 / $10.00 | $6.25 / $25.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $1.00 / $4.00 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.25 / $1.00 |
|
||||
| o1 | $15.00 / $60.00 | $37.50 / $150.00 |
|
||||
| o3 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.75 / $11.00 |
|
||||
| GPT-5.1 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.50 / $4.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.10 / $0.80 |
|
||||
| GPT-4o | $2.50 / $10.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $0.80 / $3.20 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.20 / $0.80 |
|
||||
| o1 | $15.00 / $60.00 | $30.00 / $120.00 |
|
||||
| o3 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.20 / $8.80 |
|
||||
|
||||
**Anthropic**
|
||||
| Modelo | Precio base (Entrada/Salida) | Precio alojado (Entrada/Salida) |
|
||||
| Modelo | Precio base (entrada/salida) | Precio alojado (entrada/salida) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $12.50 / $62.50 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $37.50 / $187.50 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.50 / $12.50 |
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $10.00 / $50.00 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $30.00 / $150.00 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.00 / $10.00 |
|
||||
|
||||
**Google**
|
||||
| Modelo | Precio base (Entrada/Salida) | Precio alojado (Entrada/Salida) |
|
||||
| Modelo | Precio base (entrada/salida) | Precio alojado (entrada/salida) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $5.00 / $30.00 |
|
||||
| Gemini 2.5 Pro | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 2.5 Flash | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $4.00 / $24.00 |
|
||||
| Gemini 2.5 Pro | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| Gemini 2.5 Flash | $0.30 / $2.50 | $0.60 / $5.00 |
|
||||
|
||||
*El multiplicador de 2.5x cubre los costos de infraestructura y gestión de API.*
|
||||
*El multiplicador 2x cubre los costos de infraestructura y gestión de API.*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
@@ -187,10 +187,10 @@ Los diferentes planes de suscripción tienen diferentes límites de uso:
|
||||
|
||||
| Plan | Límite de uso mensual | Límites de tasa (por minuto) |
|
||||
|------|-------------------|-------------------------|
|
||||
| **Gratuito** | $10 | 5 sincrónico, 10 asincrónico |
|
||||
| **Pro** | $100 | 10 sincrónico, 50 asincrónico |
|
||||
| **Equipo** | $500 (agrupado) | 50 sincrónico, 100 asincrónico |
|
||||
| **Empresa** | Personalizado | Personalizado |
|
||||
| **Gratis** | $20 | 5 síncronas, 10 asíncronas |
|
||||
| **Pro** | $100 | 10 síncronas, 50 asíncronas |
|
||||
| **Equipo** | $500 (compartido) | 50 síncronas, 100 asíncronas |
|
||||
| **Empresarial** | Personalizado | Personalizado |
|
||||
|
||||
## Modelo de facturación
|
||||
|
||||
|
||||
@@ -35,81 +35,87 @@ Una vez que tus documentos están procesados, puedes ver y editar los fragmentos
|
||||
<Image src="/static/knowledgebase/knowledgebase.png" alt="Vista de fragmentos de documentos mostrando contenido procesado" width={800} height={500} />
|
||||
|
||||
### Configuración de fragmentos
|
||||
- **Tamaño predeterminado del fragmento**: 1.024 caracteres
|
||||
- **Rango configurable**: 100-4.000 caracteres por fragmento
|
||||
- **Superposición inteligente**: 200 caracteres por defecto para preservar el contexto
|
||||
- **División jerárquica**: Respeta la estructura del documento (secciones, párrafos, oraciones)
|
||||
|
||||
Al crear una base de conocimiento, puedes configurar cómo se dividen los documentos en fragmentos:
|
||||
|
||||
| Configuración | Unidad | Predeterminado | Rango | Descripción |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **Tamaño máximo de fragmento** | tokens | 1.024 | 100-4.000 | Tamaño máximo de cada fragmento (1 token ≈ 4 caracteres) |
|
||||
| **Tamaño mínimo de fragmento** | caracteres | 1 | 1-2.000 | Tamaño mínimo de fragmento para evitar fragmentos diminutos |
|
||||
| **Superposición** | caracteres | 200 | 0-500 | Superposición de contexto entre fragmentos consecutivos |
|
||||
|
||||
- **División jerárquica**: respeta la estructura del documento (secciones, párrafos, oraciones)
|
||||
|
||||
### Capacidades de edición
|
||||
- **Editar contenido de fragmentos**: Modificar el contenido de texto de fragmentos individuales
|
||||
- **Ajustar límites de fragmentos**: Fusionar o dividir fragmentos según sea necesario
|
||||
- **Añadir metadatos**: Mejorar fragmentos con contexto adicional
|
||||
- **Operaciones masivas**: Gestionar múltiples fragmentos de manera eficiente
|
||||
- **Editar contenido de fragmentos**: modifica el contenido de texto de fragmentos individuales
|
||||
- **Ajustar límites de fragmentos**: combina o divide fragmentos según sea necesario
|
||||
- **Añadir metadatos**: mejora los fragmentos con contexto adicional
|
||||
- **Operaciones masivas**: gestiona múltiples fragmentos de manera eficiente
|
||||
|
||||
## Procesamiento avanzado de PDF
|
||||
|
||||
Para documentos PDF, Sim ofrece capacidades de procesamiento mejoradas:
|
||||
|
||||
### Soporte OCR
|
||||
### Compatibilidad con OCR
|
||||
Cuando se configura con Azure o [Mistral OCR](https://docs.mistral.ai/ocr/):
|
||||
- **Procesamiento de documentos escaneados**: Extraer texto de PDFs basados en imágenes
|
||||
- **Manejo de contenido mixto**: Procesar PDFs con texto e imágenes
|
||||
- **Alta precisión**: Modelos avanzados de IA aseguran una extracción precisa del texto
|
||||
- **Procesamiento de documentos escaneados**: extrae texto de PDF basados en imágenes
|
||||
- **Manejo de contenido mixto**: procesa PDF con texto e imágenes
|
||||
- **Alta precisión**: los modelos de IA avanzados garantizan una extracción de texto precisa
|
||||
|
||||
## Uso del bloque de conocimiento en flujos de trabajo
|
||||
|
||||
Una vez que tus documentos son procesados, puedes utilizarlos en tus flujos de trabajo de IA a través del bloque de Conocimiento. Esto permite la Generación Aumentada por Recuperación (RAG), permitiendo a tus agentes de IA acceder y razonar sobre el contenido de tus documentos para proporcionar respuestas más precisas y contextuales.
|
||||
Una vez que tus documentos estén procesados, puedes usarlos en tus flujos de trabajo de IA a través del bloque de conocimiento. Esto habilita la generación aumentada por recuperación (RAG), permitiendo que tus agentes de IA accedan y razonen sobre el contenido de tus documentos para proporcionar respuestas más precisas y contextuales.
|
||||
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="Uso del bloque de conocimiento en flujos de trabajo" width={800} height={500} />
|
||||
|
||||
### Características del bloque de conocimiento
|
||||
- **Búsqueda semántica**: Encontrar contenido relevante usando consultas en lenguaje natural
|
||||
- **Integración de contexto**: Incluir automáticamente fragmentos relevantes en los prompts del agente
|
||||
- **Recuperación dinámica**: La búsqueda ocurre en tiempo real durante la ejecución del flujo de trabajo
|
||||
- **Puntuación de relevancia**: Resultados clasificados por similitud semántica
|
||||
- **Búsqueda semántica**: encuentra contenido relevante usando consultas en lenguaje natural
|
||||
- **Integración de contexto**: incluye automáticamente fragmentos relevantes en las indicaciones del agente
|
||||
- **Recuperación dinámica**: la búsqueda ocurre en tiempo real durante la ejecución del flujo de trabajo
|
||||
- **Puntuación de relevancia**: resultados clasificados por similitud semántica
|
||||
|
||||
### Opciones de integración
|
||||
- **Prompts del sistema**: Proporcionar contexto a tus agentes de IA
|
||||
- **Contexto dinámico**: Buscar e incluir información relevante durante las conversaciones
|
||||
- **Búsqueda multi-documento**: Consultar a través de toda tu base de conocimiento
|
||||
- **Búsqueda filtrada**: Combinar con etiquetas para una recuperación precisa de contenido
|
||||
- **Prompts del sistema**: proporciona contexto a tus agentes de IA
|
||||
- **Contexto dinámico**: busca e incluye información relevante durante las conversaciones
|
||||
- **Búsqueda multidocumento**: consulta en toda tu base de conocimiento
|
||||
- **Búsqueda filtrada**: combina con etiquetas para una recuperación precisa de contenido
|
||||
|
||||
## Tecnología de búsqueda vectorial
|
||||
|
||||
Sim utiliza búsqueda vectorial impulsada por [pgvector](https://github.com/pgvector/pgvector) para entender el significado y contexto de tu contenido:
|
||||
Sim utiliza búsqueda vectorial impulsada por [pgvector](https://github.com/pgvector/pgvector) para comprender el significado y contexto de tu contenido:
|
||||
|
||||
### Comprensión semántica
|
||||
- **Búsqueda contextual**: Encuentra contenido relevante incluso cuando las palabras clave exactas no coinciden
|
||||
- **Recuperación basada en conceptos**: Comprende las relaciones entre ideas
|
||||
- **Soporte multilingüe**: Funciona en diferentes idiomas
|
||||
- **Reconocimiento de sinónimos**: Encuentra términos y conceptos relacionados
|
||||
- **Búsqueda contextual**: encuentra contenido relevante incluso cuando las palabras clave exactas no coinciden
|
||||
- **Recuperación basada en conceptos**: comprende las relaciones entre ideas
|
||||
- **Soporte multiidioma**: funciona en diferentes idiomas
|
||||
- **Reconocimiento de sinónimos**: encuentra términos y conceptos relacionados
|
||||
|
||||
### Capacidades de búsqueda
|
||||
- **Consultas en lenguaje natural**: Haz preguntas en español simple
|
||||
- **Búsqueda por similitud**: Encuentra contenido conceptualmente similar
|
||||
- **Búsqueda híbrida**: Combina búsqueda vectorial y tradicional por palabras clave
|
||||
- **Resultados configurables**: Controla el número y umbral de relevancia de los resultados
|
||||
- **Consultas en lenguaje natural**: haz preguntas en lenguaje cotidiano
|
||||
- **Búsqueda por similitud**: encuentra contenido conceptualmente similar
|
||||
- **Búsqueda híbrida**: combina búsqueda vectorial y búsqueda tradicional por palabras clave
|
||||
- **Resultados configurables**: controla el número y el umbral de relevancia de los resultados
|
||||
|
||||
## Gestión de documentos
|
||||
|
||||
### Características de organización
|
||||
- **Carga masiva**: Sube múltiples archivos a la vez mediante la API asíncrona
|
||||
- **Estado de procesamiento**: Actualizaciones en tiempo real sobre el procesamiento de documentos
|
||||
- **Búsqueda y filtrado**: Encuentra documentos rápidamente en grandes colecciones
|
||||
- **Seguimiento de metadatos**: Captura automática de información de archivos y detalles de procesamiento
|
||||
### Funciones de organización
|
||||
- **Carga masiva**: sube múltiples archivos a la vez mediante la API asíncrona
|
||||
- **Estado de procesamiento**: actualizaciones en tiempo real sobre el procesamiento de documentos
|
||||
- **Búsqueda y filtrado**: encuentra documentos rápidamente en colecciones grandes
|
||||
- **Seguimiento de metadatos**: captura automática de información de archivos y detalles de procesamiento
|
||||
|
||||
### Seguridad y privacidad
|
||||
- **Almacenamiento seguro**: Documentos almacenados con seguridad de nivel empresarial
|
||||
- **Control de acceso**: Permisos basados en espacios de trabajo
|
||||
- **Aislamiento de procesamiento**: Cada espacio de trabajo tiene procesamiento de documentos aislado
|
||||
- **Retención de datos**: Configura políticas de retención de documentos
|
||||
- **Almacenamiento seguro**: documentos almacenados con seguridad de nivel empresarial
|
||||
- **Control de acceso**: permisos basados en el espacio de trabajo
|
||||
- **Aislamiento de procesamiento**: cada espacio de trabajo tiene procesamiento de documentos aislado
|
||||
- **Retención de datos**: configura políticas de retención de documentos
|
||||
|
||||
## Primeros pasos
|
||||
|
||||
1. **Navega a tu base de conocimiento**: Accede desde la barra lateral de tu espacio de trabajo
|
||||
2. **Sube documentos**: Arrastra y suelta o selecciona archivos para subir
|
||||
3. **Monitorea el procesamiento**: Observa cómo se procesan y dividen los documentos
|
||||
4. **Explora fragmentos**: Visualiza y edita el contenido procesado
|
||||
5. **Añade a flujos de trabajo**: Usa el bloque de Conocimiento para integrarlo con tus agentes de IA
|
||||
1. **Navega a tu base de conocimiento**: accede desde la barra lateral de tu espacio de trabajo
|
||||
2. **Sube documentos**: arrastra y suelta o selecciona archivos para subir
|
||||
3. **Monitorea el procesamiento**: observa cómo se procesan y fragmentan los documentos
|
||||
4. **Explora fragmentos**: visualiza y edita el contenido procesado
|
||||
5. **Añade a flujos de trabajo**: utiliza el bloque Knowledge para integrar con tus agentes de IA
|
||||
|
||||
La base de conocimiento transforma tus documentos estáticos en un recurso inteligente y consultable que tus flujos de trabajo de IA pueden aprovechar para obtener respuestas más informadas y contextuales.
|
||||
La base de conocimientos transforma tus documentos estáticos en un recurso inteligente y consultable que tus flujos de trabajo de IA pueden aprovechar para obtener respuestas más informadas y contextuales.
|
||||
@@ -38,16 +38,18 @@ Crear un nuevo contacto en Intercom con email, external_id o rol
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `role` | string | No | El rol del contacto. Acepta 'user' o 'lead'. Por defecto es 'lead' si no se especifica. |
|
||||
| `email` | string | No | La dirección de correo electrónico del contacto |
|
||||
| `external_id` | string | No | Un identificador único para el contacto proporcionado por el cliente |
|
||||
| `phone` | string | No | El número de teléfono del contacto |
|
||||
| `name` | string | No | El nombre del contacto |
|
||||
| `avatar` | string | No | Una URL de imagen de avatar para el contacto |
|
||||
| `signed_up_at` | number | No | El momento en que el usuario se registró como marca de tiempo Unix |
|
||||
| `last_seen_at` | number | No | El momento en que el usuario fue visto por última vez como marca de tiempo Unix |
|
||||
| `owner_id` | string | No | El id de un administrador que ha sido asignado como propietario de la cuenta del contacto |
|
||||
| `signed_up_at` | number | No | La hora en que el usuario se registró como marca de tiempo Unix |
|
||||
| `last_seen_at` | number | No | La hora en que el usuario fue visto por última vez como marca de tiempo Unix |
|
||||
| `owner_id` | string | No | El id de un administrador al que se le ha asignado la propiedad de la cuenta del contacto |
|
||||
| `unsubscribed_from_emails` | boolean | No | Si el contacto está dado de baja de los correos electrónicos |
|
||||
| `custom_attributes` | string | No | Atributos personalizados como objeto JSON \(p. ej., \{"nombre_atributo": "valor"\}\) |
|
||||
| `custom_attributes` | string | No | Atributos personalizados como objeto JSON \(ej., \{"attribute_name": "value"\}\) |
|
||||
| `company_id` | string | No | ID de empresa para asociar el contacto durante la creación |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -82,15 +84,18 @@ Actualizar un contacto existente en Intercom
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | Sí | ID del contacto a actualizar |
|
||||
| `email` | string | No | Dirección de correo electrónico del contacto |
|
||||
| `phone` | string | No | Número de teléfono del contacto |
|
||||
| `name` | string | No | Nombre del contacto |
|
||||
| `avatar` | string | No | URL de imagen de avatar para el contacto |
|
||||
| `signed_up_at` | number | No | El momento en que el usuario se registró como marca de tiempo Unix |
|
||||
| `last_seen_at` | number | No | El momento en que el usuario fue visto por última vez como marca de tiempo Unix |
|
||||
| `owner_id` | string | No | El id de un administrador que ha sido asignado como propietario de la cuenta del contacto |
|
||||
| `role` | string | No | El rol del contacto. Acepta 'user' o 'lead'. |
|
||||
| `external_id` | string | No | Un identificador único para el contacto proporcionado por el cliente |
|
||||
| `email` | string | No | La dirección de correo electrónico del contacto |
|
||||
| `phone` | string | No | El número de teléfono del contacto |
|
||||
| `name` | string | No | El nombre del contacto |
|
||||
| `avatar` | string | No | Una URL de imagen de avatar para el contacto |
|
||||
| `signed_up_at` | number | No | La hora en que el usuario se registró como marca de tiempo Unix |
|
||||
| `last_seen_at` | number | No | La hora en que el usuario fue visto por última vez como marca de tiempo Unix |
|
||||
| `owner_id` | string | No | El id de un administrador al que se le ha asignado la propiedad de la cuenta del contacto |
|
||||
| `unsubscribed_from_emails` | boolean | No | Si el contacto está dado de baja de los correos electrónicos |
|
||||
| `custom_attributes` | string | No | Atributos personalizados como objeto JSON (p. ej., \{"nombre_atributo": "valor"\}) |
|
||||
| `custom_attributes` | string | No | Atributos personalizados como objeto JSON \(ej., \{"attribute_name": "value"\}\) |
|
||||
| `company_id` | string | No | ID de empresa para asociar el contacto |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -125,9 +130,11 @@ Buscar contactos en Intercom usando una consulta
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Sí | Consulta de búsqueda \(p. ej., \{"field":"email","operator":"=","value":"user@example.com"\}\) |
|
||||
| `query` | string | Sí | Consulta de búsqueda \(ej., \{"field":"email","operator":"=","value":"user@example.com"\}\) |
|
||||
| `per_page` | number | No | Número de resultados por página \(máx: 150\) |
|
||||
| `starting_after` | string | No | Cursor para paginación |
|
||||
| `sort_field` | string | No | Campo por el cual ordenar \(ej., "name", "created_at", "last_seen_at"\) |
|
||||
| `sort_order` | string | No | Orden de clasificación: "ascending" o "descending" |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -166,9 +173,10 @@ Crear o actualizar una empresa en Intercom
|
||||
| `website` | string | No | El sitio web de la empresa |
|
||||
| `plan` | string | No | El nombre del plan de la empresa |
|
||||
| `size` | number | No | El número de empleados en la empresa |
|
||||
| `industry` | string | No | El sector en el que opera la empresa |
|
||||
| `monthly_spend` | number | No | Cuántos ingresos genera la empresa para tu negocio. Nota: Este campo trunca los decimales a números enteros \(por ejemplo, 155.98 se convierte en 155\) |
|
||||
| `industry` | string | No | La industria en la que opera la empresa |
|
||||
| `monthly_spend` | number | No | Cuántos ingresos genera la empresa para tu negocio. Nota: Este campo trunca decimales a números enteros \(ej., 155.98 se convierte en 155\) |
|
||||
| `custom_attributes` | string | No | Atributos personalizados como objeto JSON |
|
||||
| `remote_created_at` | number | No | La fecha en que creaste la empresa como marca de tiempo Unix |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -204,6 +212,7 @@ Lista todas las empresas de Intercom con soporte de paginación. Nota: Este endp
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | No | Número de resultados por página |
|
||||
| `page` | number | No | Número de página |
|
||||
| `starting_after` | string | No | Cursor para paginación \(preferido sobre paginación basada en páginas\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -221,7 +230,8 @@ Recuperar una sola conversación por ID desde Intercom
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Sí | ID de la conversación a recuperar |
|
||||
| `display_as` | string | No | Establecer como "plaintext" para recuperar mensajes en texto plano |
|
||||
| `display_as` | string | No | Establecer en "plaintext" para recuperar mensajes en texto plano |
|
||||
| `include_translations` | boolean | No | Cuando es true, las partes de la conversación se traducirán al idioma detectado de la conversación |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -240,6 +250,8 @@ Listar todas las conversaciones de Intercom con soporte de paginación
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | No | Número de resultados por página \(máx: 150\) |
|
||||
| `starting_after` | string | No | Cursor para paginación |
|
||||
| `sort` | string | No | Campo por el que ordenar \(p. ej., "waiting_since", "updated_at", "created_at"\) |
|
||||
| `order` | string | No | Orden de clasificación: "asc" \(ascendente\) o "desc" \(descendente\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -258,9 +270,10 @@ Responder a una conversación como administrador en Intercom
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Sí | ID de la conversación a la que responder |
|
||||
| `message_type` | string | Sí | Tipo de mensaje: "comment" o "note" |
|
||||
| `body` | string | Sí | El texto del cuerpo de la respuesta |
|
||||
| `admin_id` | string | No | El ID del administrador que escribe la respuesta. Si no se proporciona, se utilizará un administrador predeterminado \(Operator/Fin\). |
|
||||
| `attachment_urls` | string | No | Lista separada por comas de URLs de imágenes \(máximo 10\) |
|
||||
| `body` | string | Sí | El cuerpo de texto de la respuesta |
|
||||
| `admin_id` | string | No | El ID del administrador que escribe la respuesta. Si no se proporciona, se usará un administrador predeterminado \(Operator/Fin\). |
|
||||
| `attachment_urls` | string | No | Lista de URLs de imágenes separadas por comas \(máx 10\) |
|
||||
| `created_at` | number | No | Marca de tiempo Unix de cuándo se creó la respuesta. Si no se proporciona, se usa la hora actual. |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -278,8 +291,10 @@ Buscar conversaciones en Intercom usando una consulta
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | Sí | Consulta de búsqueda como objeto JSON |
|
||||
| `per_page` | number | No | Número de resultados por página (máx: 150) |
|
||||
| `per_page` | number | No | Número de resultados por página \(máx: 150\) |
|
||||
| `starting_after` | string | No | Cursor para paginación |
|
||||
| `sort_field` | string | No | Campo por el que ordenar \(p. ej., "created_at", "updated_at"\) |
|
||||
| `sort_order` | string | No | Orden de clasificación: "ascending" o "descending" |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -297,8 +312,12 @@ Crear un nuevo ticket en Intercom
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `ticket_type_id` | string | Sí | El ID del tipo de ticket |
|
||||
| `contacts` | string | Sí | Array JSON de identificadores de contacto (p. ej., \{"id": "contact_id"\}) |
|
||||
| `contacts` | string | Sí | Array JSON de identificadores de contacto \(p. ej., \[\{"id": "contact_id"\}\]\) |
|
||||
| `ticket_attributes` | string | Sí | Objeto JSON con atributos del ticket incluyendo _default_title_ y _default_description_ |
|
||||
| `company_id` | string | No | ID de la empresa para asociar el ticket |
|
||||
| `created_at` | number | No | Marca de tiempo Unix de cuándo se creó el ticket. Si no se proporciona, se utiliza la hora actual. |
|
||||
| `conversation_to_link_id` | string | No | ID de una conversación existente para vincular a este ticket |
|
||||
| `disable_notifications` | boolean | No | Cuando es true, suprime las notificaciones cuando se crea el ticket |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -332,13 +351,15 @@ Crear y enviar un nuevo mensaje iniciado por el administrador en Intercom
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `message_type` | string | Sí | Tipo de mensaje: "inapp" o "email" |
|
||||
| `message_type` | string | Sí | Tipo de mensaje: "inapp" para mensajes dentro de la aplicación o "email" para mensajes de correo electrónico |
|
||||
| `template` | string | Sí | Estilo de plantilla del mensaje: "plain" para texto sin formato o "personal" para estilo personalizado |
|
||||
| `subject` | string | No | El asunto del mensaje \(para tipo email\) |
|
||||
| `body` | string | Sí | El cuerpo del mensaje |
|
||||
| `from_type` | string | Sí | Tipo de remitente: "admin" |
|
||||
| `from_id` | string | Sí | El ID del administrador que envía el mensaje |
|
||||
| `to_type` | string | Sí | Tipo de destinatario: "contact" |
|
||||
| `to_id` | string | Sí | El ID del contacto que recibe el mensaje |
|
||||
| `created_at` | number | No | Marca de tiempo Unix de cuándo se creó el mensaje. Si no se proporciona, se utiliza la hora actual. |
|
||||
|
||||
#### Salida
|
||||
|
||||
|
||||
@@ -12,57 +12,54 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra la Memoria en el flujo de trabajo. Puede añadir, obtener una memoria, obtener todas las memorias y eliminar memorias.
|
||||
Integra memoria en el flujo de trabajo. Puede añadir, obtener una memoria, obtener todas las memorias y eliminar memorias.
|
||||
|
||||
## Herramientas
|
||||
|
||||
### `memory_add`
|
||||
|
||||
Añade una nueva memoria a la base de datos o agrega a una memoria existente con el mismo ID.
|
||||
Añadir una nueva memoria a la base de datos o agregar a una memoria existente con el mismo ID.
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | string | No | Identificador de conversación (p. ej., user-123, session-abc). Si ya existe una memoria con este conversationId para este bloque, el nuevo mensaje se añadirá a ella. |
|
||||
| `id` | string | No | Parámetro heredado para el identificador de conversación. Use conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `role` | string | Sí | Rol para la memoria del agente (user, assistant o system) |
|
||||
| `conversationId` | string | No | Identificador de conversación \(ej., user-123, session-abc\). Si ya existe una memoria con este conversationId, el nuevo mensaje se agregará a ella. |
|
||||
| `id` | string | No | Parámetro heredado para identificador de conversación. Usa conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `role` | string | Sí | Rol para la memoria del agente \(user, assistant o system\) |
|
||||
| `content` | string | Sí | Contenido para la memoria del agente |
|
||||
| `blockId` | string | No | ID de bloque opcional. Si no se proporciona, utiliza el ID del bloque actual del contexto de ejecución, o por defecto "default". |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indica si la memoria se añadió correctamente |
|
||||
| `success` | boolean | Si la memoria se añadió correctamente |
|
||||
| `memories` | array | Array de objetos de memoria incluyendo la memoria nueva o actualizada |
|
||||
| `error` | string | Mensaje de error si la operación falló |
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Recuperar memoria por conversationId, blockId, blockName o una combinación. Devuelve todas las memorias coincidentes.
|
||||
Recuperar memoria por conversationId. Devuelve las memorias coincidentes.
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | string | No | Identificador de conversación (p. ej., user-123, session-abc). Si se proporciona solo, devuelve todas las memorias para esta conversación en todos los bloques. |
|
||||
| `id` | string | No | Parámetro heredado para el identificador de conversación. Use conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `blockId` | string | No | Identificador de bloque. Si se proporciona solo, devuelve todas las memorias para este bloque en todas las conversaciones. Si se proporciona con conversationId, devuelve las memorias para esa conversación específica en este bloque. |
|
||||
| `blockName` | string | No | Nombre del bloque. Alternativa a blockId. Si se proporciona solo, devuelve todas las memorias para bloques con este nombre. Si se proporciona con conversationId, devuelve las memorias para esa conversación en bloques con este nombre. |
|
||||
| `conversationId` | string | No | Identificador de conversación \(ej., user-123, session-abc\). Devuelve las memorias para esta conversación. |
|
||||
| `id` | string | No | Parámetro heredado para identificador de conversación. Usa conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si la memoria fue recuperada con éxito |
|
||||
| `memories` | array | Array de objetos de memoria con campos conversationId, blockId, blockName y data |
|
||||
| `success` | boolean | Si la memoria se recuperó exitosamente |
|
||||
| `memories` | array | Array de objetos de memoria con campos conversationId y data |
|
||||
| `message` | string | Mensaje de éxito o error |
|
||||
| `error` | string | Mensaje de error si la operación falló |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
Recuperar todas las memorias de la base de datos
|
||||
Recupera todas las memorias de la base de datos
|
||||
|
||||
#### Entrada
|
||||
|
||||
@@ -73,29 +70,27 @@ Recuperar todas las memorias de la base de datos
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si todas las memorias fueron recuperadas con éxito |
|
||||
| `memories` | array | Array de todos los objetos de memoria con campos key, conversationId, blockId, blockName y data |
|
||||
| `success` | boolean | Si todas las memorias se recuperaron exitosamente |
|
||||
| `memories` | array | Array de todos los objetos de memoria con campos key, conversationId y data |
|
||||
| `message` | string | Mensaje de éxito o error |
|
||||
| `error` | string | Mensaje de error si la operación falló |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Eliminar memorias por conversationId, blockId, blockName o una combinación. Admite eliminación masiva.
|
||||
Elimina memorias por conversationId.
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | string | No | Identificador de conversación (p. ej., user-123, session-abc). Si se proporciona solo, elimina todas las memorias para esta conversación en todos los bloques. |
|
||||
| `id` | string | No | Parámetro heredado para el identificador de conversación. Use conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `blockId` | string | No | Identificador de bloque. Si se proporciona solo, elimina todas las memorias para este bloque en todas las conversaciones. Si se proporciona con conversationId, elimina las memorias para esa conversación específica en este bloque. |
|
||||
| `blockName` | string | No | Nombre del bloque. Alternativa a blockId. Si se proporciona solo, elimina todas las memorias para bloques con este nombre. Si se proporciona con conversationId, elimina las memorias para esa conversación en bloques con este nombre. |
|
||||
| `conversationId` | string | No | Identificador de conversación (ej., user-123, session-abc). Elimina todas las memorias de esta conversación. |
|
||||
| `id` | string | No | Parámetro heredado para identificador de conversación. Usa conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si la memoria fue eliminada con éxito |
|
||||
| `success` | boolean | Si la memoria se eliminó exitosamente |
|
||||
| `message` | string | Mensaje de éxito o error |
|
||||
| `error` | string | Mensaje de error si la operación falló |
|
||||
|
||||
|
||||
@@ -47,10 +47,11 @@ Consultar datos de una tabla de Supabase
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Sí | Tu ID de proyecto de Supabase \(ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | El nombre de la tabla de Supabase a consultar |
|
||||
| `filter` | string | No | Filtro de PostgREST \(ej., "id=eq.123"\) |
|
||||
| `orderBy` | string | No | Columna para ordenar \(añade DESC para orden descendente\) |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase a consultar |
|
||||
| `schema` | string | No | Esquema de base de datos desde donde consultar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `filter` | string | No | Filtro PostgREST \(p. ej., "id=eq.123"\) |
|
||||
| `orderBy` | string | No | Columna para ordenar \(añade DESC para descendente\) |
|
||||
| `limit` | number | No | Número máximo de filas a devolver |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
@@ -68,9 +69,10 @@ Insertar datos en una tabla de Supabase
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase donde insertar datos |
|
||||
| `schema` | string | No | Esquema de base de datos donde insertar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `data` | array | Sí | Los datos a insertar \(array de objetos o un solo objeto\) |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
@@ -88,9 +90,10 @@ Obtener una sola fila de una tabla de Supabase basada en criterios de filtro
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto de Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla de Supabase para consultar |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase a consultar |
|
||||
| `schema` | string | No | Esquema de base de datos desde donde consultar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `filter` | string | Sí | Filtro PostgREST para encontrar la fila específica \(p. ej., "id=eq.123"\) |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
@@ -111,8 +114,9 @@ Actualizar filas en una tabla de Supabase según criterios de filtro
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase a actualizar |
|
||||
| `schema` | string | No | Esquema de base de datos donde actualizar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `filter` | string | Sí | Filtro PostgREST para identificar las filas a actualizar \(p. ej., "id=eq.123"\) |
|
||||
| `data` | object | Sí | Datos para actualizar en las filas coincidentes |
|
||||
| `data` | object | Sí | Datos a actualizar en las filas coincidentes |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
#### Salida
|
||||
@@ -132,6 +136,7 @@ Eliminar filas de una tabla de Supabase según criterios de filtro
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase de la que eliminar |
|
||||
| `schema` | string | No | Esquema de base de datos del que eliminar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `filter` | string | Sí | Filtro PostgREST para identificar las filas a eliminar \(p. ej., "id=eq.123"\) |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
@@ -152,7 +157,8 @@ Insertar o actualizar datos en una tabla de Supabase (operación upsert)
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase donde hacer upsert de datos |
|
||||
| `data` | array | Sí | Los datos para upsert \(insertar o actualizar\) - array de objetos o un solo objeto |
|
||||
| `schema` | string | No | Esquema de base de datos donde hacer upsert \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `data` | array | Sí | Los datos para hacer upsert \(insertar o actualizar\) - array de objetos o un solo objeto |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
#### Salida
|
||||
@@ -171,7 +177,8 @@ Contar filas en una tabla de Supabase
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase de la que contar filas |
|
||||
| `table` | string | Sí | El nombre de la tabla Supabase de la que contar filas |
|
||||
| `schema` | string | No | Esquema de base de datos desde el que contar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `filter` | string | No | Filtro PostgREST \(p. ej., "status=eq.active"\) |
|
||||
| `countType` | string | No | Tipo de conteo: exact, planned o estimated \(predeterminado: exact\) |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
@@ -192,7 +199,8 @@ Realizar búsqueda de texto completo en una tabla de Supabase
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase en la que buscar |
|
||||
| `table` | string | Sí | El nombre de la tabla Supabase donde buscar |
|
||||
| `schema` | string | No | Esquema de base de datos en el que buscar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `column` | string | Sí | La columna en la que buscar |
|
||||
| `query` | string | Sí | La consulta de búsqueda |
|
||||
| `searchType` | string | No | Tipo de búsqueda: plain, phrase o websearch \(predeterminado: websearch\) |
|
||||
|
||||
@@ -47,42 +47,42 @@ La répartition des modèles montre :
|
||||
|
||||
## Options de tarification
|
||||
|
||||
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
|
||||
<Tabs items={['Modèles hébergés', 'Apportez votre propre clé API']}>
|
||||
<Tab>
|
||||
**Modèles hébergés** - Sim fournit des clés API avec un multiplicateur de prix de 2,5x :
|
||||
**Modèles hébergés** - Sim fournit des clés API avec un multiplicateur de prix de 2x :
|
||||
|
||||
**OpenAI**
|
||||
| Modèle | Prix de base (Entrée/Sortie) | Prix hébergé (Entrée/Sortie) |
|
||||
| Modèle | Prix de base (entrée/sortie) | Prix hébergé (entrée/sortie) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-5.1 | 1,25 $ / 10,00 $ | 3,13 $ / 25,00 $ |
|
||||
| GPT-5 | 1,25 $ / 10,00 $ | 3,13 $ / 25,00 $ |
|
||||
| GPT-5 Mini | 0,25 $ / 2,00 $ | 0,63 $ / 5,00 $ |
|
||||
| GPT-5 Nano | 0,05 $ / 0,40 $ | 0,13 $ / 1,00 $ |
|
||||
| GPT-4o | 2,50 $ / 10,00 $ | 6,25 $ / 25,00 $ |
|
||||
| GPT-4.1 | 2,00 $ / 8,00 $ | 5,00 $ / 20,00 $ |
|
||||
| GPT-4.1 Mini | 0,40 $ / 1,60 $ | 1,00 $ / 4,00 $ |
|
||||
| GPT-4.1 Nano | 0,10 $ / 0,40 $ | 0,25 $ / 1,00 $ |
|
||||
| o1 | 15,00 $ / 60,00 $ | 37,50 $ / 150,00 $ |
|
||||
| o3 | 2,00 $ / 8,00 $ | 5,00 $ / 20,00 $ |
|
||||
| o4 Mini | 1,10 $ / 4,40 $ | 2,75 $ / 11,00 $ |
|
||||
| GPT-5.1 | 1,25 $ / 10,00 $ | 2,50 $ / 20,00 $ |
|
||||
| GPT-5 | 1,25 $ / 10,00 $ | 2,50 $ / 20,00 $ |
|
||||
| GPT-5 Mini | 0,25 $ / 2,00 $ | 0,50 $ / 4,00 $ |
|
||||
| GPT-5 Nano | 0,05 $ / 0,40 $ | 0,10 $ / 0,80 $ |
|
||||
| GPT-4o | 2,50 $ / 10,00 $ | 5,00 $ / 20,00 $ |
|
||||
| GPT-4.1 | 2,00 $ / 8,00 $ | 4,00 $ / 16,00 $ |
|
||||
| GPT-4.1 Mini | 0,40 $ / 1,60 $ | 0,80 $ / 3,20 $ |
|
||||
| GPT-4.1 Nano | 0,10 $ / 0,40 $ | 0,20 $ / 0,80 $ |
|
||||
| o1 | 15,00 $ / 60,00 $ | 30,00 $ / 120,00 $ |
|
||||
| o3 | 2,00 $ / 8,00 $ | 4,00 $ / 16,00 $ |
|
||||
| o4 Mini | 1,10 $ / 4,40 $ | 2,20 $ / 8,80 $ |
|
||||
|
||||
**Anthropic**
|
||||
| Modèle | Prix de base (Entrée/Sortie) | Prix hébergé (Entrée/Sortie) |
|
||||
| Modèle | Prix de base (entrée/sortie) | Prix hébergé (entrée/sortie) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Claude Opus 4.5 | 5,00 $ / 25,00 $ | 12,50 $ / 62,50 $ |
|
||||
| Claude Opus 4.1 | 15,00 $ / 75,00 $ | 37,50 $ / 187,50 $ |
|
||||
| Claude Sonnet 4.5 | 3,00 $ / 15,00 $ | 7,50 $ / 37,50 $ |
|
||||
| Claude Sonnet 4.0 | 3,00 $ / 15,00 $ | 7,50 $ / 37,50 $ |
|
||||
| Claude Haiku 4.5 | 1,00 $ / 5,00 $ | 2,50 $ / 12,50 $ |
|
||||
| Claude Opus 4.5 | 5,00 $ / 25,00 $ | 10,00 $ / 50,00 $ |
|
||||
| Claude Opus 4.1 | 15,00 $ / 75,00 $ | 30,00 $ / 150,00 $ |
|
||||
| Claude Sonnet 4.5 | 3,00 $ / 15,00 $ | 6,00 $ / 30,00 $ |
|
||||
| Claude Sonnet 4.0 | 3,00 $ / 15,00 $ | 6,00 $ / 30,00 $ |
|
||||
| Claude Haiku 4.5 | 1,00 $ / 5,00 $ | 2,00 $ / 10,00 $ |
|
||||
|
||||
**Google**
|
||||
| Modèle | Prix de base (Entrée/Sortie) | Prix hébergé (Entrée/Sortie) |
|
||||
| Modèle | Prix de base (entrée/sortie) | Prix hébergé (entrée/sortie) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Gemini 3 Pro Preview | 2,00 $ / 12,00 $ | 5,00 $ / 30,00 $ |
|
||||
| Gemini 2.5 Pro | 0,15 $ / 0,60 $ | 0,38 $ / 1,50 $ |
|
||||
| Gemini 2.5 Flash | 0,15 $ / 0,60 $ | 0,38 $ / 1,50 $ |
|
||||
| Gemini 3 Pro Preview | 2,00 $ / 12,00 $ | 4,00 $ / 24,00 $ |
|
||||
| Gemini 2.5 Pro | 1,25 $ / 10,00 $ | 2,50 $ / 20,00 $ |
|
||||
| Gemini 2.5 Flash | 0,30 $ / 2,50 $ | 0,60 $ / 5,00 $ |
|
||||
|
||||
*Le multiplicateur de 2,5x couvre les coûts d'infrastructure et de gestion des API.*
|
||||
*Le multiplicateur 2x couvre les coûts d'infrastructure et de gestion des API.*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
@@ -187,7 +187,7 @@ Les différents forfaits d'abonnement ont des limites d'utilisation différentes
|
||||
|
||||
| Forfait | Limite d'utilisation mensuelle | Limites de débit (par minute) |
|
||||
|------|-------------------|-------------------------|
|
||||
| **Gratuit** | 10 $ | 5 sync, 10 async |
|
||||
| **Gratuit** | 20 $ | 5 sync, 10 async |
|
||||
| **Pro** | 100 $ | 10 sync, 50 async |
|
||||
| **Équipe** | 500 $ (mutualisé) | 50 sync, 100 async |
|
||||
| **Entreprise** | Personnalisé | Personnalisé |
|
||||
|
||||
@@ -35,53 +35,59 @@ Une fois vos documents traités, vous pouvez visualiser et modifier les segments
|
||||
<Image src="/static/knowledgebase/knowledgebase.png" alt="Vue des segments de document montrant le contenu traité" width={800} height={500} />
|
||||
|
||||
### Configuration des fragments
|
||||
- **Taille par défaut des fragments** : 1 024 caractères
|
||||
- **Plage configurable** : 100 à 4 000 caractères par fragment
|
||||
- **Chevauchement intelligent** : 200 caractères par défaut pour préserver le contexte
|
||||
- **Découpage hiérarchique** : respecte la structure du document (sections, paragraphes, phrases)
|
||||
|
||||
Lors de la création d'une base de connaissances, vous pouvez configurer la façon dont les documents sont divisés en fragments :
|
||||
|
||||
| Paramètre | Unité | Par défaut | Plage | Description |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **Taille max. du fragment** | jetons | 1 024 | 100-4 000 | Taille maximale de chaque fragment (1 jeton ≈ 4 caractères) |
|
||||
| **Taille min. du fragment** | caractères | 1 | 1-2 000 | Taille minimale du fragment pour éviter les fragments minuscules |
|
||||
| **Chevauchement** | caractères | 200 | 0-500 | Chevauchement de contexte entre fragments consécutifs |
|
||||
|
||||
- **Division hiérarchique** : respecte la structure du document (sections, paragraphes, phrases)
|
||||
|
||||
### Capacités d'édition
|
||||
- **Modifier le contenu des fragments** : modifier le contenu textuel des fragments individuels
|
||||
- **Ajuster les limites des fragments** : fusionner ou diviser les fragments selon les besoins
|
||||
- **Ajouter des métadonnées** : enrichir les fragments avec du contexte supplémentaire
|
||||
- **Opérations en masse** : gérer efficacement plusieurs fragments
|
||||
- **Modifier le contenu du fragment** : modifiez le contenu textuel des fragments individuels
|
||||
- **Ajuster les limites du fragment** : fusionnez ou divisez les fragments selon les besoins
|
||||
- **Ajouter des métadonnées** : enrichissez les fragments avec du contexte supplémentaire
|
||||
- **Opérations en masse** : gérez plusieurs fragments efficacement
|
||||
|
||||
## Traitement avancé des PDF
|
||||
|
||||
Pour les documents PDF, Sim offre des capacités de traitement améliorées :
|
||||
|
||||
### Support OCR
|
||||
Lorsque configuré avec Azure ou [Mistral OCR](https://docs.mistral.ai/ocr/) :
|
||||
- **Traitement de documents numérisés** : extraction de texte à partir de PDF basés sur des images
|
||||
- **Gestion de contenu mixte** : traitement des PDF contenant à la fois du texte et des images
|
||||
- **Haute précision** : les modèles d'IA avancés assurent une extraction précise du texte
|
||||
### Prise en charge de l'OCR
|
||||
Lorsqu'il est configuré avec Azure ou [Mistral OCR](https://docs.mistral.ai/ocr/) :
|
||||
- **Traitement de documents numérisés** : extrayez le texte des PDF basés sur des images
|
||||
- **Gestion de contenu mixte** : traitez les PDF contenant à la fois du texte et des images
|
||||
- **Haute précision** : les modèles d'IA avancés garantissent une extraction de texte précise
|
||||
|
||||
## Utilisation du bloc de connaissances dans les flux de travail
|
||||
## Utilisation du bloc de connaissances dans les workflows
|
||||
|
||||
Une fois vos documents traités, vous pouvez les utiliser dans vos flux de travail d'IA grâce au bloc de connaissances. Cela permet la génération augmentée par récupération (RAG), permettant à vos agents IA d'accéder et de raisonner sur le contenu de vos documents pour fournir des réponses plus précises et contextuelles.
|
||||
Une fois vos documents traités, vous pouvez les utiliser dans vos workflows d'IA via le bloc de connaissances. Cela active la génération augmentée par récupération (RAG), permettant à vos agents d'IA d'accéder à votre contenu documentaire et de raisonner dessus pour fournir des réponses plus précises et contextuelles.
|
||||
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="Utilisation du bloc de connaissances dans les flux de travail" width={800} height={500} />
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="Utilisation du bloc de connaissances dans les workflows" width={800} height={500} />
|
||||
|
||||
### Fonctionnalités du bloc de connaissances
|
||||
- **Recherche sémantique** : trouver du contenu pertinent à l'aide de requêtes en langage naturel
|
||||
- **Intégration du contexte** : inclure automatiquement les fragments pertinents dans les prompts des agents
|
||||
- **Récupération dynamique** : la recherche s'effectue en temps réel pendant l'exécution du flux de travail
|
||||
- **Évaluation de la pertinence** : résultats classés par similarité sémantique
|
||||
- **Recherche sémantique** : trouvez du contenu pertinent à l'aide de requêtes en langage naturel
|
||||
- **Intégration contextuelle** : incluez automatiquement les fragments pertinents dans les prompts de l'agent
|
||||
- **Récupération dynamique** : la recherche s'effectue en temps réel pendant l'exécution du workflow
|
||||
- **Score de pertinence** : résultats classés par similarité sémantique
|
||||
|
||||
### Options d'intégration
|
||||
- **Prompts système** : fournir du contexte à vos agents IA
|
||||
- **Contexte dynamique** : rechercher et inclure des informations pertinentes pendant les conversations
|
||||
- **Recherche multi-documents** : interroger l'ensemble de votre base de connaissances
|
||||
- **Recherche filtrée** : combiner avec des tags pour une récupération précise du contenu
|
||||
- **Prompts système** : fournissez du contexte à vos agents IA
|
||||
- **Contexte dynamique** : recherchez et incluez des informations pertinentes pendant les conversations
|
||||
- **Recherche multi-documents** : interrogez l'ensemble de votre base de connaissances
|
||||
- **Recherche filtrée** : combinez avec des tags pour une récupération de contenu précise
|
||||
|
||||
## Technologie de recherche vectorielle
|
||||
|
||||
Sim utilise la recherche vectorielle alimentée par [pgvector](https://github.com/pgvector/pgvector) pour comprendre le sens et le contexte de votre contenu :
|
||||
Sim utilise la recherche vectorielle propulsée par [pgvector](https://github.com/pgvector/pgvector) pour comprendre le sens et le contexte de votre contenu :
|
||||
|
||||
### Compréhension sémantique
|
||||
- **Recherche contextuelle** : trouve du contenu pertinent même lorsque les mots-clés exacts ne correspondent pas
|
||||
- **Récupération basée sur les concepts** : comprend les relations entre les idées
|
||||
- **Prise en charge multilingue** : fonctionne dans différentes langues
|
||||
- **Support multilingue** : fonctionne dans différentes langues
|
||||
- **Reconnaissance des synonymes** : trouve des termes et concepts associés
|
||||
|
||||
### Capacités de recherche
|
||||
@@ -90,26 +96,26 @@ Sim utilise la recherche vectorielle alimentée par [pgvector](https://github.co
|
||||
- **Recherche hybride** : combine la recherche vectorielle et la recherche traditionnelle par mots-clés
|
||||
- **Résultats configurables** : contrôlez le nombre et le seuil de pertinence des résultats
|
||||
|
||||
## Gestion documentaire
|
||||
## Gestion des documents
|
||||
|
||||
### Fonctionnalités d'organisation
|
||||
- **Téléchargement en masse** : téléchargez plusieurs fichiers à la fois via l'API asynchrone
|
||||
- **État de traitement** : mises à jour en temps réel sur le traitement des documents
|
||||
- **Statut de traitement** : mises à jour en temps réel sur le traitement des documents
|
||||
- **Recherche et filtrage** : trouvez rapidement des documents dans de grandes collections
|
||||
- **Suivi des métadonnées** : capture automatique des informations de fichier et des détails de traitement
|
||||
|
||||
### Sécurité et confidentialité
|
||||
- **Stockage sécurisé** : documents stockés avec une sécurité de niveau entreprise
|
||||
- **Contrôle d'accès** : autorisations basées sur l'espace de travail
|
||||
- **Contrôle d'accès** : permissions basées sur l'espace de travail
|
||||
- **Isolation du traitement** : chaque espace de travail dispose d'un traitement de documents isolé
|
||||
- **Conservation des données** : configurez les politiques de conservation des documents
|
||||
|
||||
## Premiers pas
|
||||
|
||||
1. **Accédez à votre base de connaissances** : accessible depuis la barre latérale de votre espace de travail
|
||||
1. **Accédez à votre base de connaissances** : accès depuis la barre latérale de votre espace de travail
|
||||
2. **Téléchargez des documents** : glissez-déposez ou sélectionnez des fichiers à télécharger
|
||||
3. **Surveillez le traitement** : observez le traitement et le découpage des documents
|
||||
4. **Explorez les fragments** : visualisez et modifiez le contenu traité
|
||||
5. **Ajoutez aux flux de travail** : utilisez le bloc Connaissances pour l'intégrer à vos agents IA
|
||||
3. **Surveillez le traitement** : observez le traitement et la segmentation des documents
|
||||
4. **Explorez les segments** : visualisez et modifiez le contenu traité
|
||||
5. **Ajoutez aux workflows** : utilisez le bloc Knowledge pour intégrer avec vos agents IA
|
||||
|
||||
La base de connaissances transforme vos documents statiques en une ressource intelligente et consultable que vos flux de travail IA peuvent exploiter pour des réponses plus informées et contextuelles.
|
||||
La base de connaissances transforme vos documents statiques en une ressource intelligente et consultable que vos workflows IA peuvent exploiter pour des réponses plus éclairées et contextuelles.
|
||||
@@ -38,17 +38,19 @@ Créer un nouveau contact dans Intercom avec email, external_id ou role
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `email` | string | Non | L'adresse email du contact |
|
||||
| --------- | ---- | ------------ | ----------- |
|
||||
| `role` | string | Non | Le rôle du contact. Accepte « user » ou « lead ». Par défaut « lead » si non spécifié. |
|
||||
| `email` | string | Non | L'adresse e-mail du contact |
|
||||
| `external_id` | string | Non | Un identifiant unique pour le contact fourni par le client |
|
||||
| `phone` | string | Non | Le numéro de téléphone du contact |
|
||||
| `name` | string | Non | Le nom du contact |
|
||||
| `avatar` | string | Non | Une URL d'image d'avatar pour le contact |
|
||||
| `signed_up_at` | number | Non | L'heure à laquelle l'utilisateur s'est inscrit sous forme d'horodatage Unix |
|
||||
| `last_seen_at` | number | Non | L'heure à laquelle l'utilisateur a été vu pour la dernière fois sous forme d'horodatage Unix |
|
||||
| `owner_id` | string | Non | L'identifiant d'un administrateur qui a été assigné comme propriétaire du compte du contact |
|
||||
| `unsubscribed_from_emails` | boolean | Non | Indique si le contact est désabonné des emails |
|
||||
| `custom_attributes` | string | Non | Attributs personnalisés sous forme d'objet JSON (par exemple, \{"nom_attribut": "valeur"\}) |
|
||||
| `signed_up_at` | number | Non | L'heure d'inscription de l'utilisateur sous forme d'horodatage Unix |
|
||||
| `last_seen_at` | number | Non | L'heure de dernière activité de l'utilisateur sous forme d'horodatage Unix |
|
||||
| `owner_id` | string | Non | L'identifiant d'un administrateur auquel la propriété du compte du contact a été attribuée |
|
||||
| `unsubscribed_from_emails` | boolean | Non | Indique si le contact s'est désabonné des e-mails |
|
||||
| `custom_attributes` | string | Non | Attributs personnalisés sous forme d'objet JSON (par ex., \{"nom_attribut": "valeur"\}) |
|
||||
| `company_id` | string | Non | Identifiant de l'entreprise à associer au contact lors de la création |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -81,17 +83,20 @@ Mettre à jour un contact existant dans Intercom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `contactId` | string | Oui | ID du contact à mettre à jour |
|
||||
| `email` | string | Non | Adresse e-mail du contact |
|
||||
| `phone` | string | Non | Numéro de téléphone du contact |
|
||||
| `name` | string | Non | Nom du contact |
|
||||
| `avatar` | string | Non | URL de l'image d'avatar pour le contact |
|
||||
| `signed_up_at` | number | Non | Moment où l'utilisateur s'est inscrit, en timestamp Unix |
|
||||
| `last_seen_at` | number | Non | Moment où l'utilisateur a été vu pour la dernière fois, en timestamp Unix |
|
||||
| `owner_id` | string | Non | ID d'un administrateur qui a été assigné comme propriétaire du compte du contact |
|
||||
| `unsubscribed_from_emails` | boolean | Non | Indique si le contact est désabonné des e-mails |
|
||||
| `custom_attributes` | string | Non | Attributs personnalisés sous forme d'objet JSON (par exemple, \{"nom_attribut": "valeur"\}) |
|
||||
| --------- | ---- | ------------ | ----------- |
|
||||
| `contactId` | string | Oui | Identifiant du contact à mettre à jour |
|
||||
| `role` | string | Non | Le rôle du contact. Accepte « user » ou « lead ». |
|
||||
| `external_id` | string | Non | Un identifiant unique pour le contact fourni par le client |
|
||||
| `email` | string | Non | L'adresse e-mail du contact |
|
||||
| `phone` | string | Non | Le numéro de téléphone du contact |
|
||||
| `name` | string | Non | Le nom du contact |
|
||||
| `avatar` | string | Non | Une URL d'image d'avatar pour le contact |
|
||||
| `signed_up_at` | number | Non | L'heure d'inscription de l'utilisateur sous forme d'horodatage Unix |
|
||||
| `last_seen_at` | number | Non | L'heure de dernière activité de l'utilisateur sous forme d'horodatage Unix |
|
||||
| `owner_id` | string | Non | L'identifiant d'un administrateur auquel la propriété du compte du contact a été attribuée |
|
||||
| `unsubscribed_from_emails` | boolean | Non | Indique si le contact s'est désabonné des e-mails |
|
||||
| `custom_attributes` | string | Non | Attributs personnalisés sous forme d'objet JSON (par ex., \{"nom_attribut": "valeur"\}) |
|
||||
| `company_id` | string | Non | Identifiant de l'entreprise à associer au contact |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -125,10 +130,12 @@ Rechercher des contacts dans Intercom à l'aide d'une requête
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | chaîne | Oui | Requête de recherche \(ex., \{"field":"email","operator":"=","value":"user@example.com"\}\) |
|
||||
| `per_page` | nombre | Non | Nombre de résultats par page \(max : 150\) |
|
||||
| `starting_after` | chaîne | Non | Curseur pour la pagination |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `query` | string | Oui | Requête de recherche \(par ex., \{"field":"email","operator":"=","value":"user@example.com"\}\) |
|
||||
| `per_page` | number | Non | Nombre de résultats par page \(max : 150\) |
|
||||
| `starting_after` | string | Non | Curseur pour la pagination |
|
||||
| `sort_field` | string | Non | Champ de tri \(par ex., "name", "created_at", "last_seen_at"\) |
|
||||
| `sort_order` | string | Non | Ordre de tri : "ascending" ou "descending" |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -161,15 +168,16 @@ Créer ou mettre à jour une entreprise dans Intercom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `company_id` | string | Oui | Votre identifiant unique pour l'entreprise |
|
||||
| `name` | string | Non | Le nom de l'entreprise |
|
||||
| `website` | string | Non | Le site web de l'entreprise |
|
||||
| `plan` | string | Non | Le nom du forfait de l'entreprise |
|
||||
| `plan` | string | Non | Le nom du plan de l'entreprise |
|
||||
| `size` | number | Non | Le nombre d'employés dans l'entreprise |
|
||||
| `industry` | string | Non | Le secteur d'activité de l'entreprise |
|
||||
| `monthly_spend` | number | Non | Le montant des revenus que l'entreprise génère pour votre activité. Remarque : ce champ tronque les décimales en nombres entiers (par exemple, 155,98 devient 155) |
|
||||
| `monthly_spend` | number | Non | Le chiffre d'affaires que l'entreprise génère pour votre activité. Remarque : ce champ tronque les nombres décimaux en entiers \(par ex., 155,98 devient 155\) |
|
||||
| `custom_attributes` | string | Non | Attributs personnalisés sous forme d'objet JSON |
|
||||
| `remote_created_at` | number | Non | La date de création de l'entreprise par vous sous forme d'horodatage Unix |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -205,6 +213,7 @@ Liste toutes les entreprises d'Intercom avec prise en charge de la pagination. R
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `per_page` | number | Non | Nombre de résultats par page |
|
||||
| `page` | number | Non | Numéro de page |
|
||||
| `starting_after` | string | Non | Curseur pour la pagination \(préféré à la pagination par page\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -220,9 +229,10 @@ Récupérer une seule conversation par ID depuis Intercom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Oui | ID de la conversation à récupérer |
|
||||
| `display_as` | string | Non | Définir à "plaintext" pour récupérer les messages en texte brut |
|
||||
| `display_as` | string | Non | Définir sur "plaintext" pour récupérer les messages en texte brut |
|
||||
| `include_translations` | boolean | Non | Lorsque true, les parties de la conversation seront traduites dans la langue détectée de la conversation |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -238,9 +248,11 @@ Lister toutes les conversations depuis Intercom avec prise en charge de la pagin
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | Non | Nombre de résultats par page \(max : 150\) |
|
||||
| `starting_after` | string | Non | Curseur pour la pagination |
|
||||
| `sort` | string | Non | Champ de tri \(ex., "waiting_since", "updated_at", "created_at"\) |
|
||||
| `order` | string | Non | Ordre de tri : "asc" \(croissant\) ou "desc" \(décroissant\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -256,12 +268,13 @@ Répondre à une conversation en tant qu'administrateur dans Intercom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | Oui | ID de la conversation à laquelle répondre |
|
||||
| `message_type` | string | Oui | Type de message : "comment" ou "note" |
|
||||
| `body` | string | Oui | Le corps du texte de la réponse |
|
||||
| `admin_id` | string | Non | L'ID de l'administrateur qui rédige la réponse. Si non fourni, un administrateur par défaut (Operator/Fin) sera utilisé. |
|
||||
| `attachment_urls` | string | Non | Liste d'URLs d'images séparées par des virgules (max 10) |
|
||||
| `body` | string | Oui | Corps du texte de la réponse |
|
||||
| `admin_id` | string | Non | ID de l'administrateur qui rédige la réponse. Si non fourni, un administrateur par défaut \(Operator/Fin\) sera utilisé. |
|
||||
| `attachment_urls` | string | Non | Liste d'URL d'images séparées par des virgules \(max 10\) |
|
||||
| `created_at` | number | Non | Horodatage Unix du moment où la réponse a été créée. Si non fourni, l'heure actuelle est utilisée. |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -277,10 +290,12 @@ Rechercher des conversations dans Intercom à l'aide d'une requête
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `query` | string | Oui | Requête de recherche sous forme d'objet JSON |
|
||||
| `per_page` | number | Non | Nombre de résultats par page (max : 150) |
|
||||
| `per_page` | number | Non | Nombre de résultats par page \(max : 150\) |
|
||||
| `starting_after` | string | Non | Curseur pour la pagination |
|
||||
| `sort_field` | string | Non | Champ de tri \(par exemple, "created_at", "updated_at"\) |
|
||||
| `sort_order` | string | Non | Ordre de tri : "ascending" ou "descending" |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -296,10 +311,14 @@ Créer un nouveau ticket dans Intercom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `ticket_type_id` | string | Oui | L'ID du type de ticket |
|
||||
| `contacts` | string | Oui | Tableau JSON d'identifiants de contact (par ex., \[\{"id": "contact_id"\}\]) |
|
||||
| `contacts` | string | Oui | Tableau JSON d'identifiants de contacts \(par exemple, \[\{"id": "contact_id"\}\]\) |
|
||||
| `ticket_attributes` | string | Oui | Objet JSON avec les attributs du ticket incluant _default_title_ et _default_description_ |
|
||||
| `company_id` | string | Non | ID de l'entreprise à associer au ticket |
|
||||
| `created_at` | number | Non | Horodatage Unix du moment où le ticket a été créé. Si non fourni, l'heure actuelle est utilisée. |
|
||||
| `conversation_to_link_id` | string | Non | ID d'une conversation existante à lier à ce ticket |
|
||||
| `disable_notifications` | boolean | Non | Lorsque défini sur true, supprime les notifications lors de la création du ticket |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -332,14 +351,16 @@ Créer et envoyer un nouveau message initié par l'administrateur dans Intercom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `message_type` | string | Oui | Type de message : "inapp" ou "email" |
|
||||
| `subject` | string | Non | Objet du message (pour le type email) |
|
||||
| `body` | string | Oui | Corps du message |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `message_type` | string | Oui | Type de message : "inapp" pour les messages in-app ou "email" pour les messages e-mail |
|
||||
| `template` | string | Oui | Style du modèle de message : "plain" pour texte brut ou "personal" pour style personnalisé |
|
||||
| `subject` | string | Non | Le sujet du message \(pour le type e-mail\) |
|
||||
| `body` | string | Oui | Le corps du message |
|
||||
| `from_type` | string | Oui | Type d'expéditeur : "admin" |
|
||||
| `from_id` | string | Oui | ID de l'administrateur qui envoie le message |
|
||||
| `from_id` | string | Oui | L'ID de l'administrateur qui envoie le message |
|
||||
| `to_type` | string | Oui | Type de destinataire : "contact" |
|
||||
| `to_id` | string | Oui | ID du contact qui reçoit le message |
|
||||
| `to_id` | string | Oui | L'ID du contact qui reçoit le message |
|
||||
| `created_at` | number | Non | Horodatage Unix du moment où le message a été créé. Si non fourni, l'heure actuelle est utilisée. |
|
||||
|
||||
#### Sortie
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Mémoire
|
||||
description: Ajouter un stockage de mémoire
|
||||
description: Ajouter un magasin de mémoire
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -12,53 +12,50 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrer la mémoire dans le flux de travail. Peut ajouter, obtenir une mémoire, obtenir toutes les mémoires et supprimer des mémoires.
|
||||
Intégrer la mémoire dans le flux de travail. Permet d'ajouter, d'obtenir une mémoire, d'obtenir toutes les mémoires et de supprimer des mémoires.
|
||||
|
||||
## Outils
|
||||
|
||||
### `memory_add`
|
||||
|
||||
Ajoutez une nouvelle mémoire à la base de données ou complétez une mémoire existante avec le même ID.
|
||||
Ajouter une nouvelle mémoire à la base de données ou ajouter à une mémoire existante avec le même ID.
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation (par ex., user-123, session-abc). Si une mémoire avec cet identifiant de conversation existe déjà pour ce bloc, le nouveau message y sera ajouté. |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation \(par ex., user-123, session-abc\). Si une mémoire avec cet identifiant existe déjà, le nouveau message y sera ajouté. |
|
||||
| `id` | chaîne | Non | Paramètre hérité pour l'identifiant de conversation. Utilisez conversationId à la place. Fourni pour la rétrocompatibilité. |
|
||||
| `role` | chaîne | Oui | Rôle pour la mémoire de l'agent (user, assistant, ou system) |
|
||||
| `role` | chaîne | Oui | Rôle pour la mémoire de l'agent \(user, assistant ou system\) |
|
||||
| `content` | chaîne | Oui | Contenu pour la mémoire de l'agent |
|
||||
| `blockId` | chaîne | Non | ID de bloc optionnel. Si non fourni, utilise l'ID du bloc actuel du contexte d'exécution, ou par défaut "default". |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indique si la mémoire a été ajoutée avec succès |
|
||||
| `memories` | array | Tableau d'objets de mémoire incluant la nouvelle mémoire ou celle mise à jour |
|
||||
| `error` | string | Message d'erreur si l'opération a échoué |
|
||||
| `success` | booléen | Indique si la mémoire a été ajoutée avec succès |
|
||||
| `memories` | tableau | Tableau d'objets mémoire incluant la mémoire nouvelle ou mise à jour |
|
||||
| `error` | chaîne | Message d'erreur en cas d'échec de l'opération |
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Récupérer la mémoire par conversationId, blockId, blockName, ou une combinaison. Renvoie toutes les mémoires correspondantes.
|
||||
Récupérer la mémoire par conversationId. Renvoie les mémoires correspondantes.
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation (par ex., user-123, session-abc). Si fourni seul, renvoie toutes les mémoires pour cette conversation à travers tous les blocs. |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation \(par ex., user-123, session-abc\). Renvoie les mémoires pour cette conversation. |
|
||||
| `id` | chaîne | Non | Paramètre hérité pour l'identifiant de conversation. Utilisez conversationId à la place. Fourni pour la rétrocompatibilité. |
|
||||
| `blockId` | chaîne | Non | Identifiant de bloc. Si fourni seul, renvoie toutes les mémoires pour ce bloc à travers toutes les conversations. Si fourni avec conversationId, renvoie les mémoires pour cette conversation spécifique dans ce bloc. |
|
||||
| `blockName` | chaîne | Non | Nom du bloc. Alternative à blockId. Si fourni seul, renvoie toutes les mémoires pour les blocs avec ce nom. Si fourni avec conversationId, renvoie les mémoires pour cette conversation dans les blocs avec ce nom. |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | booléen | Indique si la mémoire a été récupérée avec succès |
|
||||
| `memories` | tableau | Tableau d'objets de mémoire avec les champs conversationId, blockId, blockName et data |
|
||||
| `memories` | tableau | Tableau d'objets mémoire avec les champs conversationId et data |
|
||||
| `message` | chaîne | Message de succès ou d'erreur |
|
||||
| `error` | chaîne | Message d'erreur si l'opération a échoué |
|
||||
| `error` | chaîne | Message d'erreur en cas d'échec |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -74,30 +71,28 @@ Récupérer toutes les mémoires de la base de données
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | booléen | Indique si toutes les mémoires ont été récupérées avec succès |
|
||||
| `memories` | tableau | Tableau de tous les objets de mémoire avec les champs key, conversationId, blockId, blockName et data |
|
||||
| `memories` | tableau | Tableau de tous les objets mémoire avec les champs key, conversationId et data |
|
||||
| `message` | chaîne | Message de succès ou d'erreur |
|
||||
| `error` | chaîne | Message d'erreur si l'opération a échoué |
|
||||
| `error` | chaîne | Message d'erreur en cas d'échec |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Supprimer des mémoires par conversationId, blockId, blockName, ou une combinaison. Prend en charge la suppression en masse.
|
||||
Supprimer les mémoires par conversationId.
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation \(par exemple, user-123, session-abc\). Si fourni seul, supprime toutes les mémoires pour cette conversation dans tous les blocs. |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation (par ex., user-123, session-abc). Supprime toutes les mémoires pour cette conversation. |
|
||||
| `id` | chaîne | Non | Paramètre hérité pour l'identifiant de conversation. Utilisez conversationId à la place. Fourni pour la rétrocompatibilité. |
|
||||
| `blockId` | chaîne | Non | Identifiant de bloc. Si fourni seul, supprime toutes les mémoires pour ce bloc dans toutes les conversations. Si fourni avec conversationId, supprime les mémoires pour cette conversation spécifique dans ce bloc. |
|
||||
| `blockName` | chaîne | Non | Nom du bloc. Alternative à blockId. Si fourni seul, supprime toutes les mémoires pour les blocs avec ce nom. Si fourni avec conversationId, supprime les mémoires pour cette conversation dans les blocs avec ce nom. |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | booléen | Indique si le souvenir a été supprimé avec succès |
|
||||
| `success` | booléen | Indique si la mémoire a été supprimée avec succès |
|
||||
| `message` | chaîne | Message de succès ou d'erreur |
|
||||
| `error` | chaîne | Message d'erreur si l'opération a échoué |
|
||||
| `error` | chaîne | Message d'erreur en cas d'échec |
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -47,12 +47,13 @@ Interroger des données d'une table Supabase
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | chaîne | Oui | Votre ID de projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | chaîne | Oui | Le nom de la table Supabase à interroger |
|
||||
| `filter` | chaîne | Non | Filtre PostgREST \(ex. : "id=eq.123"\) |
|
||||
| `orderBy` | chaîne | Non | Colonne pour le tri \(ajoutez DESC pour décroissant\) |
|
||||
| `limit` | nombre | Non | Nombre maximum de lignes à retourner |
|
||||
| `apiKey` | chaîne | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à interroger |
|
||||
| `schema` | string | Non | Schéma de base de données à interroger \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `filter` | string | Non | Filtre PostgREST \(ex. : "id=eq.123"\) |
|
||||
| `orderBy` | string | Non | Colonne pour le tri \(ajoutez DESC pour l'ordre décroissant\) |
|
||||
| `limit` | number | Non | Nombre maximum de lignes à retourner |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -69,10 +70,11 @@ Insérer des données dans une table Supabase
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | chaîne | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `table` | chaîne | Oui | Le nom de la table Supabase dans laquelle insérer des données |
|
||||
| `data` | tableau | Oui | Les données à insérer (tableau d'objets ou un seul objet) |
|
||||
| `apiKey` | chaîne | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase dans laquelle insérer des données |
|
||||
| `schema` | string | Non | Schéma de base de données dans lequel insérer \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `data` | array | Oui | Les données à insérer \(tableau d'objets ou un seul objet\) |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -89,9 +91,10 @@ Obtenir une seule ligne d'une table Supabase selon des critères de filtrage
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à interroger |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour trouver la ligne spécifique (ex. : "id=eq.123") |
|
||||
| `schema` | string | Non | Schéma de base de données à interroger \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour trouver la ligne spécifique \(ex. : "id=eq.123"\) |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
@@ -109,9 +112,10 @@ Mettre à jour des lignes dans une table Supabase selon des critères de filtrag
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à mettre à jour |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour identifier les lignes à mettre à jour (ex. : "id=eq.123") |
|
||||
| `schema` | string | Non | Schéma de base de données dans lequel mettre à jour \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour identifier les lignes à mettre à jour \(ex. : "id=eq.123"\) |
|
||||
| `data` | object | Oui | Données à mettre à jour dans les lignes correspondantes |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
@@ -130,9 +134,10 @@ Supprimer des lignes d'une table Supabase selon des critères de filtrage
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase d'où supprimer des lignes |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour identifier les lignes à supprimer (ex. : "id=eq.123") |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à partir de laquelle supprimer |
|
||||
| `schema` | string | Non | Schéma de base de données à partir duquel supprimer \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour identifier les lignes à supprimer \(ex. : "id=eq.123"\) |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
@@ -150,10 +155,11 @@ Insérer ou mettre à jour des données dans une table Supabase (opération upse
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | chaîne | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `table` | chaîne | Oui | Le nom de la table Supabase dans laquelle upserter des données |
|
||||
| `data` | tableau | Oui | Les données à upserter (insérer ou mettre à jour) - tableau d'objets ou un seul objet |
|
||||
| `apiKey` | chaîne | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase dans laquelle effectuer l'upsert |
|
||||
| `schema` | string | Non | Schéma de base de données dans lequel effectuer l'upsert \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `data` | array | Oui | Les données à insérer ou mettre à jour \(upsert\) - tableau d'objets ou un seul objet |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -170,11 +176,12 @@ Compter les lignes dans une table Supabase
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | chaîne | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `table` | chaîne | Oui | Le nom de la table Supabase dont compter les lignes |
|
||||
| `filter` | chaîne | Non | Filtre PostgREST (ex. : "status=eq.active") |
|
||||
| `countType` | chaîne | Non | Type de comptage : exact, planned ou estimated (par défaut : exact) |
|
||||
| `apiKey` | chaîne | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase dont compter les lignes |
|
||||
| `schema` | string | Non | Schéma de base de données à partir duquel compter \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `filter` | string | Non | Filtre PostgREST \(ex. : "status=eq.active"\) |
|
||||
| `countType` | string | Non | Type de comptage : exact, planned ou estimated \(par défaut : exact\) |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -191,14 +198,15 @@ Effectuer une recherche en texte intégral sur une table Supabase
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | chaîne | Oui | L'ID de votre projet Supabase (ex. : jdrkgepadsdopsntdlom) |
|
||||
| `table` | chaîne | Oui | Le nom de la table Supabase à rechercher |
|
||||
| `column` | chaîne | Oui | La colonne dans laquelle rechercher |
|
||||
| `query` | chaîne | Oui | La requête de recherche |
|
||||
| `searchType` | chaîne | Non | Type de recherche : plain, phrase ou websearch (par défaut : websearch) |
|
||||
| `language` | chaîne | Non | Langue pour la configuration de recherche textuelle (par défaut : english) |
|
||||
| `limit` | nombre | Non | Nombre maximum de lignes à retourner |
|
||||
| `apiKey` | chaîne | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à rechercher |
|
||||
| `schema` | string | Non | Schéma de base de données dans lequel rechercher \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `column` | string | Oui | La colonne dans laquelle rechercher |
|
||||
| `query` | string | Oui | La requête de recherche |
|
||||
| `searchType` | string | Non | Type de recherche : plain, phrase ou websearch \(par défaut : websearch\) |
|
||||
| `language` | string | Non | Langue pour la configuration de recherche textuelle \(par défaut : english\) |
|
||||
| `limit` | number | Non | Nombre maximum de lignes à retourner |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
#### Sortie
|
||||
|
||||
|
||||
@@ -49,40 +49,40 @@ AIブロックを使用するワークフローでは、ログで詳細なコス
|
||||
|
||||
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
|
||||
<Tab>
|
||||
**ホステッドモデル** - Simは2.5倍の価格倍率でAPIキーを提供します:
|
||||
**ホステッドモデル** - Simは2倍の価格乗数でAPIキーを提供します:
|
||||
|
||||
**OpenAI**
|
||||
| モデル | 基本価格(入力/出力) | ホステッド価格(入力/出力) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-5.1 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.63 / $5.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.13 / $1.00 |
|
||||
| GPT-4o | $2.50 / $10.00 | $6.25 / $25.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $1.00 / $4.00 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.25 / $1.00 |
|
||||
| o1 | $15.00 / $60.00 | $37.50 / $150.00 |
|
||||
| o3 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.75 / $11.00 |
|
||||
| GPT-5.1 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.50 / $4.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.10 / $0.80 |
|
||||
| GPT-4o | $2.50 / $10.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $0.80 / $3.20 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.20 / $0.80 |
|
||||
| o1 | $15.00 / $60.00 | $30.00 / $120.00 |
|
||||
| o3 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.20 / $8.80 |
|
||||
|
||||
**Anthropic**
|
||||
| モデル | 基本価格(入力/出力) | ホステッド価格(入力/出力) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $12.50 / $62.50 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $37.50 / $187.50 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.50 / $12.50 |
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $10.00 / $50.00 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $30.00 / $150.00 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.00 / $10.00 |
|
||||
|
||||
**Google**
|
||||
| モデル | 基本価格(入力/出力) | ホステッド価格(入力/出力) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $5.00 / $30.00 |
|
||||
| Gemini 2.5 Pro | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 2.5 Flash | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $4.00 / $24.00 |
|
||||
| Gemini 2.5 Pro | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| Gemini 2.5 Flash | $0.30 / $2.50 | $0.60 / $5.00 |
|
||||
|
||||
*2.5倍の倍率はインフラストラクチャとAPI管理コストをカバーしています。*
|
||||
*2倍の乗数は、インフラストラクチャとAPI管理コストをカバーします。*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
@@ -185,12 +185,12 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
||||
|
||||
サブスクリプションプランによって使用制限が異なります:
|
||||
|
||||
| プラン | 月間使用制限 | レート制限(分あたり) |
|
||||
| プラン | 月間使用制限 | レート制限(毎分) |
|
||||
|------|-------------------|-------------------------|
|
||||
| **無料** | $10 | 5同期、10非同期 |
|
||||
| **プロ** | $100 | 10同期、50非同期 |
|
||||
| **チーム** | $500(プール) | 50同期、100非同期 |
|
||||
| **エンタープライズ** | カスタム | カスタム |
|
||||
| **Free** | $20 | 同期5、非同期10 |
|
||||
| **Pro** | $100 | 同期10、非同期50 |
|
||||
| **Team** | $500(プール) | 同期50、非同期100 |
|
||||
| **Enterprise** | カスタム | カスタム |
|
||||
|
||||
## 課金モデル
|
||||
|
||||
|
||||
@@ -34,81 +34,87 @@ SimはPDF、Word(DOC/DOCX)、プレーンテキスト(TXT)、Markdown(
|
||||
<Image src="/static/knowledgebase/knowledgebase.png" alt="処理されたコンテンツを表示するドキュメントチャンクビュー" width={800} height={500} />
|
||||
|
||||
### チャンク設定
|
||||
- **デフォルトチャンクサイズ**: 1,024文字
|
||||
- **設定可能範囲**: チャンクあたり100〜4,000文字
|
||||
- **スマートオーバーラップ**: コンテキスト保持のためデフォルトで200文字
|
||||
- **階層的分割**: 文書構造(セクション、段落、文)を尊重
|
||||
|
||||
ナレッジベースを作成する際、ドキュメントをチャンクに分割する方法を設定できます。
|
||||
|
||||
| 設定 | 単位 | デフォルト | 範囲 | 説明 |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **最大チャンクサイズ** | トークン | 1,024 | 100-4,000 | 各チャンクの最大サイズ(1トークン ≈ 4文字) |
|
||||
| **最小チャンクサイズ** | 文字 | 1 | 1-2,000 | 小さな断片を避けるための最小チャンクサイズ |
|
||||
| **オーバーラップ** | 文字 | 200 | 0-500 | 連続するチャンク間のコンテキストオーバーラップ |
|
||||
|
||||
- **階層的分割**: ドキュメント構造(セクション、段落、文)を尊重
|
||||
|
||||
### 編集機能
|
||||
- **チャンク内容の編集**: 個々のチャンクのテキスト内容を修正
|
||||
- **チャンク境界の調整**: 必要に応じてチャンクの結合や分割
|
||||
- **チャンクコンテンツの編集**: 個々のチャンクのテキストコンテンツを変更
|
||||
- **チャンク境界の調整**: 必要に応じてチャンクを結合または分割
|
||||
- **メタデータの追加**: 追加のコンテキストでチャンクを強化
|
||||
- **一括操作**: 複数のチャンクを効率的に管理
|
||||
|
||||
## 高度なPDF処理
|
||||
|
||||
PDFドキュメントについて、Simは強化された処理機能を提供します:
|
||||
PDFドキュメントに対して、Simは強化された処理機能を提供します。
|
||||
|
||||
### OCRサポート
|
||||
Azureまたは[Mistral OCR](https://docs.mistral.ai/ocr/)で構成されている場合:
|
||||
- **スキャンされたドキュメント処理**: 画像ベースのPDFからテキストを抽出
|
||||
- **混合コンテンツ処理**: テキストと画像の両方を含むPDFを処理
|
||||
- **高精度**: 高度なAIモデルが正確なテキスト抽出を保証
|
||||
Azureまたは[Mistral OCR](https://docs.mistral.ai/ocr/)で設定されている場合:
|
||||
- **スキャンされたドキュメントの処理**: 画像ベースのPDFからテキストを抽出
|
||||
- **混合コンテンツの処理**: テキストと画像の両方を含むPDFを処理
|
||||
- **高精度**: 高度なAIモデルにより正確なテキスト抽出を保証
|
||||
|
||||
## ワークフローでのナレッジブロックの使用
|
||||
|
||||
ドキュメントが処理されると、ナレッジブロックを通じてAIワークフローで使用できるようになります。これにより検索拡張生成(RAG)が可能になり、AIエージェントがドキュメントの内容にアクセスして推論し、より正確でコンテキストに沿った回答を提供できます。
|
||||
ドキュメントが処理されると、ナレッジブロックを通じてAIワークフローで使用できます。これにより検索拡張生成(RAG)が可能になり、AIエージェントがドキュメントコンテンツにアクセスして推論し、より正確でコンテキストに沿った応答を提供できます。
|
||||
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="ワークフローでのナレッジブロックの使用" width={800} height={500} />
|
||||
|
||||
### ナレッジブロックの機能
|
||||
- **意味検索**: 自然言語クエリを使用して関連コンテンツを検索
|
||||
- **コンテキスト統合**: エージェントプロンプトに関連チャンクを自動的に含める
|
||||
- **動的検索**: ワークフロー実行中にリアルタイムで検索が行われる
|
||||
- **関連性スコアリング**: 意味的類似性によって結果がランク付け
|
||||
- **セマンティック検索**: 自然言語クエリを使用して関連コンテンツを検索
|
||||
- **コンテキスト統合**: 関連するチャンクをエージェントプロンプトに自動的に含める
|
||||
- **動的検索**: ワークフロー実行中にリアルタイムで検索を実行
|
||||
- **関連性スコアリング**: セマンティック類似度によって結果をランク付け
|
||||
|
||||
### 統合オプション
|
||||
- **システムプロンプト**: AIエージェントにコンテキストを提供
|
||||
- **動的コンテキスト**: 会話中に関連情報を検索して含める
|
||||
- **複数ドキュメント検索**: ナレッジベース全体を横断して検索
|
||||
- **フィルター検索**: タグと組み合わせて正確なコンテンツ検索
|
||||
- **複数ドキュメント検索**: ナレッジベース全体をクエリ
|
||||
- **フィルター検索**: タグと組み合わせて正確なコンテンツ取得
|
||||
|
||||
## ベクトル検索技術
|
||||
|
||||
Simは[pgvector](https://github.com/pgvector/pgvector)を活用したベクトル検索を使用して、コンテンツの意味とコンテキストを理解します:
|
||||
Simは[pgvector](https://github.com/pgvector/pgvector)を活用したベクトル検索により、コンテンツの意味とコンテキストを理解します。
|
||||
|
||||
### 意味的理解
|
||||
- **コンテキスト検索**:正確なキーワードが一致しなくても関連コンテンツを見つける
|
||||
- **概念ベースの検索**:アイデア間の関係性を理解
|
||||
- **多言語サポート**:異なる言語間で機能
|
||||
- **同義語認識**:関連する用語や概念を見つける
|
||||
### セマンティック理解
|
||||
- **コンテキスト検索**: 正確なキーワードが一致しない場合でも関連コンテンツを検索
|
||||
- **概念ベースの取得**: アイデア間の関係を理解
|
||||
- **多言語サポート**: 異なる言語間で動作
|
||||
- **同義語認識**: 関連する用語と概念を検索
|
||||
|
||||
### 検索機能
|
||||
- **自然言語クエリ**:平易な日本語で質問できる
|
||||
- **類似性検索**:概念的に類似したコンテンツを見つける
|
||||
- **ハイブリッド検索**:ベクトル検索と従来のキーワード検索を組み合わせる
|
||||
- **結果の設定**:結果の数と関連性の閾値を制御
|
||||
- **自然言語クエリ**: 平易な英語で質問
|
||||
- **類似検索**: 概念的に類似したコンテンツを検索
|
||||
- **ハイブリッド検索**: ベクトル検索と従来のキーワード検索を組み合わせ
|
||||
- **設定可能な結果**: 結果の数と関連性のしきい値を制御
|
||||
|
||||
## ドキュメント管理
|
||||
|
||||
### 整理機能
|
||||
- **一括アップロード**:非同期APIを通じて複数のファイルを一度にアップロード
|
||||
- **処理状況**:ドキュメント処理のリアルタイム更新
|
||||
- **検索とフィルタリング**:大規模なコレクションからドキュメントを素早く見つける
|
||||
- **メタデータ追跡**:ファイル情報と処理詳細の自動キャプチャ
|
||||
- **一括アップロード**: 非同期API経由で複数ファイルを一度にアップロード
|
||||
- **処理ステータス**: ドキュメント処理のリアルタイム更新
|
||||
- **検索とフィルター**: 大規模なコレクション内でドキュメントを素早く検索
|
||||
- **メタデータ追跡**: ファイル情報と処理詳細の自動キャプチャ
|
||||
|
||||
### セキュリティとプライバシー
|
||||
- **安全なストレージ**:エンタープライズグレードのセキュリティでドキュメントを保存
|
||||
- **アクセス制御**:ワークスペースベースの権限
|
||||
- **処理の分離**:各ワークスペースは分離されたドキュメント処理を持つ
|
||||
- **データ保持**:ドキュメント保持ポリシーの設定
|
||||
- **安全なストレージ**: エンタープライズグレードのセキュリティでドキュメントを保存
|
||||
- **アクセス制御**: ワークスペースベースの権限
|
||||
- **処理の分離**: 各ワークスペースは分離されたドキュメント処理を実施
|
||||
- **データ保持**: ドキュメント保持ポリシーを設定
|
||||
|
||||
## はじめに
|
||||
|
||||
1. **ナレッジベースに移動**:ワークスペースのサイドバーからアクセス
|
||||
2. **ドキュメントのアップロード**:ドラッグ&ドロップまたはファイルを選択してアップロード
|
||||
3. **処理の監視**:ドキュメントが処理されチャンク化される過程を確認
|
||||
4. **チャンクの探索**:処理されたコンテンツを表示・編集
|
||||
5. **ワークフローへの追加**:ナレッジブロックを使用してAIエージェントと統合
|
||||
1. **ナレッジベースに移動**: ワークスペースのサイドバーからアクセス
|
||||
2. **ドキュメントをアップロード**: ドラッグアンドドロップまたはファイルを選択してアップロード
|
||||
3. **処理を監視**: ドキュメントが処理され、チャンク化される様子を確認
|
||||
4. **チャンクを探索**: 処理されたコンテンツを表示および編集
|
||||
5. **ワークフローに追加**: Knowledgeブロックを使用してAIエージェントと統合
|
||||
|
||||
ナレッジベースは静的なドキュメントを、AIワークフローがより情報に基づいた文脈的な応答のために活用できる、インテリジェントで検索可能なリソースに変換します。
|
||||
ナレッジベースは、静的なドキュメントをインテリジェントで検索可能なリソースに変換し、AIワークフローがより情報に基づいた文脈に応じた応答を活用できるようにします。
|
||||
@@ -38,16 +38,18 @@ Intercomをワークフローに統合します。連絡先の作成、取得、
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `role` | string | いいえ | 連絡先の役割。「user」または「lead」を指定可能。未指定の場合は「lead」がデフォルト |
|
||||
| `email` | string | いいえ | 連絡先のメールアドレス |
|
||||
| `external_id` | string | いいえ | クライアントが提供する連絡先の一意の識別子 |
|
||||
| `phone` | string | いいえ | 連絡先の電話番号 |
|
||||
| `name` | string | いいえ | 連絡先の名前 |
|
||||
| `avatar` | string | いいえ | 連絡先のアバター画像URL |
|
||||
| `signed_up_at` | number | いいえ | ユーザーが登録した時間(Unixタイムスタンプ) |
|
||||
| `last_seen_at` | number | いいえ | ユーザーが最後に確認された時間(Unixタイムスタンプ) |
|
||||
| `signed_up_at` | number | いいえ | ユーザーが登録した時刻(Unixタイムスタンプ) |
|
||||
| `last_seen_at` | number | いいえ | ユーザーが最後に確認された時刻(Unixタイムスタンプ) |
|
||||
| `owner_id` | string | いいえ | 連絡先のアカウント所有権が割り当てられた管理者のID |
|
||||
| `unsubscribed_from_emails` | boolean | いいえ | 連絡先がメールの配信を解除しているかどうか |
|
||||
| `custom_attributes` | string | いいえ | JSONオブジェクトとしてのカスタム属性(例:\{"attribute_name": "value"\}) |
|
||||
| `unsubscribed_from_emails` | boolean | いいえ | 連絡先がメールの配信停止をしているかどうか |
|
||||
| `custom_attributes` | string | いいえ | カスタム属性(JSONオブジェクト形式、例:\{"attribute_name": "value"\}) |
|
||||
| `company_id` | string | いいえ | 作成時に連絡先を関連付ける会社ID |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -82,15 +84,18 @@ Intercomの既存の連絡先を更新する
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | はい | 更新する連絡先ID |
|
||||
| `role` | string | いいえ | 連絡先の役割。「user」または「lead」を指定可能 |
|
||||
| `external_id` | string | いいえ | クライアントが提供する連絡先の一意の識別子 |
|
||||
| `email` | string | いいえ | 連絡先のメールアドレス |
|
||||
| `phone` | string | いいえ | 連絡先の電話番号 |
|
||||
| `name` | string | いいえ | 連絡先の名前 |
|
||||
| `avatar` | string | いいえ | 連絡先のアバター画像URL |
|
||||
| `signed_up_at` | number | いいえ | ユーザーが登録した時間(Unixタイムスタンプ) |
|
||||
| `last_seen_at` | number | いいえ | ユーザーが最後に確認された時間(Unixタイムスタンプ) |
|
||||
| `signed_up_at` | number | いいえ | ユーザーが登録した時刻(Unixタイムスタンプ) |
|
||||
| `last_seen_at` | number | いいえ | ユーザーが最後に確認された時刻(Unixタイムスタンプ) |
|
||||
| `owner_id` | string | いいえ | 連絡先のアカウント所有権が割り当てられた管理者のID |
|
||||
| `unsubscribed_from_emails` | boolean | いいえ | 連絡先がメールの配信を解除しているかどうか |
|
||||
| `custom_attributes` | string | いいえ | JSONオブジェクトとしてのカスタム属性(例:\{"attribute_name": "value"\}) |
|
||||
| `unsubscribed_from_emails` | boolean | いいえ | 連絡先がメールの配信停止をしているかどうか |
|
||||
| `custom_attributes` | string | いいえ | カスタム属性(JSONオブジェクト形式、例:\{"attribute_name": "value"\}) |
|
||||
| `company_id` | string | いいえ | 連絡先を関連付ける会社ID |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -128,6 +133,8 @@ Intercomの既存の連絡先を更新する
|
||||
| `query` | string | はい | 検索クエリ(例:\{"field":"email","operator":"=","value":"user@example.com"\}) |
|
||||
| `per_page` | number | いいえ | ページあたりの結果数(最大:150) |
|
||||
| `starting_after` | string | いいえ | ページネーション用カーソル |
|
||||
| `sort_field` | string | いいえ | ソート対象のフィールド(例:"name"、"created_at"、"last_seen_at") |
|
||||
| `sort_order` | string | いいえ | ソート順:"ascending"(昇順)または"descending"(降順) |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -166,9 +173,10 @@ Intercomで企業を作成または更新する
|
||||
| `website` | string | いいえ | 企業のウェブサイト |
|
||||
| `plan` | string | いいえ | 企業のプラン名 |
|
||||
| `size` | number | いいえ | 企業の従業員数 |
|
||||
| `industry` | string | いいえ | 企業が事業を展開している業界 |
|
||||
| `monthly_spend` | number | いいえ | 企業があなたのビジネスにもたらす収益額。注:このフィールドは小数点以下を切り捨てて整数にします(例:155.98は155になります) |
|
||||
| `industry` | string | いいえ | 企業が事業を展開する業界 |
|
||||
| `monthly_spend` | number | いいえ | 企業があなたのビジネスに生み出す収益額。注:このフィールドは小数点以下を切り捨てて整数にします(例:155.98は155になります) |
|
||||
| `custom_attributes` | string | いいえ | JSONオブジェクトとしてのカスタム属性 |
|
||||
| `remote_created_at` | number | いいえ | あなたが企業を作成した時刻(Unixタイムスタンプ) |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -204,6 +212,7 @@ IDによってIntercomから単一の企業を取得する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | いいえ | ページあたりの結果数 |
|
||||
| `page` | number | いいえ | ページ番号 |
|
||||
| `starting_after` | string | いいえ | ページネーション用カーソル(ページベースのページネーションより推奨) |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -221,7 +230,8 @@ IDによりIntercomから単一の会話を取得
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | はい | 取得する会話ID |
|
||||
| `display_as` | string | いいえ | プレーンテキストでメッセージを取得するには「plaintext」に設定 |
|
||||
| `display_as` | string | いいえ | プレーンテキストでメッセージを取得する場合は「plaintext」に設定 |
|
||||
| `include_translations` | boolean | いいえ | trueの場合、会話パーツは会話の検出言語に翻訳されます |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -240,6 +250,8 @@ IDによりIntercomから単一の会話を取得
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | number | いいえ | ページあたりの結果数(最大:150) |
|
||||
| `starting_after` | string | いいえ | ページネーション用カーソル |
|
||||
| `sort` | string | いいえ | ソートするフィールド(例:「waiting_since」、「updated_at」、「created_at」) |
|
||||
| `order` | string | いいえ | ソート順:「asc」(昇順)または「desc」(降順) |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -258,9 +270,10 @@ IDによりIntercomから単一の会話を取得
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | はい | 返信する会話ID |
|
||||
| `message_type` | string | はい | メッセージタイプ:「comment」または「note」 |
|
||||
| `body` | string | はい | 返信の本文テキスト |
|
||||
| `admin_id` | string | いいえ | 返信を作成する管理者のID。提供されない場合、デフォルトの管理者(オペレーター/Fin)が使用されます。 |
|
||||
| `attachment_urls` | string | いいえ | 画像URLのカンマ区切りリスト(最大10件) |
|
||||
| `body` | string | はい | 返信のテキスト本文 |
|
||||
| `admin_id` | string | いいえ | 返信を作成する管理者のID。指定しない場合、デフォルトの管理者(Operator/Fin)が使用されます。 |
|
||||
| `attachment_urls` | string | いいえ | カンマ区切りの画像URL一覧(最大10個) |
|
||||
| `created_at` | number | いいえ | 返信が作成されたときのUnixタイムスタンプ。指定しない場合、現在時刻が使用されます。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -279,7 +292,9 @@ IDによりIntercomから単一の会話を取得
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | はい | JSONオブジェクトとしての検索クエリ |
|
||||
| `per_page` | number | いいえ | ページあたりの結果数(最大:150) |
|
||||
| `starting_after` | string | いいえ | ページネーション用のカーソル |
|
||||
| `starting_after` | string | いいえ | ページネーション用カーソル |
|
||||
| `sort_field` | string | いいえ | ソートするフィールド(例:「created_at」、「updated_at」) |
|
||||
| `sort_order` | string | いいえ | ソート順:「ascending」または「descending」 |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -299,6 +314,10 @@ Intercomで新しいチケットを作成する
|
||||
| `ticket_type_id` | string | はい | チケットタイプのID |
|
||||
| `contacts` | string | はい | 連絡先識別子のJSON配列(例:\[\{"id": "contact_id"\}\]) |
|
||||
| `ticket_attributes` | string | はい | _default_title_と_default_description_を含むチケット属性のJSONオブジェクト |
|
||||
| `company_id` | string | いいえ | チケットに関連付ける会社ID |
|
||||
| `created_at` | number | いいえ | チケットが作成された時間(Unixタイムスタンプ)。提供されない場合、現在時刻が使用されます。 |
|
||||
| `conversation_to_link_id` | string | いいえ | このチケットにリンクする既存の会話のID |
|
||||
| `disable_notifications` | boolean | いいえ | trueの場合、チケット作成時の通知を抑制します |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -332,13 +351,15 @@ Intercomで管理者が開始した新しいメッセージを作成して送信
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `message_type` | string | はい | メッセージタイプ:「inapp」または「email」 |
|
||||
| `message_type` | string | はい | メッセージタイプ:アプリ内メッセージの場合は「inapp」、メールメッセージの場合は「email」 |
|
||||
| `template` | string | はい | メッセージテンプレートスタイル:プレーンテキストの場合は「plain」、パーソナライズスタイルの場合は「personal」 |
|
||||
| `subject` | string | いいえ | メッセージの件名(emailタイプの場合) |
|
||||
| `body` | string | はい | メッセージの本文 |
|
||||
| `from_type` | string | はい | 送信者タイプ:「admin」 |
|
||||
| `from_type` | string | はい | 送信者タイプ:「admin」 |
|
||||
| `from_id` | string | はい | メッセージを送信する管理者のID |
|
||||
| `to_type` | string | はい | 受信者タイプ:「contact」 |
|
||||
| `to_type` | string | はい | 受信者タイプ:「contact」 |
|
||||
| `to_id` | string | はい | メッセージを受信する連絡先のID |
|
||||
| `created_at` | number | いいえ | メッセージが作成された時間(Unixタイムスタンプ)。提供されない場合、現在時刻が使用されます。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: メモリー
|
||||
description: メモリーストアを追加
|
||||
description: メモリストアを追加
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -12,23 +12,22 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
## 使用方法
|
||||
|
||||
ワークフローにメモリーを統合します。メモリーの追加、取得、すべてのメモリーの取得、メモリーの削除が可能です。
|
||||
ワークフローにメモリを統合します。メモリの追加、取得、全メモリの取得、削除が可能です。
|
||||
|
||||
## ツール
|
||||
|
||||
### `memory_add`
|
||||
|
||||
新しいメモリーをデータベースに追加するか、同じIDの既存のメモリーに追加します。
|
||||
データベースに新しいメモリを追加するか、同じIDを持つ既存のメモリに追記します。
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。このブロックに対してこの会話IDのメモリがすでに存在する場合、新しいメッセージはそれに追加されます。 |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。この会話IDを持つメモリが既に存在する場合、新しいメッセージが追記されます。 |
|
||||
| `id` | string | いいえ | 会話識別子のレガシーパラメータ。代わりにconversationIdを使用してください。後方互換性のために提供されています。 |
|
||||
| `role` | string | はい | エージェントメモリの役割(user、assistant、またはsystem) |
|
||||
| `role` | string | はい | エージェントメモリのロール(user、assistant、またはsystem) |
|
||||
| `content` | string | はい | エージェントメモリのコンテンツ |
|
||||
| `blockId` | string | いいえ | オプションのブロックID。提供されない場合、実行コンテキストから現在のブロックIDを使用するか、デフォルトで「default」になります。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -40,29 +39,27 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
### `memory_get`
|
||||
|
||||
conversationId、blockId、blockName、またはそれらの組み合わせによってメモリを取得します。一致するすべてのメモリを返します。
|
||||
会話IDによってメモリを取得します。一致するメモリを返します。
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。単独で提供された場合、すべてのブロックにわたるこの会話のすべてのメモリを返します。 |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。この会話のメモリを返します。 |
|
||||
| `id` | string | いいえ | 会話識別子のレガシーパラメータ。代わりにconversationIdを使用してください。後方互換性のために提供されています。 |
|
||||
| `blockId` | string | いいえ | ブロック識別子。単独で提供された場合、すべての会話にわたるこのブロックのすべてのメモリを返します。conversationIdと一緒に提供された場合、そのブロック内の特定の会話のメモリを返します。 |
|
||||
| `blockName` | string | いいえ | ブロック名。blockIdの代替。単独で提供された場合、この名前を持つブロックのすべてのメモリを返します。conversationIdと一緒に提供された場合、この名前を持つブロック内のその会話のメモリを返します。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | メモリが正常に取得されたかどうか |
|
||||
| `memories` | array | conversationId、blockId、blockName、およびdataフィールドを含むメモリオブジェクトの配列 |
|
||||
| `memories` | array | conversationIdとdataフィールドを含むメモリオブジェクトの配列 |
|
||||
| `message` | string | 成功またはエラーメッセージ |
|
||||
| `error` | string | 操作が失敗した場合のエラーメッセージ |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
データベースからすべてのメモリを取得する
|
||||
データベースからすべてのメモリを取得します
|
||||
|
||||
#### 入力
|
||||
|
||||
@@ -74,22 +71,20 @@ conversationId、blockId、blockName、またはそれらの組み合わせに
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | すべてのメモリが正常に取得されたかどうか |
|
||||
| `memories` | array | key、conversationId、blockId、blockName、およびdataフィールドを含むすべてのメモリオブジェクトの配列 |
|
||||
| `memories` | array | key、conversationId、dataフィールドを含むすべてのメモリオブジェクトの配列 |
|
||||
| `message` | string | 成功またはエラーメッセージ |
|
||||
| `error` | string | 操作が失敗した場合のエラーメッセージ |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
conversationId、blockId、blockName、またはそれらの組み合わせによってメモリを削除します。一括削除をサポートしています。
|
||||
conversationIdによってメモリを削除します。
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。単独で提供された場合、すべてのブロックにわたるこの会話のすべてのメモリを削除します。 |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。この会話のすべてのメモリを削除します。 |
|
||||
| `id` | string | いいえ | 会話識別子のレガシーパラメータ。代わりにconversationIdを使用してください。後方互換性のために提供されています。 |
|
||||
| `blockId` | string | いいえ | ブロック識別子。単独で提供された場合、すべての会話にわたるこのブロックのすべてのメモリを削除します。conversationIdと共に提供された場合、そのブロック内の特定の会話のメモリを削除します。 |
|
||||
| `blockName` | string | いいえ | ブロック名。blockIdの代替。単独で提供された場合、この名前を持つブロックのすべてのメモリを削除します。conversationIdと共に提供された場合、この名前を持つブロック内のその会話のメモリを削除します。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ Supabaseテーブルからデータを照会する
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | 照会するSupabaseテーブルの名前 |
|
||||
| `table` | string | はい | クエリするSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | クエリするデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `filter` | string | いいえ | PostgRESTフィルター(例:"id=eq.123") |
|
||||
| `orderBy` | string | いいえ | 並べ替える列(降順の場合はDESCを追加) |
|
||||
| `limit` | number | いいえ | 返す最大行数 |
|
||||
@@ -71,6 +72,7 @@ Supabaseテーブルにデータを挿入する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | データを挿入するSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | 挿入するデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `data` | array | はい | 挿入するデータ(オブジェクトの配列または単一のオブジェクト) |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
|
||||
@@ -89,10 +91,11 @@ Supabaseテーブルにデータを挿入する
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | SupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | クエリを実行するSupabaseテーブルの名前 |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | クエリするSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | クエリするデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `filter` | string | はい | 特定の行を見つけるためのPostgRESTフィルター(例:"id=eq.123") |
|
||||
| `apiKey` | string | はい | Supabaseサービスロールのシークレットキー |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -111,7 +114,8 @@ Supabaseテーブルにデータを挿入する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | 更新するSupabaseテーブルの名前 |
|
||||
| `filter` | string | はい | 更新する行を識別するPostgRESTフィルター(例:"id=eq.123") |
|
||||
| `schema` | string | いいえ | 更新するデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `filter` | string | はい | 更新する行を識別するためのPostgRESTフィルター(例:"id=eq.123") |
|
||||
| `data` | object | はい | 一致する行で更新するデータ |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
|
||||
@@ -132,7 +136,8 @@ Supabaseテーブルにデータを挿入する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | 削除するSupabaseテーブルの名前 |
|
||||
| `filter` | string | はい | 削除する行を識別するPostgRESTフィルター(例:"id=eq.123") |
|
||||
| `schema` | string | いいえ | 削除するデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `filter` | string | はい | 削除する行を識別するためのPostgRESTフィルター(例:"id=eq.123") |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
|
||||
#### 出力
|
||||
@@ -151,8 +156,9 @@ Supabaseテーブルにデータを挿入または更新する(アップサー
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | データをアップサートするSupabaseテーブルの名前 |
|
||||
| `data` | array | はい | アップサート(挿入または更新)するデータ - オブジェクトの配列または単一のオブジェクト |
|
||||
| `table` | string | はい | データをupsertするSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | upsertするデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `data` | array | はい | upsert(挿入または更新)するデータ(オブジェクトの配列または単一のオブジェクト) |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
|
||||
#### 出力
|
||||
@@ -172,6 +178,7 @@ Supabaseテーブルの行数をカウントする
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | 行数をカウントするSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | カウント元のデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用 |
|
||||
| `filter` | string | いいえ | PostgRESTフィルター(例:"status=eq.active") |
|
||||
| `countType` | string | いいえ | カウントタイプ:exact、planned、またはestimated(デフォルト:exact) |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
@@ -193,6 +200,7 @@ Supabaseテーブルで全文検索を実行する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | 検索するSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | 検索するデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用 |
|
||||
| `column` | string | はい | 検索する列 |
|
||||
| `query` | string | はい | 検索クエリ |
|
||||
| `searchType` | string | いいえ | 検索タイプ:plain、phrase、またはwebsearch(デフォルト:websearch) |
|
||||
|
||||
@@ -47,42 +47,42 @@ totalCost = baseExecutionCharge + modelCost
|
||||
|
||||
## 定价选项
|
||||
|
||||
<Tabs items={['托管模型', '使用您自己的 API 密钥']}>
|
||||
<Tabs items={[ '托管模型', '自带 API 密钥' ]}>
|
||||
<Tab>
|
||||
**托管模型** - Sim 提供 API 密钥,价格为基础价格的 2.5 倍:
|
||||
**托管模型** - Sim 提供 API 密钥,价格为基础价格的 2 倍:
|
||||
|
||||
**OpenAI**
|
||||
| 模型 | 基础价格(输入/输出) | 托管价格(输入/输出) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-5.1 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $3.13 / $25.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.63 / $5.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.13 / $1.00 |
|
||||
| GPT-4o | $2.50 / $10.00 | $6.25 / $25.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $1.00 / $4.00 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.25 / $1.00 |
|
||||
| o1 | $15.00 / $60.00 | $37.50 / $150.00 |
|
||||
| o3 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.75 / $11.00 |
|
||||
| GPT-5.1 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| GPT-5 Mini | $0.25 / $2.00 | $0.50 / $4.00 |
|
||||
| GPT-5 Nano | $0.05 / $0.40 | $0.10 / $0.80 |
|
||||
| GPT-4o | $2.50 / $10.00 | $5.00 / $20.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| GPT-4.1 Mini | $0.40 / $1.60 | $0.80 / $3.20 |
|
||||
| GPT-4.1 Nano | $0.10 / $0.40 | $0.20 / $0.80 |
|
||||
| o1 | $15.00 / $60.00 | $30.00 / $120.00 |
|
||||
| o3 | $2.00 / $8.00 | $4.00 / $16.00 |
|
||||
| o4 Mini | $1.10 / $4.40 | $2.20 / $8.80 |
|
||||
|
||||
**Anthropic**
|
||||
| 模型 | 基础价格(输入/输出) | 托管价格(输入/输出) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $12.50 / $62.50 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $37.50 / $187.50 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.50 / $12.50 |
|
||||
| Claude Opus 4.5 | $5.00 / $25.00 | $10.00 / $50.00 |
|
||||
| Claude Opus 4.1 | $15.00 / $75.00 | $30.00 / $150.00 |
|
||||
| Claude Sonnet 4.5 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Sonnet 4.0 | $3.00 / $15.00 | $6.00 / $30.00 |
|
||||
| Claude Haiku 4.5 | $1.00 / $5.00 | $2.00 / $10.00 |
|
||||
|
||||
**Google**
|
||||
| 模型 | 基础价格(输入/输出) | 托管价格(输入/输出) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $5.00 / $30.00 |
|
||||
| Gemini 2.5 Pro | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 2.5 Flash | $0.15 / $0.60 | $0.38 / $1.50 |
|
||||
| Gemini 3 Pro Preview | $2.00 / $12.00 | $4.00 / $24.00 |
|
||||
| Gemini 2.5 Pro | $1.25 / $10.00 | $2.50 / $20.00 |
|
||||
| Gemini 2.5 Flash | $0.30 / $2.50 | $0.60 / $5.00 |
|
||||
|
||||
*2.5 倍的价格倍增用于覆盖基础设施和 API 管理成本。*
|
||||
*2 倍系数涵盖了基础设施和 API 管理成本。*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
@@ -185,12 +185,12 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
||||
|
||||
不同的订阅计划有不同的使用限制:
|
||||
|
||||
| 计划 | 每月使用限制 | 速率限制(每分钟) |
|
||||
| 方案 | 每月使用限额 | 速率限制(每分钟) |
|
||||
|------|-------------------|-------------------------|
|
||||
| **免费** | $10 | 5 同步,10 异步 |
|
||||
| **专业** | $100 | 10 同步,50 异步 |
|
||||
| **团队** | $500(共享) | 50 同步,100 异步 |
|
||||
| **企业** | 自定义 | 自定义 |
|
||||
| **Free** | $20 | 5 sync,10 async |
|
||||
| **Pro** | $100 | 10 sync,50 async |
|
||||
| **Team** | $500(共享) | 50 sync,100 async |
|
||||
| **Enterprise** | 定制 | 定制 |
|
||||
|
||||
## 计费模式
|
||||
|
||||
|
||||
@@ -34,81 +34,87 @@ Sim 支持 PDF、Word (DOC/DOCX)、纯文本 (TXT)、Markdown (MD)、HTML、Exce
|
||||
<Image src="/static/knowledgebase/knowledgebase.png" alt="显示已处理内容的文档分块视图" width={800} height={500} />
|
||||
|
||||
### 分块配置
|
||||
- **默认分块大小**:1,024 个字符
|
||||
- **可配置范围**:每块 100-4,000 个字符
|
||||
- **智能重叠**:默认重叠 200 个字符以保留上下文
|
||||
|
||||
在创建知识库时,您可以配置文档如何被拆分为多个分块:
|
||||
|
||||
| 设置 | 单位 | 默认值 | 范围 | 说明 |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **最大分块大小** | tokens | 1,024 | 100-4,000 | 每个分块的最大大小(1 token ≈ 4 个字符) |
|
||||
| **最小分块大小** | 字符 | 1 | 1-2,000 | 避免生成过小分块的最小分块大小 |
|
||||
| **重叠量** | 字符 | 200 | 0-500 | 相邻分块之间的上下文重叠字符数 |
|
||||
|
||||
- **分层拆分**:遵循文档结构(章节、段落、句子)
|
||||
|
||||
### 编辑功能
|
||||
- **编辑分块内容**:修改单个分块的文本内容
|
||||
- **编辑分块内容**:可修改单个分块的文本内容
|
||||
- **调整分块边界**:根据需要合并或拆分分块
|
||||
- **添加元数据**:为分块添加额外的上下文信息
|
||||
- **添加元数据**:为分块补充更多上下文信息
|
||||
- **批量操作**:高效管理多个分块
|
||||
|
||||
## 高级 PDF 处理
|
||||
|
||||
对于 PDF 文档,Sim 提供增强的处理功能:
|
||||
对于 PDF 文档,Sim 提供了增强的处理能力:
|
||||
|
||||
### OCR 支持
|
||||
当配置了 Azure 或 [Mistral OCR](https://docs.mistral.ai/ocr/) 时:
|
||||
- **扫描文档处理**:从基于图像的 PDF 中提取文本
|
||||
- **混合内容处理**:处理同时包含文本和图像的 PDF
|
||||
- **高精度**:先进的 AI 模型确保准确的文本提取
|
||||
- **混合内容处理**:处理同时包含文本和图片的 PDF
|
||||
- **高精度**:先进的 AI 模型确保文本提取的准确性
|
||||
|
||||
## 在工作流中使用知识块
|
||||
|
||||
一旦您的文档被处理,您可以通过知识块在 AI 工作流中使用它们。这使得检索增强生成(RAG)成为可能,让您的 AI 代理能够访问并推理文档内容,从而提供更准确、有上下文的响应。
|
||||
文档处理完成后,您可以通过知识块在 AI 工作流中使用它们。这实现了 RAG(检索增强生成),让您的 AI 智能体能够访问并理解文档内容,从而提供更准确、有上下文的回复。
|
||||
|
||||
<Image src="/static/knowledgebase/knowledgebase-2.png" alt="在工作流中使用知识块" width={800} height={500} />
|
||||
|
||||
### 知识块功能
|
||||
- **语义搜索**:使用自然语言查询查找相关内容
|
||||
- **上下文集成**:自动将相关分块包含在代理提示中
|
||||
- **动态检索**:在工作流执行期间实时搜索
|
||||
- **相关性评分**:根据语义相似性对结果进行排名
|
||||
- **语义搜索**:通过自然语言查询查找相关内容
|
||||
- **上下文集成**:自动将相关分块纳入智能体提示词
|
||||
- **动态检索**:在工作流执行时实时搜索
|
||||
- **相关性评分**:根据语义相似度对结果进行排序
|
||||
|
||||
### 集成选项
|
||||
- **系统提示**:为您的 AI 代理提供上下文
|
||||
- **动态上下文**:在对话中搜索并包含相关信息
|
||||
- **多文档搜索**:在整个知识库中查询
|
||||
- **过滤搜索**:结合标签实现精确内容检索
|
||||
- **系统提示**:为你的 AI 智能体提供上下文
|
||||
- **动态上下文**:在对话中搜索并纳入相关信息
|
||||
- **多文档搜索**:可在整个知识库中查询
|
||||
- **筛选搜索**:结合标签,实现精准内容检索
|
||||
|
||||
## 向量搜索技术
|
||||
|
||||
Sim 使用由 [pgvector](https://github.com/pgvector/pgvector) 提供支持的向量搜索来理解您的内容的含义和上下文:
|
||||
Sim 利用 [pgvector](https://github.com/pgvector/pgvector) 提供的向量搜索,理解你的内容的含义和上下文:
|
||||
|
||||
### 语义理解
|
||||
- **上下文搜索**:即使精确的关键词不匹配,也能找到相关内容
|
||||
- **基于概念的检索**:理解想法之间的关系
|
||||
- **多语言支持**:支持跨不同语言工作
|
||||
- **同义词识别**:找到相关术语和概念
|
||||
- **上下文搜索**:即使关键词不完全匹配,也能找到相关内容
|
||||
- **基于概念的检索**:理解不同想法之间的关系
|
||||
- **多语言支持**:可跨多种语言使用
|
||||
- **同义词识别**:发现相关术语和概念
|
||||
|
||||
### 搜索功能
|
||||
- **自然语言查询**:用简单的英语提问
|
||||
- **相似性搜索**:找到概念上相似的内容
|
||||
- **混合搜索**:结合向量和传统关键词搜索
|
||||
- **可配置结果**:控制结果的数量和相关性阈值
|
||||
### 搜索能力
|
||||
- **自然语言查询**:可用简单英文提问
|
||||
- **相似度搜索**:查找概念上相似的内容
|
||||
- **混合搜索**:结合向量与传统关键词搜索
|
||||
- **结果可配置**:可控制结果数量和相关性阈值
|
||||
|
||||
## 文档管理
|
||||
|
||||
### 组织功能
|
||||
- **批量上传**:通过异步 API 一次上传多个文件
|
||||
- **处理状态**:实时更新文档处理状态
|
||||
- **搜索和过滤**:在大型集合中快速找到文档
|
||||
- **元数据跟踪**:自动捕获文件信息和处理详情
|
||||
- **处理状态**:实时更新文档处理进度
|
||||
- **搜索与筛选**:在大型集合中快速查找文档
|
||||
- **元数据追踪**:自动记录文件信息和处理详情
|
||||
|
||||
### 安全性和隐私
|
||||
- **安全存储**:文档以企业级安全性存储
|
||||
- **访问控制**:基于工作区的权限设置
|
||||
- **处理隔离**:每个工作区的文档处理是独立的
|
||||
- **数据保留**:配置文档保留策略
|
||||
### 安全与隐私
|
||||
- **安全存储**:文档采用企业级安全存储
|
||||
- **访问控制**:基于工作区的权限管理
|
||||
- **处理隔离**:每个工作区的文档处理相互隔离
|
||||
- **数据保留**:可配置文档保留策略
|
||||
|
||||
## 快速入门
|
||||
## 快速开始
|
||||
|
||||
1. **导航到您的知识库**:从工作区侧边栏访问
|
||||
2. **上传文档**:拖放或选择文件进行上传
|
||||
3. **监控处理**:查看文档的处理和分块进度
|
||||
4. **探索分块**:查看和编辑处理后的内容
|
||||
5. **添加到工作流**:使用知识块与您的 AI 代理集成
|
||||
1. **进入你的知识库**:可在工作区侧边栏访问
|
||||
2. **上传文档**:拖拽或选择文件上传
|
||||
3. **监控处理进度**:实时查看文档处理与分块
|
||||
4. **浏览分块内容**:查看并编辑已处理内容
|
||||
5. **添加到工作流**:使用 Knowledge 模块集成到你的 AI 智能体
|
||||
|
||||
知识库将您的静态文档转化为智能的、可搜索的资源,使您的 AI 工作流能够利用这些资源提供更有信息量和上下文的响应。
|
||||
知识库将您的静态文档转化为智能、可搜索的资源,使您的 AI 工作流能够利用这些信息,提供更有见地和更具上下文的回应。
|
||||
@@ -38,16 +38,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `email` | string | 否 | 联系人的电子邮件地址 |
|
||||
| `external_id` | string | 否 | 客户提供的联系人的唯一标识符 |
|
||||
| `phone` | string | 否 | 联系人的电话号码 |
|
||||
| `name` | string | 否 | 联系人的姓名 |
|
||||
| `avatar` | string | 否 | 联系人的头像图片 URL |
|
||||
| `role` | string | 否 | 联系人角色。可选 'user' 或 'lead'。如未指定,默认为 'lead'。 |
|
||||
| `email` | string | 否 | 联系人邮箱地址 |
|
||||
| `external_id` | string | 否 | 客户端为联系人提供的唯一标识符 |
|
||||
| `phone` | string | 否 | 联系人电话号码 |
|
||||
| `name` | string | 否 | 联系人姓名 |
|
||||
| `avatar` | string | 否 | 联系人头像图片 URL |
|
||||
| `signed_up_at` | number | 否 | 用户注册时间(Unix 时间戳) |
|
||||
| `last_seen_at` | number | 否 | 用户上次访问时间(Unix 时间戳) |
|
||||
| `owner_id` | string | 否 | 被分配为联系人账户所有者的管理员 ID |
|
||||
| `unsubscribed_from_emails` | boolean | 否 | 联系人是否取消订阅电子邮件 |
|
||||
| `custom_attributes` | string | 否 | 自定义属性,格式为 JSON 对象 \(例如,\{"attribute_name": "value"\}\) |
|
||||
| `last_seen_at` | number | 否 | 用户最后一次在线时间(Unix 时间戳) |
|
||||
| `owner_id` | string | 否 | 被分配为该联系人账户所有者的管理员 ID |
|
||||
| `unsubscribed_from_emails` | boolean | 否 | 联系人是否已退订邮件 |
|
||||
| `custom_attributes` | string | 否 | 自定义属性,格式为 JSON 对象(如:\{"attribute_name": "value"\}) |
|
||||
| `company_id` | string | 否 | 创建联系人时关联的公司 ID |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -82,15 +84,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `contactId` | string | 是 | 要更新的联系人 ID |
|
||||
| `email` | string | 否 | 联系人的电子邮件地址 |
|
||||
| `phone` | string | 否 | 联系人的电话号码 |
|
||||
| `name` | string | 否 | 联系人的姓名 |
|
||||
| `avatar` | string | 否 | 联系人的头像图片 URL |
|
||||
| `role` | string | 否 | 联系人角色。可选 'user' 或 'lead'。 |
|
||||
| `external_id` | string | 否 | 客户端为联系人提供的唯一标识符 |
|
||||
| `email` | string | 否 | 联系人邮箱地址 |
|
||||
| `phone` | string | 否 | 联系人电话号码 |
|
||||
| `name` | string | 否 | 联系人姓名 |
|
||||
| `avatar` | string | 否 | 联系人头像图片 URL |
|
||||
| `signed_up_at` | number | 否 | 用户注册时间(Unix 时间戳) |
|
||||
| `last_seen_at` | number | 否 | 用户上次访问时间(Unix 时间戳) |
|
||||
| `owner_id` | string | 否 | 分配了账户所有权的管理员 ID |
|
||||
| `unsubscribed_from_emails` | boolean | 否 | 联系人是否取消订阅电子邮件 |
|
||||
| `custom_attributes` | string | 否 | 自定义属性,格式为 JSON 对象 \(例如:\{"attribute_name": "value"\}\) |
|
||||
| `last_seen_at` | number | 否 | 用户最后一次在线时间(Unix 时间戳) |
|
||||
| `owner_id` | string | 否 | 被分配为该联系人账户所有者的管理员 ID |
|
||||
| `unsubscribed_from_emails` | boolean | 否 | 联系人是否已退订邮件 |
|
||||
| `custom_attributes` | string | 否 | 自定义属性,格式为 JSON 对象(如:\{"attribute_name": "value"\}) |
|
||||
| `company_id` | string | 否 | 关联的公司 ID |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -123,11 +128,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | 是 | 搜索查询 \(例如, \{"field":"email","operator":"=","value":"user@example.com"\}\) |
|
||||
| `per_page` | number | 否 | 每页结果数量 \(最大值: 150\) |
|
||||
| `starting_after` | string | 否 | 分页游标 |
|
||||
| `query` | string | 是 | 搜索查询(例如:\{"field":"email", "operator":"=", "value":"user@example.com"\}) |
|
||||
| `per_page` | number | 否 | 每页结果数量(最大值:150) |
|
||||
| `starting_after` | string | 否 | 用于分页的游标 |
|
||||
| `sort_field` | string | 否 | 排序字段(例如:"name","created_at","last_seen_at") |
|
||||
| `sort_order` | string | 否 | 排序方式:“ascending” 或 “descending” |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -159,16 +166,17 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `company_id` | string | 是 | 您的公司唯一标识符 |
|
||||
| `name` | string | 否 | 公司的名称 |
|
||||
| `company_id` | string | 是 | 您为公司设置的唯一标识符 |
|
||||
| `name` | string | 否 | 公司名称 |
|
||||
| `website` | string | 否 | 公司网站 |
|
||||
| `plan` | string | 否 | 公司计划名称 |
|
||||
| `plan` | string | 否 | 公司套餐名称 |
|
||||
| `size` | number | 否 | 公司员工数量 |
|
||||
| `industry` | string | 否 | 公司所属行业 |
|
||||
| `monthly_spend` | number | 否 | 公司为您的业务创造的收入。注意:此字段会将浮点数截断为整数(例如,155.98 会变为 155) |
|
||||
| `custom_attributes` | string | 否 | 作为 JSON 对象的自定义属性 |
|
||||
| `monthly_spend` | number | 否 | 公司为您的业务带来的收入。注意:此字段会将浮点数截断为整数(例如:155.98 变为 155) |
|
||||
| `custom_attributes` | string | 否 | 自定义属性,格式为 JSON 对象 |
|
||||
| `remote_created_at` | number | 否 | 您创建公司时的 Unix 时间戳 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -200,10 +208,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | 数字 | 否 | 每页结果数量 |
|
||||
| `page` | 数字 | 否 | 页码 |
|
||||
| `per_page` | number | 否 | 每页结果数量 |
|
||||
| `page` | number | 否 | 页码 |
|
||||
| `starting_after` | string | 否 | 分页游标(优先于基于页码的分页) |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -220,8 +229,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | 字符串 | 是 | 要检索的会话 ID |
|
||||
| `display_as` | 字符串 | 否 | 设置为 "plaintext" 以检索纯文本消息 |
|
||||
| `conversationId` | string | 是 | 要检索的会话 ID |
|
||||
| `display_as` | string | 否 | 设为 "plaintext" 可检索纯文本消息 |
|
||||
| `include_translations` | boolean | 否 | 若为 true,会话内容将被翻译为检测到的会话语言 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -238,8 +248,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `per_page` | 数字 | 否 | 每页结果数量 \(最大值: 150\) |
|
||||
| `starting_after` | 字符串 | 否 | 分页游标 |
|
||||
| `per_page` | number | 否 | 每页结果数量(最大值:150) |
|
||||
| `starting_after` | string | 否 | 分页游标 |
|
||||
| `sort` | string | 否 | 排序字段(例如:"waiting_since"、"updated_at"、"created_at") |
|
||||
| `order` | string | 否 | 排序方式:"asc"(升序)或 "desc"(降序) |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -258,9 +270,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | 是 | 要回复的会话 ID |
|
||||
| `message_type` | string | 是 | 消息类型:"comment" 或 "note" |
|
||||
| `body` | string | 是 | 回复的正文文本 |
|
||||
| `admin_id` | string | 否 | 撰写回复的管理员 ID。如果未提供,将使用默认管理员(Operator/Fin)。 |
|
||||
| `attachment_urls` | string | 否 | 逗号分隔的图片 URL 列表(最多 10 个) |
|
||||
| `body` | string | 是 | 回复正文 |
|
||||
| `admin_id` | string | 否 | 回复管理员的 ID。如果未提供,将使用默认管理员(Operator/Fin)。 |
|
||||
| `attachment_urls` | string | 否 | 以逗号分隔的图片 URL 列表(最多 10 个) |
|
||||
| `created_at` | number | 否 | 回复创建时的 Unix 时间戳。如果未提供,则使用当前时间。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -275,11 +288,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | 是 | 作为 JSON 对象的搜索查询 |
|
||||
| `per_page` | number | 否 | 每页结果数量(最大值:150) |
|
||||
| `starting_after` | string | 否 | 用于分页的游标 |
|
||||
| `starting_after` | string | 否 | 分页游标 |
|
||||
| `sort_field` | string | 否 | 排序字段(例如:"created_at","updated_at") |
|
||||
| `sort_order` | string | 否 | 排序顺序:“ascending” 或 “descending” |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -294,11 +309,15 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `ticket_type_id` | string | 是 | 工单类型的 ID |
|
||||
| `contacts` | string | 是 | 联系人标识符的 JSON 数组(例如,\[\{"id": "contact_id"\}\]) |
|
||||
| `ticket_attributes` | string | 是 | 包含工单属性的 JSON 对象,包括 _default_title_ 和 _default_description_ |
|
||||
| `contacts` | string | 是 | 联系人标识符的 JSON 数组(例如:\[\{"id": "contact_id"\}\]) |
|
||||
| `ticket_attributes` | string | 是 | 包含 _default_title_ 和 _default_description_ 的工单属性 JSON 对象 |
|
||||
| `company_id` | string | 否 | 要关联工单的公司 ID |
|
||||
| `created_at` | number | 否 | 工单创建时的 Unix 时间戳。如果未提供,则使用当前时间。 |
|
||||
| `conversation_to_link_id` | string | 否 | 要关联到此工单的现有会话 ID |
|
||||
| `disable_notifications` | boolean | 否 | 若为 true,创建工单时将不发送通知 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -332,13 +351,15 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `message_type` | string | 是 | 消息类型:"inapp" 或 "email" |
|
||||
| `subject` | string | 否 | 消息主题(针对 email 类型) |
|
||||
| `message_type` | string | 是 | 消息类型:“inapp” 表示应用内消息,“email” 表示电子邮件消息 |
|
||||
| `template` | string | 是 | 消息模板样式:“plain” 表示纯文本,“personal” 表示个性化样式 |
|
||||
| `subject` | string | 否 | 消息主题(仅适用于 email 类型) |
|
||||
| `body` | string | 是 | 消息正文 |
|
||||
| `from_type` | string | 是 | 发送者类型:"admin" |
|
||||
| `from_type` | string | 是 | 发送方类型:“admin” |
|
||||
| `from_id` | string | 是 | 发送消息的管理员 ID |
|
||||
| `to_type` | string | 是 | 接收者类型:"contact" |
|
||||
| `to_type` | string | 是 | 接收方类型:“contact” |
|
||||
| `to_id` | string | 是 | 接收消息的联系人的 ID |
|
||||
| `created_at` | number | 否 | 消息创建时的 Unix 时间戳。如果未提供,则使用当前时间。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 内存
|
||||
description: 添加内存存储
|
||||
description: 添加记忆存储
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -12,53 +12,50 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
## 使用说明
|
||||
|
||||
将记忆集成到工作流程中。可以添加记忆、获取记忆、获取所有记忆以及删除记忆。
|
||||
将 Memory 集成到工作流程中。可以添加、获取单条记忆、获取所有记忆,以及删除记忆。
|
||||
|
||||
## 工具
|
||||
|
||||
### `memory_add`
|
||||
|
||||
向数据库添加新的内存,或将数据追加到具有相同 ID 的现有内存中。
|
||||
向数据库添加新记忆,或将内容追加到已有相同 ID 的记忆中。
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | 否 | 会话标识符(例如,user-123,session-abc)。如果此 block 已存在具有该 conversationId 的内存,新消息将附加到该内存中。 |
|
||||
| `id` | string | 否 | 会话标识符的旧参数。请改用 conversationId。为向后兼容而提供。 |
|
||||
| `role` | string | 是 | 代理内存的角色(user、assistant 或 system) |
|
||||
| `content` | string | 是 | 代理内存的内容 |
|
||||
| `blockId` | string | 否 | 可选的 block ID。如果未提供,将使用执行上下文中的当前 block ID,或默认为 "default"。 |
|
||||
| --------- | ---- | ---- | ----------- |
|
||||
| `conversationId` | string | 否 | 会话标识符(如 user-123、session-abc)。如果已存在该 conversationId 的记忆,则新消息会追加到其中。 |
|
||||
| `id` | string | 否 | 旧版会话标识参数。请使用 conversationId,保留用于兼容性。 |
|
||||
| `role` | string | 是 | agent 记忆的角色(user、assistant 或 system) |
|
||||
| `content` | string | 是 | agent 记忆的内容 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | 布尔值 | 是否成功添加了内存 |
|
||||
| `memories` | 数组 | 包含新添加或更新内存的内存对象数组 |
|
||||
| `error` | 字符串 | 如果操作失败,显示错误信息 |
|
||||
| `success` | boolean | 记忆是否添加成功 |
|
||||
| `memories` | array | 包含新建或更新记忆的记忆对象数组 |
|
||||
| `error` | string | 操作失败时的错误信息 |
|
||||
|
||||
### `memory_get`
|
||||
|
||||
通过 conversationId、blockId、blockName 或其组合检索内存。返回所有匹配的内存。
|
||||
根据 conversationId 检索记忆,返回匹配的记忆内容。
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | 否 | 会话标识符(例如,user-123,session-abc)。如果单独提供,将返回此会话在所有 block 中的所有内存。 |
|
||||
| `id` | string | 否 | 会话标识符的旧参数。请改用 conversationId。为向后兼容而提供。 |
|
||||
| `blockId` | string | 否 | block 标识符。如果单独提供,将返回此 block 中所有会话的所有内存。如果与 conversationId 一起提供,将返回此 block 中该特定会话的内存。 |
|
||||
| `blockName` | string | 否 | block 名称。blockId 的替代选项。如果单独提供,将返回具有此名称的 block 的所有内存。如果与 conversationId 一起提供,将返回具有此名称的 block 中该会话的内存。 |
|
||||
| --------- | ---- | ---- | ----------- |
|
||||
| `conversationId` | string | 否 | 会话标识符(如 user-123、session-abc)。返回该会话的记忆。 |
|
||||
| `id` | string | 否 | 旧版会话标识参数。请使用 conversationId,保留用于兼容性。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 内存是否成功检索 |
|
||||
| `memories` | array | 包含 conversationId、blockId、blockName 和 data 字段的内存对象数组 |
|
||||
| `success` | boolean | 是否成功检索到内存 |
|
||||
| `memories` | array | 包含 conversationId 和 data 字段的内存对象数组 |
|
||||
| `message` | string | 成功或错误信息 |
|
||||
| `error` | string | 如果操作失败的错误信息 |
|
||||
| `error` | string | 操作失败时的错误信息 |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -74,32 +71,30 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 是否成功检索到所有内存 |
|
||||
| `memories` | array | 包含 key、conversationId、blockId、blockName 和 data 字段的所有内存对象数组 |
|
||||
| `memories` | array | 包含 key、conversationId 和 data 字段的所有内存对象数组 |
|
||||
| `message` | string | 成功或错误信息 |
|
||||
| `error` | string | 如果操作失败的错误信息 |
|
||||
| `error` | string | 操作失败时的错误信息 |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
通过 conversationId、blockId、blockName 或其组合删除内存。支持批量删除。
|
||||
根据 conversationId 删除内存。
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `conversationId` | string | 否 | 会话标识符 \(例如,user-123,session-abc\)。如果单独提供,将删除此会话在所有块中的所有内存。 |
|
||||
| `id` | string | 否 | 会话标识符的旧参数。请改用 conversationId。为向后兼容而提供。 |
|
||||
| `blockId` | string | 否 | 块标识符。如果单独提供,将删除此块中所有会话的所有内存。如果与 conversationId 一起提供,将删除此块中特定会话的内存。 |
|
||||
| `blockName` | string | 否 | 块名称。是 blockId 的替代项。如果单独提供,将删除具有此名称的块的所有内存。如果与 conversationId 一起提供,将删除此名称的块中该会话的内存。 |
|
||||
| `conversationId` | string | 否 | 会话标识符(如 user-123、session-abc)。将删除该会话的所有内存。 |
|
||||
| `id` | string | 否 | 旧版会话标识参数。请使用 conversationId,保留用于兼容性。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 内存是否成功删除 |
|
||||
| `success` | boolean | 是否成功删除内存 |
|
||||
| `message` | string | 成功或错误信息 |
|
||||
| `error` | string | 如果操作失败的错误信息 |
|
||||
| `error` | string | 操作失败时的错误信息 |
|
||||
|
||||
## 注意
|
||||
## 注意事项
|
||||
|
||||
- 类别:`blocks`
|
||||
- 类型:`memory`
|
||||
|
||||
@@ -47,12 +47,13 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | 字符串 | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | 字符串 | 是 | 要查询的 Supabase 表名 |
|
||||
| `filter` | 字符串 | 否 | PostgREST 过滤条件 \(例如:"id=eq.123"\) |
|
||||
| `orderBy` | 字符串 | 否 | 排序的列名 \(添加 DESC 表示降序\) |
|
||||
| `limit` | 数字 | 否 | 返回的最大行数 |
|
||||
| `apiKey` | 字符串 | 是 | 您的 Supabase 服务角色密钥 |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要查询的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要查询的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `filter` | string | 否 | PostgREST 过滤条件 \(例如:"id=eq.123"\) |
|
||||
| `orderBy` | string | 否 | 排序的列名 \(添加 DESC 表示降序\) |
|
||||
| `limit` | number | 否 | 返回的最大行数 |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -71,7 +72,8 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要插入数据的 Supabase 表名 |
|
||||
| `data` | array | 是 | 要插入的数据 \(对象数组或单个对象\) |
|
||||
| `schema` | string | 否 | 要插入的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `data` | array | 是 | 要插入的数据(对象数组或单个对象)|
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
@@ -91,7 +93,8 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要查询的 Supabase 表名 |
|
||||
| `filter` | string | 是 | PostgREST 筛选条件以找到特定行 \(例如:"id=eq.123"\) |
|
||||
| `schema` | string | 否 | 要查询的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `filter` | string | 是 | 用于查找特定行的 PostgREST 过滤条件 \(例如:"id=eq.123"\) |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
@@ -110,9 +113,10 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要更新的 Supabase 表的名称 |
|
||||
| `filter` | string | 是 | 用于标识要更新行的 PostgREST 筛选条件 \(例如:"id=eq.123"\) |
|
||||
| `data` | object | 是 | 要更新到匹配行的数据 |
|
||||
| `table` | string | 是 | 要更新的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要更新的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `filter` | string | 是 | PostgREST 筛选条件,用于定位要更新的行 \(例如:"id=eq.123"\) |
|
||||
| `data` | object | 是 | 要在匹配行中更新的数据 |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
@@ -131,8 +135,9 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要删除的 Supabase 表的名称 |
|
||||
| `filter` | string | 是 | 用于标识要删除行的 PostgREST 筛选条件 \(例如:"id=eq.123"\) |
|
||||
| `table` | string | 是 | 要删除数据的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要删除数据的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `filter` | string | 是 | PostgREST 筛选条件,用于定位要删除的行 \(例如:"id=eq.123"\) |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
@@ -151,8 +156,9 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要插入或更新数据的 Supabase 表名 |
|
||||
| `data` | array | 是 | 要插入或更新的数据 \(插入或更新\) - 对象数组或单个对象 |
|
||||
| `table` | string | 是 | 要 upsert 数据的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要 upsert 的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `data` | array | 是 | 要 upsert(插入或更新)的数据——对象数组或单个对象 |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
@@ -172,6 +178,7 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要统计行数的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要统计的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。 |
|
||||
| `filter` | string | 否 | PostgREST 过滤条件 \(例如:"status=eq.active"\) |
|
||||
| `countType` | string | 否 | 计数类型:exact、planned 或 estimated \(默认:exact\) |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
@@ -193,8 +200,9 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要搜索的 Supabase 表名 |
|
||||
| `column` | string | 是 | 要搜索的列 |
|
||||
| `query` | string | 是 | 搜索查询 |
|
||||
| `schema` | string | 否 | 要搜索的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。 |
|
||||
| `column` | string | 是 | 要搜索的列名 |
|
||||
| `query` | string | 是 | 搜索查询内容 |
|
||||
| `searchType` | string | 否 | 搜索类型:plain、phrase 或 websearch \(默认:websearch\) |
|
||||
| `language` | string | 否 | 文本搜索配置的语言 \(默认:english\) |
|
||||
| `limit` | number | 否 | 返回的最大行数 |
|
||||
|
||||
@@ -698,49 +698,49 @@ checksums:
|
||||
content/11: 04bd9805ef6a50af8469463c34486dbf
|
||||
content/12: a3671dd7ba76a87dc75464d9bf9b7b4b
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 319b69dde6e263f38497a0a84dc58e60
|
||||
content/14: 80578981b8b3a1cf579e52ff05e7468d
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: 09ed43219d02501c829594dbf4128959
|
||||
content/17: 88ae2285d728c80937e1df8194d92c60
|
||||
content/18: cb8a6d5bf54beed29f0809f80b27648d
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: cdbe4726ca8dd4bb6a013055e14117f5
|
||||
content/20: 3212d5f414ea8ad920385eff8b18e61a
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: 5c59a9fe4d16c81655acd350d08a052e
|
||||
content/23: 7d96d99e45880195ccbd34bddaac6319
|
||||
content/24: 75d05f96dff406db06b338d9ab8d0bd7
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: c5aa1613dc223a1c4348732b24c13e8b
|
||||
content/26: cfd801fa517b4bcfa5fa034b2c4e908a
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: a0284632eb0a15e66f69479ec477c5b1
|
||||
content/29: b1e60734e590a8ad894a96581a253bf4
|
||||
content/30: bebedc0826cdad098631d8090379501e
|
||||
content/31: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/32: 017a5d7255b0b029754d91c0c4063a15
|
||||
content/32: d1ac6e1bb29969a317cb9b98bbd3bf5e
|
||||
content/33: bcadfc362b69078beee0088e5936c98b
|
||||
content/34: 31a3025a3f0b9f7348f8c0b45a47d1dd
|
||||
content/35: 9378daf3cd90dde934d19068f626e262
|
||||
content/36: 65b3f733c34d0adb46e689c95980a45f
|
||||
content/37: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/38: 35e18d7355312276b6706a79cdb4bb98
|
||||
content/38: 2308dbb1aa8b94869e61aa991dc1182a
|
||||
content/39: bcadfc362b69078beee0088e5936c98b
|
||||
content/40: ebebd366813cd5cfb35e70121ab97565
|
||||
content/41: 16f5fe78076e326d643c194312c730a5
|
||||
content/42: 5d1098c4ada4a79ade1464bd8853fc9e
|
||||
content/43: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/44: b37f6692238f3a45bfcf106f0d192b21
|
||||
content/44: 39557f0370b8aa3677ee69977a78cb5d
|
||||
content/45: bcadfc362b69078beee0088e5936c98b
|
||||
content/46: 1788748095a805b62a0e21403789dad7
|
||||
content/47: 0c504770bfe726a98df3e3fadeaf7640
|
||||
content/48: 5673ae2a352e532a43b65322df0d33a8
|
||||
content/49: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/50: 479fb8fb1b760e9a045b375aabfb1a45
|
||||
content/50: f2e7eefa05af1e373a3c667164b00276
|
||||
content/51: bcadfc362b69078beee0088e5936c98b
|
||||
content/52: 72f3fd98f52ec42be8a95a019d74c253
|
||||
content/53: 14db7775b92dc99c54d1fd5497d298ca
|
||||
content/54: f84723b1195268ffc05970b701bf866a
|
||||
content/55: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/56: 90c244b243ef73e8d08cc6265ff61445
|
||||
content/56: 826a89366439187745611f2080d6ddd1
|
||||
content/57: bcadfc362b69078beee0088e5936c98b
|
||||
content/58: f1735da5af62123df5e5345ab834b2fa
|
||||
content/59: dd231637d3d327f0daf546fe92594ac6
|
||||
@@ -1896,25 +1896,25 @@ checksums:
|
||||
content/5: b061763378e5f0aca9a25f819d03961d
|
||||
content/6: 75972cfff5aa2f1d4c24f2a1c867cfb7
|
||||
content/7: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/8: 8579c5fe58782fed019acfd5019c515e
|
||||
content/8: deb556a0ce8537461dd58a02e584d808
|
||||
content/9: bcadfc362b69078beee0088e5936c98b
|
||||
content/10: 467bff9c1a90c96930d1b05286dd4bf8
|
||||
content/11: ba06fa96a9fe3d308546a32490e5a8d8
|
||||
content/12: 58490686b3358445d2fa89e8a048fb51
|
||||
content/12: 8461d1c991d4c595a64940dc51b130e5
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 9acf9c7ac7b83db796a143abc8f8de0f
|
||||
content/14: 72895fa081d39eec0bdf7877fd53580e
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: d4ac7483993edc4308e6034d4bd551bd
|
||||
content/16: 8a2c3d0b818f57012cb3d2e50d2dd05d
|
||||
content/17: e13dff194d0bc1cecec833cb9805ceaa
|
||||
content/18: 8813ba0bc9fbf636f3a38e59667df896
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: d71b6bb8e2dd6ce98101aec6a1dd77f2
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: d2f04b0f593a08f7656e7431a6b4e5e5
|
||||
content/22: 7b17554ad765c6283fc5fe6c29b9cc77
|
||||
content/23: 9eebc263273839cc24231b56fd90b71d
|
||||
content/24: 9acb060c11b48ae498d55aceb053b996
|
||||
content/24: c442d573aaae976f5fab534388cee115
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: 707c54d664fcfc307cea6027721b940b
|
||||
content/26: 2bb55f4566a06bedea778f010f093d19
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: b48618ae66e09c34074972d091ceaef0
|
||||
content/29: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
@@ -4407,26 +4407,29 @@ checksums:
|
||||
content/9: 072b097db5f884122f323c5d8386671c
|
||||
content/10: 58882acf2056c9e54e27f38f1203f0cd
|
||||
content/11: 68d81199ca72ece1c31474f5729f98cd
|
||||
content/12: d152f1dcd6cd068be14a7ae05afd216e
|
||||
content/13: bf918d2d9b62a9ab640371cea0629c28
|
||||
content/14: 9b2ebe8a9c3efa9f14c35f6d71a7a274
|
||||
content/15: fcc7c89bc4c545daecba16f986165f9e
|
||||
content/16: 395dc70853e0c9ca156db72f3156ae36
|
||||
content/17: b5b3cc0a48c0f06fa4c475544be4a0e6
|
||||
content/18: 462ac511233020ce80369b6f834387bf
|
||||
content/19: cad571a6ce02e0807c5c23699b115c17
|
||||
content/20: 50251a58c6c1b448dc9a9ed65d7db7aa
|
||||
content/21: 791148e6bad524eb53286aeea5ddbf24
|
||||
content/22: ec70f2b34ad4b3839ff42a76c0378655
|
||||
content/23: dd4d559eba1a9c83a9166bc491d2b2ac
|
||||
content/24: 6b0f081f8453c427f3b3720d44e62857
|
||||
content/25: 84dc89f2666e7201c72c258cb4c4e2d0
|
||||
content/26: 4ae642d59321118a88ebf4ce8751c05a
|
||||
content/27: 4703a7028a16d9e716631be2a49c72bb
|
||||
content/28: d1c7c19aae4736f403b0df60cb6e48a4
|
||||
content/29: 9e5a786192608844493dfbb6e4100886
|
||||
content/30: 8961e5fb3a49bb48580b23dfd5e053c6
|
||||
content/31: 1022391b9b79b1d2e9f8db789f9c50a2
|
||||
content/12: 95cb1a1f36f0e562d85e0e54f0f439ba
|
||||
content/13: ddaad9625d2ed0f4ec1b96094e7ec7c0
|
||||
content/14: a050d81709025cb6c6c72619c80219c5
|
||||
content/15: 8a7329562f5f1324f94ae0bc9b2b3853
|
||||
content/16: bf918d2d9b62a9ab640371cea0629c28
|
||||
content/17: 9b2ebe8a9c3efa9f14c35f6d71a7a274
|
||||
content/18: fcc7c89bc4c545daecba16f986165f9e
|
||||
content/19: 395dc70853e0c9ca156db72f3156ae36
|
||||
content/20: b5b3cc0a48c0f06fa4c475544be4a0e6
|
||||
content/21: 462ac511233020ce80369b6f834387bf
|
||||
content/22: cad571a6ce02e0807c5c23699b115c17
|
||||
content/23: 50251a58c6c1b448dc9a9ed65d7db7aa
|
||||
content/24: 791148e6bad524eb53286aeea5ddbf24
|
||||
content/25: ec70f2b34ad4b3839ff42a76c0378655
|
||||
content/26: dd4d559eba1a9c83a9166bc491d2b2ac
|
||||
content/27: 6b0f081f8453c427f3b3720d44e62857
|
||||
content/28: 84dc89f2666e7201c72c258cb4c4e2d0
|
||||
content/29: 4ae642d59321118a88ebf4ce8751c05a
|
||||
content/30: 4703a7028a16d9e716631be2a49c72bb
|
||||
content/31: d1c7c19aae4736f403b0df60cb6e48a4
|
||||
content/32: 9e5a786192608844493dfbb6e4100886
|
||||
content/33: 8961e5fb3a49bb48580b23dfd5e053c6
|
||||
content/34: 1022391b9b79b1d2e9f8db789f9c50a2
|
||||
3fd794279590b9d143d409252c8bcf91:
|
||||
meta/title: 439cb79e8dfd7923d35b85cfbb6fd201
|
||||
content/0: dc697d1a69b3cb0152fda60b44bc7da1
|
||||
@@ -4567,11 +4570,11 @@ checksums:
|
||||
content/10: d19c8c67f52eb08b6a49c0969a9c8b86
|
||||
content/11: 4024a36e0d9479ff3191fb9cd2b2e365
|
||||
content/12: 0396a1e5d9548207f56e6b6cae85a542
|
||||
content/13: e3e263fb516c8a5413e94064e7700410
|
||||
content/14: 41bbe0664b778cddce4e14055f6342a9
|
||||
content/15: 8a274099c74e6b8dac89b8c413601d98
|
||||
content/16: 55cac8cbd4373a3d1b51c8bbc536a7ce
|
||||
content/17: 8159087a0aa1e5967545fa6ce86ec5f4
|
||||
content/13: 4bfdeac5ad21c75209dcdfde85aa52b0
|
||||
content/14: 35df9a16b866dbe4bb9fc1d7aee42711
|
||||
content/15: 135c044066cea8cc0e22f06d67754ec5
|
||||
content/16: 6882b91e30548d7d331388c26cf2e948
|
||||
content/17: 29aed7061148ae46fa6ec8bcbc857c3d
|
||||
content/18: e0571c88ea5bcd4305a6f5772dcbed98
|
||||
content/19: 83fc31418ff454a5e06b290e3708ef32
|
||||
content/20: 4392b5939a6d5774fb080cad1ee1dbb8
|
||||
@@ -4594,7 +4597,7 @@ checksums:
|
||||
content/37: 7bb928aba33a4013ad5f08487da5bbf9
|
||||
content/38: dbbf313837f13ddfa4a8843d71cb9cc4
|
||||
content/39: cf10560ae6defb8ee5da344fc6509f6e
|
||||
content/40: c5dc6e5de6e45b17ee1f5eb567a18e2f
|
||||
content/40: 1dea5c6442c127ae290185db0cef067b
|
||||
content/41: 332dab0588fb35dabb64b674ba6120eb
|
||||
content/42: 714b3f99b0a8686bbb3434deb1f682b3
|
||||
content/43: ba18ac99184b17d7e49bd1abdc814437
|
||||
@@ -47157,7 +47160,7 @@ checksums:
|
||||
content/9: b037aed60eb91ac0b010b6c1ce1a1a70
|
||||
content/10: b1c4181c4bc75edd5dfa188bcdd3b6c4
|
||||
content/11: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/12: a830a7e59569945973febaed7634fcf8
|
||||
content/12: a71a30e9f91c10daf481ea8f542e91f6
|
||||
content/13: bcadfc362b69078beee0088e5936c98b
|
||||
content/14: 59c08999f9c404330ebd8f8a7d21e1a1
|
||||
content/15: 49d191d312481589419c68a5506b0d71
|
||||
@@ -47169,7 +47172,7 @@ checksums:
|
||||
content/21: 2e70c0a22a98675a13b493b9761ff92f
|
||||
content/22: 107f6e51a1e896ee4d18f8ed4f82c50f
|
||||
content/23: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/24: 3a9d07c3ebf40ef00a67fb694dfbcf97
|
||||
content/24: e506fbf4b80deecb3b44b29b8dc3438b
|
||||
content/25: bcadfc362b69078beee0088e5936c98b
|
||||
content/26: a9096a341b00ce4f4891daaca2586d1c
|
||||
content/27: 934a0124aa2118682b2b17fa258ff06a
|
||||
@@ -47181,7 +47184,7 @@ checksums:
|
||||
content/33: 1a1e332b525e86f7fd92f9da1ac0096c
|
||||
content/34: 00098e1591c0f80ef6287d934d391409
|
||||
content/35: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/36: 991ec136b689e0dc95719260a0cef14a
|
||||
content/36: e52688ff2fa61ce71026f33930e1ec86
|
||||
content/37: bcadfc362b69078beee0088e5936c98b
|
||||
content/38: d84fb23e5dfc9d41a177acd7dfb28e72
|
||||
content/39: 17be090a79154f557bc96f940c687aea
|
||||
@@ -47193,7 +47196,7 @@ checksums:
|
||||
content/45: c76943404f9c8d34a85e6315359ed0c4
|
||||
content/46: b5e111e430aa1c929fb07d5844bf65eb
|
||||
content/47: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/48: 3e3ced1f6eb6c0ef39098531beb12598
|
||||
content/48: 6692edffddc28d3c64974ded23d1def2
|
||||
content/49: bcadfc362b69078beee0088e5936c98b
|
||||
content/50: dbc08cce26f9565e719891bbbf4632a9
|
||||
content/51: d0ce65f5420745c45ab42b7edd135bf4
|
||||
@@ -47205,37 +47208,37 @@ checksums:
|
||||
content/57: 440f2732ad006bee8cccc975fdbf673a
|
||||
content/58: 7a7048c54763b0109643f37e583381ce
|
||||
content/59: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/60: 5672a02f72bdb165f91b43e4ad24c4a9
|
||||
content/60: 11ad0a529a7fcc5892ae811cde6894f6
|
||||
content/61: bcadfc362b69078beee0088e5936c98b
|
||||
content/62: c7055d8ce044e49929d4f005a28d7c0a
|
||||
content/63: 2d7bad4340c1bc6a28e836e180e26c00
|
||||
content/64: 576dbecf29644e7abf59d25ffda5728c
|
||||
content/65: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/66: 61a490d594e10484da99d6b0e8ee684d
|
||||
content/66: 59015900ce6b64caff0784491ec59ff9
|
||||
content/67: bcadfc362b69078beee0088e5936c98b
|
||||
content/68: 2f225a893086726db6b6a994cc8a5e3c
|
||||
content/69: 63cbf703cf33e0fee06f12fb23184352
|
||||
content/70: dae1fda5ec57e1b598a7e2596007a775
|
||||
content/71: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/72: 7f00464f8e9368368ed1104fba516e5d
|
||||
content/72: 757f42df5247f2e6684ab32888d30e11
|
||||
content/73: bcadfc362b69078beee0088e5936c98b
|
||||
content/74: 380f805a5118dd4957f4fcce41e01b86
|
||||
content/75: 935f1a713d05f32d3d826434a7e715ee
|
||||
content/76: e505d8f656fb6e3b65a98cb73d744598
|
||||
content/77: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/78: e218c4f319e6a6a50c0535d3ee5e8fcf
|
||||
content/78: 2e77859b0f2c89186fc6a2d51287ea47
|
||||
content/79: bcadfc362b69078beee0088e5936c98b
|
||||
content/80: 22bd99d5b844817b808b9d0d3baddac4
|
||||
content/81: e959b48af94a559e9c46cbd7653d2dd2
|
||||
content/82: 5e3c04c5a9fabfceb7fcc00215f93bf9
|
||||
content/83: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/84: 8382a2ecb67d09244a2f004e098e0b46
|
||||
content/84: a92b2a22061ee6fd453af32e0155f5aa
|
||||
content/85: bcadfc362b69078beee0088e5936c98b
|
||||
content/86: d84fb23e5dfc9d41a177acd7dfb28e72
|
||||
content/87: c886f11a0852010b90a1032b97118920
|
||||
content/88: c60c832c08f9e1ff5f91565bf4ba549e
|
||||
content/89: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/90: 9019e9ff616caa381458fe1526c87840
|
||||
content/90: 1545794f4e8e696db96c3b660de684ec
|
||||
content/91: bcadfc362b69078beee0088e5936c98b
|
||||
content/92: 573530e346d195727862b03b380f40fc
|
||||
content/93: 3d31dedf076ec23547189a3eb5fe04c4
|
||||
@@ -47247,7 +47250,7 @@ checksums:
|
||||
content/99: e1a03f917ad8b0a1ebec9a601aa3eede
|
||||
content/100: 3aa857b8f85da07ee2d87e65c95b76d0
|
||||
content/101: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/102: c9e677ff65fd547dffa46b9c517402ac
|
||||
content/102: cc49a24c087d08717866a162cc47776c
|
||||
content/103: bcadfc362b69078beee0088e5936c98b
|
||||
content/104: c6d621ee3cdc66de2c20b70a39aafe12
|
||||
content/105: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
|
||||
@@ -41,7 +41,7 @@ interface PricingTier {
|
||||
* Free plan features with consistent icons
|
||||
*/
|
||||
const FREE_PLAN_FEATURES: PricingFeature[] = [
|
||||
{ icon: DollarSign, text: '$10 usage limit' },
|
||||
{ icon: DollarSign, text: '$20 usage limit' },
|
||||
{ icon: HardDrive, text: '5GB file storage' },
|
||||
{ icon: Workflow, text: 'Public template access' },
|
||||
{ icon: Database, text: 'Limited log retention' },
|
||||
|
||||
@@ -70,19 +70,6 @@ vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/api/workflows/[id]/execute/route', () => ({
|
||||
createFilteredResult: vi.fn().mockImplementation((result: any) => ({
|
||||
...result,
|
||||
logs: undefined,
|
||||
metadata: result.metadata
|
||||
? {
|
||||
...result.metadata,
|
||||
workflowConnections: undefined,
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('Chat Identifier API Route', () => {
|
||||
const mockAddCorsHeaders = vi.fn().mockImplementation((response) => response)
|
||||
const mockValidateChatAuth = vi.fn().mockResolvedValue({ authorized: true })
|
||||
|
||||
@@ -206,7 +206,6 @@ export async function POST(
|
||||
|
||||
const { createStreamingResponse } = await import('@/lib/workflows/streaming/streaming')
|
||||
const { SSE_HEADERS } = await import('@/lib/core/utils/sse')
|
||||
const { createFilteredResult } = await import('@/app/api/workflows/[id]/execute/route')
|
||||
|
||||
const workflowInput: any = { input, conversationId }
|
||||
if (files && Array.isArray(files) && files.length > 0) {
|
||||
@@ -267,7 +266,6 @@ export async function POST(
|
||||
isSecureMode: true,
|
||||
workflowTriggerType: 'chat',
|
||||
},
|
||||
createFilteredResult,
|
||||
executionId,
|
||||
})
|
||||
|
||||
|
||||
@@ -34,13 +34,24 @@ const CreateDocumentSchema = z.object({
|
||||
documentTagsData: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Schema for bulk document creation with processing options
|
||||
*
|
||||
* Processing options units:
|
||||
* - chunkSize: tokens (1 token ≈ 4 characters)
|
||||
* - minCharactersPerChunk: characters
|
||||
* - chunkOverlap: characters
|
||||
*/
|
||||
const BulkCreateDocumentsSchema = z.object({
|
||||
documents: z.array(CreateDocumentSchema),
|
||||
processingOptions: z.object({
|
||||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||||
chunkSize: z.number().min(100).max(4000),
|
||||
/** Minimum chunk size in characters */
|
||||
minCharactersPerChunk: z.number().min(1).max(2000),
|
||||
recipe: z.string(),
|
||||
lang: z.string(),
|
||||
/** Overlap between chunks in characters */
|
||||
chunkOverlap: z.number().min(0).max(500),
|
||||
}),
|
||||
bulk: z.literal(true),
|
||||
|
||||
@@ -12,6 +12,14 @@ import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/a
|
||||
|
||||
const logger = createLogger('KnowledgeBaseByIdAPI')
|
||||
|
||||
/**
|
||||
* Schema for updating a knowledge base
|
||||
*
|
||||
* Chunking config units:
|
||||
* - maxSize: tokens (1 token ≈ 4 characters)
|
||||
* - minSize: characters
|
||||
* - overlap: tokens (1 token ≈ 4 characters)
|
||||
*/
|
||||
const UpdateKnowledgeBaseSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').optional(),
|
||||
description: z.string().optional(),
|
||||
@@ -20,10 +28,23 @@ const UpdateKnowledgeBaseSchema = z.object({
|
||||
workspaceId: z.string().nullable().optional(),
|
||||
chunkingConfig: z
|
||||
.object({
|
||||
maxSize: z.number(),
|
||||
minSize: z.number(),
|
||||
overlap: z.number(),
|
||||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||||
maxSize: z.number().min(100).max(4000),
|
||||
/** Minimum chunk size in characters */
|
||||
minSize: z.number().min(1).max(2000),
|
||||
/** Overlap between chunks in characters */
|
||||
overlap: z.number().min(0).max(500),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Convert maxSize from tokens to characters for comparison (1 token ≈ 4 chars)
|
||||
const maxSizeInChars = data.maxSize * 4
|
||||
return data.minSize < maxSizeInChars
|
||||
},
|
||||
{
|
||||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
|
||||
@@ -139,8 +139,8 @@ describe('Knowledge Base API Route', () => {
|
||||
const invalidData = {
|
||||
name: 'Test KB',
|
||||
chunkingConfig: {
|
||||
maxSize: 100,
|
||||
minSize: 200, // Invalid: minSize > maxSize
|
||||
maxSize: 100, // 100 tokens = 400 characters
|
||||
minSize: 500, // Invalid: minSize (500 chars) > maxSize (400 chars)
|
||||
overlap: 50,
|
||||
},
|
||||
}
|
||||
@@ -168,7 +168,7 @@ describe('Knowledge Base API Route', () => {
|
||||
expect(data.data.embeddingDimension).toBe(1536)
|
||||
expect(data.data.chunkingConfig).toEqual({
|
||||
maxSize: 1024,
|
||||
minSize: 1,
|
||||
minSize: 100,
|
||||
overlap: 200,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,14 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('KnowledgeBaseAPI')
|
||||
|
||||
/**
|
||||
* Schema for creating a knowledge base
|
||||
*
|
||||
* Chunking config units:
|
||||
* - maxSize: tokens (1 token ≈ 4 characters)
|
||||
* - minSize: characters
|
||||
* - overlap: tokens (1 token ≈ 4 characters)
|
||||
*/
|
||||
const CreateKnowledgeBaseSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
description: z.string().optional(),
|
||||
@@ -15,18 +23,28 @@ const CreateKnowledgeBaseSchema = z.object({
|
||||
embeddingDimension: z.literal(1536).default(1536),
|
||||
chunkingConfig: z
|
||||
.object({
|
||||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||||
maxSize: z.number().min(100).max(4000).default(1024),
|
||||
minSize: z.number().min(1).max(2000).default(1),
|
||||
/** Minimum chunk size in characters */
|
||||
minSize: z.number().min(1).max(2000).default(100),
|
||||
/** Overlap between chunks in tokens (1 token ≈ 4 characters) */
|
||||
overlap: z.number().min(0).max(500).default(200),
|
||||
})
|
||||
.default({
|
||||
maxSize: 1024,
|
||||
minSize: 1,
|
||||
minSize: 100,
|
||||
overlap: 200,
|
||||
})
|
||||
.refine((data) => data.minSize < data.maxSize, {
|
||||
message: 'Min chunk size must be less than max chunk size',
|
||||
}),
|
||||
.refine(
|
||||
(data) => {
|
||||
// Convert maxSize from tokens to characters for comparison (1 token ≈ 4 chars)
|
||||
const maxSizeInChars = data.maxSize * 4
|
||||
return data.minSize < maxSizeInChars
|
||||
},
|
||||
{
|
||||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
|
||||
@@ -183,7 +183,12 @@ describe('Knowledge Utils', () => {
|
||||
|
||||
describe('processDocumentAsync', () => {
|
||||
it.concurrent('should insert embeddings before updating document counters', async () => {
|
||||
kbRows.push({ id: 'kb1', userId: 'user1', workspaceId: null })
|
||||
kbRows.push({
|
||||
id: 'kb1',
|
||||
userId: 'user1',
|
||||
workspaceId: null,
|
||||
chunkingConfig: { maxSize: 1024, minSize: 1, overlap: 200 },
|
||||
})
|
||||
docRows.push({ id: 'doc1', knowledgeBaseId: 'kb1' })
|
||||
|
||||
await processDocumentAsync(
|
||||
|
||||
@@ -1,54 +1,16 @@
|
||||
import { db } from '@sim/db'
|
||||
import { memory, workflowBlocks } from '@sim/db/schema'
|
||||
import { memory, permissions, workspace } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getWorkflowAccessContext } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('MemoryByIdAPI')
|
||||
|
||||
/**
|
||||
* Parse memory key into conversationId and blockId
|
||||
* Key format: conversationId:blockId
|
||||
*/
|
||||
function parseMemoryKey(key: string): { conversationId: string; blockId: string } | null {
|
||||
const parts = key.split(':')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
conversationId: parts[0],
|
||||
blockId: parts[1],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup block name from block ID
|
||||
*/
|
||||
async function getBlockName(blockId: string, workflowId: string): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await db
|
||||
.select({ name: workflowBlocks.name })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result[0].name
|
||||
} catch (error) {
|
||||
logger.error('Error looking up block name', { error, blockId, workflowId })
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const memoryQuerySchema = z.object({
|
||||
workflowId: z.string().uuid('Invalid workflow ID format'),
|
||||
workspaceId: z.string().uuid('Invalid workspace ID format'),
|
||||
})
|
||||
|
||||
const agentMemoryDataSchema = z.object({
|
||||
@@ -64,26 +26,56 @@ const memoryPutBodySchema = z.object({
|
||||
data: z.union([agentMemoryDataSchema, genericMemoryDataSchema], {
|
||||
errorMap: () => ({ message: 'Invalid memory data structure' }),
|
||||
}),
|
||||
workflowId: z.string().uuid('Invalid workflow ID format'),
|
||||
workspaceId: z.string().uuid('Invalid workspace ID format'),
|
||||
})
|
||||
|
||||
/**
|
||||
* Validates authentication and workflow access for memory operations
|
||||
* @param request - The incoming request
|
||||
* @param workflowId - The workflow ID to check access for
|
||||
* @param requestId - Request ID for logging
|
||||
* @param action - 'read' for GET, 'write' for PUT/DELETE
|
||||
* @returns Object with userId if successful, or error response if failed
|
||||
*/
|
||||
async function checkWorkspaceAccess(
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<{ hasAccess: boolean; canWrite: boolean }> {
|
||||
const [workspaceRow] = await db
|
||||
.select({ ownerId: workspace.ownerId })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceRow) {
|
||||
return { hasAccess: false, canWrite: false }
|
||||
}
|
||||
|
||||
if (workspaceRow.ownerId === userId) {
|
||||
return { hasAccess: true, canWrite: true }
|
||||
}
|
||||
|
||||
const [permissionRow] = await db
|
||||
.select({ permissionType: permissions.permissionType })
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, userId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!permissionRow) {
|
||||
return { hasAccess: false, canWrite: false }
|
||||
}
|
||||
|
||||
return {
|
||||
hasAccess: true,
|
||||
canWrite: permissionRow.permissionType === 'write' || permissionRow.permissionType === 'admin',
|
||||
}
|
||||
}
|
||||
|
||||
async function validateMemoryAccess(
|
||||
request: NextRequest,
|
||||
workflowId: string,
|
||||
workspaceId: string,
|
||||
requestId: string,
|
||||
action: 'read' | 'write'
|
||||
): Promise<{ userId: string } | { error: NextResponse }> {
|
||||
const authResult = await checkHybridAuth(request, {
|
||||
requireWorkflowId: false,
|
||||
})
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`)
|
||||
return {
|
||||
@@ -94,30 +86,20 @@ async function validateMemoryAccess(
|
||||
}
|
||||
}
|
||||
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found`)
|
||||
const { hasAccess, canWrite } = await checkWorkspaceAccess(workspaceId, authResult.userId)
|
||||
if (!hasAccess) {
|
||||
return {
|
||||
error: NextResponse.json(
|
||||
{ success: false, error: { message: 'Workflow not found' } },
|
||||
{ success: false, error: { message: 'Workspace not found' } },
|
||||
{ status: 404 }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const { isOwner, workspacePermission } = accessContext
|
||||
const hasAccess =
|
||||
action === 'read'
|
||||
? isOwner || workspacePermission !== null
|
||||
: isOwner || workspacePermission === 'write' || workspacePermission === 'admin'
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied ${action} access to workflow ${workflowId}`
|
||||
)
|
||||
if (action === 'write' && !canWrite) {
|
||||
return {
|
||||
error: NextResponse.json(
|
||||
{ success: false, error: { message: 'Access denied' } },
|
||||
{ success: false, error: { message: 'Write access denied' } },
|
||||
{ status: 403 }
|
||||
),
|
||||
}
|
||||
@@ -129,40 +111,28 @@ async function validateMemoryAccess(
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
/**
|
||||
* GET handler for retrieving a specific memory by ID
|
||||
*/
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = generateRequestId()
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
logger.info(`[${requestId}] Processing memory get request for ID: ${id}`)
|
||||
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
|
||||
const validation = memoryQuerySchema.safeParse({ workflowId })
|
||||
const workspaceId = url.searchParams.get('workspaceId')
|
||||
|
||||
const validation = memoryQuerySchema.safeParse({ workspaceId })
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: errorMessage } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workflowId: validatedWorkflowId } = validation.data
|
||||
const { workspaceId: validatedWorkspaceId } = validation.data
|
||||
|
||||
const accessCheck = await validateMemoryAccess(request, validatedWorkflowId, requestId, 'read')
|
||||
const accessCheck = await validateMemoryAccess(request, validatedWorkspaceId, requestId, 'read')
|
||||
if ('error' in accessCheck) {
|
||||
return accessCheck.error
|
||||
}
|
||||
@@ -170,72 +140,33 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const memories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
|
||||
.orderBy(memory.createdAt)
|
||||
.limit(1)
|
||||
|
||||
if (memories.length === 0) {
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${validatedWorkflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory not found',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory not found' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const mem = memories[0]
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
|
||||
let enrichedMemory
|
||||
if (!parsed) {
|
||||
enrichedMemory = {
|
||||
conversationId: mem.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: mem.data,
|
||||
}
|
||||
} else {
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = (await getBlockName(blockId, validatedWorkflowId)) || 'unknown'
|
||||
|
||||
enrichedMemory = {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: mem.data,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Memory retrieved successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
logger.info(`[${requestId}] Memory retrieved: ${id} for workspace: ${validatedWorkspaceId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: enrichedMemory,
|
||||
},
|
||||
{ success: true, data: { conversationId: mem.key, data: mem.data } },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error retrieving memory`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to retrieve memory',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: error.message || 'Failed to retrieve memory' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE handler for removing a specific memory
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -244,32 +175,28 @@ export async function DELETE(
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
logger.info(`[${requestId}] Processing memory delete request for ID: ${id}`)
|
||||
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
|
||||
const validation = memoryQuerySchema.safeParse({ workflowId })
|
||||
const workspaceId = url.searchParams.get('workspaceId')
|
||||
|
||||
const validation = memoryQuerySchema.safeParse({ workspaceId })
|
||||
if (!validation.success) {
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: errorMessage } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workflowId: validatedWorkflowId } = validation.data
|
||||
const { workspaceId: validatedWorkspaceId } = validation.data
|
||||
|
||||
const accessCheck = await validateMemoryAccess(request, validatedWorkflowId, requestId, 'write')
|
||||
const accessCheck = await validateMemoryAccess(
|
||||
request,
|
||||
validatedWorkspaceId,
|
||||
requestId,
|
||||
'write'
|
||||
)
|
||||
if ('error' in accessCheck) {
|
||||
return accessCheck.error
|
||||
}
|
||||
@@ -277,61 +204,41 @@ export async function DELETE(
|
||||
const existingMemory = await db
|
||||
.select({ id: memory.id })
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
|
||||
.limit(1)
|
||||
|
||||
if (existingMemory.length === 0) {
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${validatedWorkflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory not found',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory not found' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Memory deleted successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
logger.info(`[${requestId}] Memory deleted: ${id} for workspace: ${validatedWorkspaceId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: { message: 'Memory deleted successfully' },
|
||||
},
|
||||
{ success: true, data: { message: 'Memory deleted successfully' } },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error deleting memory`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to delete memory',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: error.message || 'Failed to delete memory' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT handler for updating a specific memory
|
||||
*/
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = generateRequestId()
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
logger.info(`[${requestId}] Processing memory update request for ID: ${id}`)
|
||||
|
||||
let validatedData
|
||||
let validatedWorkflowId
|
||||
let validatedWorkspaceId
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validation = memoryPutBodySchema.safeParse(body)
|
||||
@@ -340,34 +247,27 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const errorMessage = validation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Invalid request body: ${errorMessage}`,
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: `Invalid request body: ${errorMessage}` } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
validatedData = validation.data.data
|
||||
validatedWorkflowId = validation.data.workflowId
|
||||
} catch (error: any) {
|
||||
logger.warn(`[${requestId}] Failed to parse request body: ${error.message}`)
|
||||
validatedWorkspaceId = validation.data.workspaceId
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid JSON in request body',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Invalid JSON in request body' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const accessCheck = await validateMemoryAccess(request, validatedWorkflowId, requestId, 'write')
|
||||
const accessCheck = await validateMemoryAccess(
|
||||
request,
|
||||
validatedWorkspaceId,
|
||||
requestId,
|
||||
'write'
|
||||
)
|
||||
if ('error' in accessCheck) {
|
||||
return accessCheck.error
|
||||
}
|
||||
@@ -375,18 +275,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const existingMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
|
||||
.limit(1)
|
||||
|
||||
if (existingMemories.length === 0) {
|
||||
logger.warn(`[${requestId}] Memory not found: ${id} for workflow: ${validatedWorkflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory not found',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory not found' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
@@ -396,14 +290,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const errorMessage = agentValidation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Agent memory validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Invalid agent memory data: ${errorMessage}`,
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: `Invalid agent memory data: ${errorMessage}` } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
@@ -411,59 +299,26 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
const now = new Date()
|
||||
await db
|
||||
.update(memory)
|
||||
.set({
|
||||
data: validatedData,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.set({ data: validatedData, updatedAt: now })
|
||||
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
|
||||
|
||||
const updatedMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.where(and(eq(memory.key, id), eq(memory.workspaceId, validatedWorkspaceId)))
|
||||
.limit(1)
|
||||
|
||||
const mem = updatedMemories[0]
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
|
||||
let enrichedMemory
|
||||
if (!parsed) {
|
||||
enrichedMemory = {
|
||||
conversationId: mem.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: mem.data,
|
||||
}
|
||||
} else {
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = (await getBlockName(blockId, validatedWorkflowId)) || 'unknown'
|
||||
|
||||
enrichedMemory = {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: mem.data,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Memory updated successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
logger.info(`[${requestId}] Memory updated: ${id} for workspace: ${validatedWorkspaceId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: enrichedMemory,
|
||||
},
|
||||
{ success: true, data: { conversationId: mem.key, data: mem.data } },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error updating memory`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to update memory',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: error.message || 'Failed to update memory' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,56 @@
|
||||
import { db } from '@sim/db'
|
||||
import { memory, workflowBlocks } from '@sim/db/schema'
|
||||
import { and, eq, inArray, isNull, like } from 'drizzle-orm'
|
||||
import { memory, permissions, workspace } from '@sim/db/schema'
|
||||
import { and, eq, isNull, like } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getWorkflowAccessContext } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('MemoryAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
/**
|
||||
* Parse memory key into conversationId and blockId
|
||||
* Key format: conversationId:blockId
|
||||
* @param key The memory key to parse
|
||||
* @returns Object with conversationId and blockId, or null if invalid
|
||||
*/
|
||||
function parseMemoryKey(key: string): { conversationId: string; blockId: string } | null {
|
||||
const parts = key.split(':')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
async function checkWorkspaceAccess(
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<{ hasAccess: boolean; canWrite: boolean }> {
|
||||
const [workspaceRow] = await db
|
||||
.select({ ownerId: workspace.ownerId })
|
||||
.from(workspace)
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.limit(1)
|
||||
|
||||
if (!workspaceRow) {
|
||||
return { hasAccess: false, canWrite: false }
|
||||
}
|
||||
|
||||
if (workspaceRow.ownerId === userId) {
|
||||
return { hasAccess: true, canWrite: true }
|
||||
}
|
||||
|
||||
const [permissionRow] = await db
|
||||
.select({ permissionType: permissions.permissionType })
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, userId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!permissionRow) {
|
||||
return { hasAccess: false, canWrite: false }
|
||||
}
|
||||
|
||||
return {
|
||||
conversationId: parts[0],
|
||||
blockId: parts[1],
|
||||
hasAccess: true,
|
||||
canWrite: permissionRow.permissionType === 'write' || permissionRow.permissionType === 'admin',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET handler for searching and retrieving memories
|
||||
* Supports query parameters:
|
||||
* - query: Search string for memory keys
|
||||
* - type: Filter by memory type
|
||||
* - limit: Maximum number of results (default: 50)
|
||||
* - workflowId: Filter by workflow ID (required)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -45,102 +59,32 @@ export async function GET(request: NextRequest) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory access attempt`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: authResult.error || 'Authentication required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: authResult.error || 'Authentication required' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing memory search request`)
|
||||
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const workspaceId = url.searchParams.get('workspaceId')
|
||||
const searchQuery = url.searchParams.get('query')
|
||||
const blockNameFilter = url.searchParams.get('blockName')
|
||||
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required parameter: workflowId`)
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId parameter is required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'workspaceId parameter is required' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found for user ${authResult.userId}`)
|
||||
const { hasAccess } = await checkWorkspaceAccess(workspaceId, authResult.userId)
|
||||
if (!hasAccess) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workspacePermission, isOwner } = accessContext
|
||||
|
||||
if (!isOwner && !workspacePermission) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied access to workflow ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Access denied to this workflow',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Access denied to this workspace' } },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] User ${authResult.userId} (${authResult.authType}) accessing memories for workflow ${workflowId}`
|
||||
)
|
||||
|
||||
const conditions = []
|
||||
|
||||
conditions.push(isNull(memory.deletedAt))
|
||||
|
||||
conditions.push(eq(memory.workflowId, workflowId))
|
||||
|
||||
let blockIdsToFilter: string[] | null = null
|
||||
if (blockNameFilter) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(
|
||||
and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.name, blockNameFilter))
|
||||
)
|
||||
|
||||
if (blocks.length === 0) {
|
||||
logger.info(
|
||||
`[${requestId}] No blocks found with name "${blockNameFilter}" for workflow: ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: { memories: [] },
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
blockIdsToFilter = blocks.map((b) => b.id)
|
||||
}
|
||||
const conditions = [isNull(memory.deletedAt), eq(memory.workspaceId, workspaceId)]
|
||||
|
||||
if (searchQuery) {
|
||||
conditions.push(like(memory.key, `%${searchQuery}%`))
|
||||
@@ -153,95 +97,27 @@ export async function GET(request: NextRequest) {
|
||||
.orderBy(memory.createdAt)
|
||||
.limit(limit)
|
||||
|
||||
const filteredMemories = blockIdsToFilter
|
||||
? rawMemories.filter((mem) => {
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
return parsed && blockIdsToFilter.includes(parsed.blockId)
|
||||
})
|
||||
: rawMemories
|
||||
|
||||
const blockIds = new Set<string>()
|
||||
const parsedKeys = new Map<string, { conversationId: string; blockId: string }>()
|
||||
|
||||
for (const mem of filteredMemories) {
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
if (parsed) {
|
||||
blockIds.add(parsed.blockId)
|
||||
parsedKeys.set(mem.key, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
const blockNameMap = new Map<string, string>()
|
||||
if (blockIds.size > 0) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id, name: workflowBlocks.name })
|
||||
.from(workflowBlocks)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowBlocks.workflowId, workflowId),
|
||||
inArray(workflowBlocks.id, Array.from(blockIds))
|
||||
)
|
||||
)
|
||||
|
||||
for (const block of blocks) {
|
||||
blockNameMap.set(block.id, block.name)
|
||||
}
|
||||
}
|
||||
|
||||
const enrichedMemories = filteredMemories.map((mem) => {
|
||||
const parsed = parsedKeys.get(mem.key)
|
||||
|
||||
if (!parsed) {
|
||||
return {
|
||||
conversationId: mem.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: mem.data,
|
||||
}
|
||||
}
|
||||
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = blockNameMap.get(blockId) || 'unknown'
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: mem.data,
|
||||
}
|
||||
})
|
||||
const enrichedMemories = rawMemories.map((mem) => ({
|
||||
conversationId: mem.key,
|
||||
data: mem.data,
|
||||
}))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Found ${enrichedMemories.length} memories for workflow: ${workflowId}`
|
||||
`[${requestId}] Found ${enrichedMemories.length} memories for workspace: ${workspaceId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: { memories: enrichedMemories },
|
||||
},
|
||||
{ success: true, data: { memories: enrichedMemories } },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error searching memories`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to search memories',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: error.message || 'Failed to search memories' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST handler for creating new memories
|
||||
* Requires:
|
||||
* - key: Unique identifier for the memory (within workflow scope)
|
||||
* - type: Memory type ('agent')
|
||||
* - data: Memory content (agent message with role and content)
|
||||
* - workflowId: ID of the workflow this memory belongs to
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -250,123 +126,63 @@ export async function POST(request: NextRequest) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory creation attempt`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: authResult.error || 'Authentication required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: authResult.error || 'Authentication required' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing memory creation request`)
|
||||
|
||||
const body = await request.json()
|
||||
const { key, data, workflowId } = body
|
||||
const { key, data, workspaceId } = body
|
||||
|
||||
if (!key) {
|
||||
logger.warn(`[${requestId}] Missing required field: key`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory key is required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory key is required' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
logger.warn(`[${requestId}] Missing required field: data`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory data is required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory data is required' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required field: workflowId`)
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId is required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'workspaceId is required' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found for user ${authResult.userId}`)
|
||||
const { hasAccess, canWrite } = await checkWorkspaceAccess(workspaceId, authResult.userId)
|
||||
if (!hasAccess) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Workspace not found' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workspacePermission, isOwner } = accessContext
|
||||
|
||||
const hasWritePermission =
|
||||
isOwner || workspacePermission === 'write' || workspacePermission === 'admin'
|
||||
|
||||
if (!hasWritePermission) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied write access to workflow ${workflowId}`
|
||||
)
|
||||
if (!canWrite) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Write access denied to this workflow',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Write access denied to this workspace' } },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] User ${authResult.userId} (${authResult.authType}) creating memory for workflow ${workflowId}`
|
||||
)
|
||||
|
||||
const dataToValidate = Array.isArray(data) ? data : [data]
|
||||
|
||||
for (const msg of dataToValidate) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.role || !msg.content) {
|
||||
logger.warn(`[${requestId}] Missing required message fields`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory requires messages with role and content',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory requires messages with role and content' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['user', 'assistant', 'system'].includes(msg.role)) {
|
||||
logger.warn(`[${requestId}] Invalid message role: ${msg.role}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Message role must be user, assistant, or system',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Message role must be user, assistant, or system' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
@@ -382,114 +198,59 @@ export async function POST(request: NextRequest) {
|
||||
.insert(memory)
|
||||
.values({
|
||||
id,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
key,
|
||||
data: initialData,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [memory.workflowId, memory.key],
|
||||
target: [memory.workspaceId, memory.key],
|
||||
set: {
|
||||
data: sql`${memory.data} || ${JSON.stringify(initialData)}::jsonb`,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Memory operation successful (atomic): ${key} for workflow: ${workflowId}`
|
||||
)
|
||||
logger.info(`[${requestId}] Memory operation successful: ${key} for workspace: ${workspaceId}`)
|
||||
|
||||
const allMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId), isNull(memory.deletedAt)))
|
||||
.where(
|
||||
and(eq(memory.key, key), eq(memory.workspaceId, workspaceId), isNull(memory.deletedAt))
|
||||
)
|
||||
.orderBy(memory.createdAt)
|
||||
|
||||
if (allMemories.length === 0) {
|
||||
logger.warn(`[${requestId}] No memories found after creating/updating memory: ${key}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Failed to retrieve memory after creation/update',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Failed to retrieve memory after creation/update' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const memoryRecord = allMemories[0]
|
||||
const parsed = parseMemoryKey(memoryRecord.key)
|
||||
|
||||
let enrichedMemory
|
||||
if (!parsed) {
|
||||
enrichedMemory = {
|
||||
conversationId: memoryRecord.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: memoryRecord.data,
|
||||
}
|
||||
} else {
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = await (async () => {
|
||||
const blocks = await db
|
||||
.select({ name: workflowBlocks.name })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
|
||||
.limit(1)
|
||||
return blocks.length > 0 ? blocks[0].name : 'unknown'
|
||||
})()
|
||||
|
||||
enrichedMemory = {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: memoryRecord.data,
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: enrichedMemory,
|
||||
},
|
||||
{ success: true, data: { conversationId: memoryRecord.key, data: memoryRecord.data } },
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
if (error.code === '23505') {
|
||||
logger.warn(`[${requestId}] Duplicate key violation`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory with this key already exists',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Memory with this key already exists' } },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error creating memory`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to create memory',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: error.message || 'Failed to create memory' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE handler for pattern-based memory deletion
|
||||
* Supports query parameters:
|
||||
* - workflowId: Required
|
||||
* - conversationId: Optional - delete all memories for this conversation
|
||||
* - blockId: Optional - delete all memories for this block
|
||||
* - blockName: Optional - delete all memories for blocks with this name
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -498,175 +259,52 @@ export async function DELETE(request: NextRequest) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory deletion attempt`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: authResult.error || 'Authentication required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: authResult.error || 'Authentication required' } },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing memory deletion request`)
|
||||
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const workspaceId = url.searchParams.get('workspaceId')
|
||||
const conversationId = url.searchParams.get('conversationId')
|
||||
const blockId = url.searchParams.get('blockId')
|
||||
const blockName = url.searchParams.get('blockName')
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required parameter: workflowId`)
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId parameter is required',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'workspaceId parameter is required' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!conversationId && !blockId && !blockName) {
|
||||
logger.warn(`[${requestId}] No filter parameters provided`)
|
||||
if (!conversationId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'At least one of conversationId, blockId, or blockName must be provided',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'conversationId must be provided' } },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found for user ${authResult.userId}`)
|
||||
const { hasAccess, canWrite } = await checkWorkspaceAccess(workspaceId, authResult.userId)
|
||||
if (!hasAccess) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Workspace not found' } },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workspacePermission, isOwner } = accessContext
|
||||
|
||||
const hasWritePermission =
|
||||
isOwner || workspacePermission === 'write' || workspacePermission === 'admin'
|
||||
|
||||
if (!hasWritePermission) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied delete access to workflow ${workflowId}`
|
||||
)
|
||||
if (!canWrite) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Write access denied to this workflow',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: 'Write access denied to this workspace' } },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] User ${authResult.userId} (${authResult.authType}) deleting memories for workflow ${workflowId}`
|
||||
)
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, conversationId), eq(memory.workspaceId, workspaceId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
let deletedCount = 0
|
||||
const deletedCount = result.length
|
||||
|
||||
if (conversationId && blockId) {
|
||||
const key = `${conversationId}:${blockId}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount = result.length
|
||||
} else if (conversationId && blockName) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.name, blockName)))
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
message: `No blocks found with name "${blockName}"`,
|
||||
deletedCount: 0,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
const key = `${conversationId}:${block.id}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount += result.length
|
||||
}
|
||||
} else if (conversationId) {
|
||||
const pattern = `${conversationId}:%`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(like(memory.key, pattern), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount = result.length
|
||||
} else if (blockId) {
|
||||
const pattern = `%:${blockId}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(like(memory.key, pattern), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount = result.length
|
||||
} else if (blockName) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.name, blockName)))
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
message: `No blocks found with name "${blockName}"`,
|
||||
deletedCount: 0,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
const pattern = `%:${block.id}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(like(memory.key, pattern), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount += result.length
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully deleted ${deletedCount} memories for workflow: ${workflowId}`
|
||||
)
|
||||
logger.info(`[${requestId}] Deleted ${deletedCount} memories for workspace: ${workspaceId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
@@ -683,12 +321,7 @@ export async function DELETE(request: NextRequest) {
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error deleting memories`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to delete memories',
|
||||
},
|
||||
},
|
||||
{ success: false, error: { message: error.message || 'Failed to delete memories' } },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,29 @@ const logger = createLogger('OpenRouterModelsAPI')
|
||||
|
||||
interface OpenRouterModel {
|
||||
id: string
|
||||
context_length?: number
|
||||
supported_parameters?: string[]
|
||||
pricing?: {
|
||||
prompt?: string
|
||||
completion?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface OpenRouterResponse {
|
||||
data: OpenRouterModel[]
|
||||
}
|
||||
|
||||
export interface OpenRouterModelInfo {
|
||||
id: string
|
||||
contextLength?: number
|
||||
supportsStructuredOutputs?: boolean
|
||||
supportsTools?: boolean
|
||||
pricing?: {
|
||||
input: number
|
||||
output: number
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
||||
@@ -24,23 +41,51 @@ export async function GET(_request: NextRequest) {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
return NextResponse.json({ models: [] })
|
||||
return NextResponse.json({ models: [], modelInfo: {} })
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OpenRouterResponse
|
||||
const allModels = Array.from(new Set(data.data?.map((model) => `openrouter/${model.id}`) ?? []))
|
||||
const models = filterBlacklistedModels(allModels)
|
||||
|
||||
const modelInfo: Record<string, OpenRouterModelInfo> = {}
|
||||
const allModels: string[] = []
|
||||
|
||||
for (const model of data.data ?? []) {
|
||||
const modelId = `openrouter/${model.id}`
|
||||
allModels.push(modelId)
|
||||
|
||||
const supportedParams = model.supported_parameters ?? []
|
||||
modelInfo[modelId] = {
|
||||
id: modelId,
|
||||
contextLength: model.context_length,
|
||||
supportsStructuredOutputs: supportedParams.includes('structured_outputs'),
|
||||
supportsTools: supportedParams.includes('tools'),
|
||||
pricing: model.pricing
|
||||
? {
|
||||
input: Number.parseFloat(model.pricing.prompt ?? '0') * 1000000,
|
||||
output: Number.parseFloat(model.pricing.completion ?? '0') * 1000000,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueModels = Array.from(new Set(allModels))
|
||||
const models = filterBlacklistedModels(uniqueModels)
|
||||
|
||||
const structuredOutputCount = Object.values(modelInfo).filter(
|
||||
(m) => m.supportsStructuredOutputs
|
||||
).length
|
||||
|
||||
logger.info('Successfully fetched OpenRouter models', {
|
||||
count: models.length,
|
||||
filtered: allModels.length - models.length,
|
||||
filtered: uniqueModels.length - models.length,
|
||||
withStructuredOutputs: structuredOutputCount,
|
||||
})
|
||||
|
||||
return NextResponse.json({ models })
|
||||
return NextResponse.json({ models, modelInfo })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching OpenRouter models', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
return NextResponse.json({ models: [] })
|
||||
return NextResponse.json({ models: [], modelInfo: {} })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
|
||||
query: validated.query,
|
||||
type: 'auto',
|
||||
useAutoprompt: true,
|
||||
text: true,
|
||||
highlights: true,
|
||||
apiKey: env.EXA_API_KEY,
|
||||
})
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function POST(request: NextRequest) {
|
||||
const results = (result.output.results || []).map((r: any, index: number) => ({
|
||||
title: r.title || '',
|
||||
link: r.url || '',
|
||||
snippet: r.text || '',
|
||||
snippet: Array.isArray(r.highlights) ? r.highlights.join(' ... ') : '',
|
||||
date: r.publishedDate || undefined,
|
||||
position: index + 1,
|
||||
}))
|
||||
|
||||
@@ -69,7 +69,7 @@ function safeStringify(value: unknown): string {
|
||||
}
|
||||
|
||||
async function updateUserStatsForWand(
|
||||
workflowId: string,
|
||||
userId: string,
|
||||
usage: {
|
||||
prompt_tokens?: number
|
||||
completion_tokens?: number
|
||||
@@ -88,21 +88,6 @@ async function updateUserStatsForWand(
|
||||
}
|
||||
|
||||
try {
|
||||
const [workflowRecord] = await db
|
||||
.select({ userId: workflow.userId, workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord?.userId) {
|
||||
logger.warn(
|
||||
`[${requestId}] No user found for workflow ${workflowId}, cannot update user stats`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const userId = workflowRecord.userId
|
||||
const workspaceId = workflowRecord.workspaceId
|
||||
const totalTokens = usage.total_tokens || 0
|
||||
const promptTokens = usage.prompt_tokens || 0
|
||||
const completionTokens = usage.completion_tokens || 0
|
||||
@@ -146,8 +131,6 @@ async function updateUserStatsForWand(
|
||||
inputTokens: promptTokens,
|
||||
outputTokens: completionTokens,
|
||||
cost: costToStore,
|
||||
workspaceId: workspaceId ?? undefined,
|
||||
workflowId,
|
||||
})
|
||||
|
||||
await checkAndBillOverageThreshold(userId)
|
||||
@@ -325,6 +308,11 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
if (data === '[DONE]') {
|
||||
logger.info(`[${requestId}] Received [DONE] signal`)
|
||||
|
||||
if (finalUsage) {
|
||||
await updateUserStatsForWand(session.user.id, finalUsage, requestId)
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`)
|
||||
)
|
||||
@@ -353,10 +341,6 @@ export async function POST(req: NextRequest) {
|
||||
`[${requestId}] Received usage data: ${JSON.stringify(parsed.usage)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (chunkCount % 10 === 0) {
|
||||
logger.debug(`[${requestId}] Processed ${chunkCount} chunks`)
|
||||
}
|
||||
} catch (parseError) {
|
||||
logger.debug(
|
||||
`[${requestId}] Skipped non-JSON line: ${data.substring(0, 100)}`
|
||||
@@ -365,12 +349,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Wand generation streaming completed successfully`)
|
||||
|
||||
if (finalUsage && workflowId) {
|
||||
await updateUserStatsForWand(workflowId, finalUsage, requestId)
|
||||
}
|
||||
} catch (streamError: any) {
|
||||
logger.error(`[${requestId}] Streaming error`, {
|
||||
name: streamError?.name,
|
||||
@@ -438,8 +416,8 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
logger.info(`[${requestId}] Wand generation successful`)
|
||||
|
||||
if (completion.usage && workflowId) {
|
||||
await updateUserStatsForWand(workflowId, completion.usage, requestId)
|
||||
if (completion.usage) {
|
||||
await updateUserStatsForWand(session.user.id, completion.usage, requestId)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, content: generatedContent })
|
||||
|
||||
@@ -49,126 +49,6 @@ const ExecuteWorkflowSchema = z.object({
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Execute workflow with streaming support - used by chat and other streaming endpoints
|
||||
*
|
||||
* This function assumes preprocessing has already been completed.
|
||||
* Callers must run preprocessExecution() first to validate workflow, check usage limits,
|
||||
* and resolve actor before calling this function.
|
||||
*
|
||||
* This is a wrapper function that:
|
||||
* - Supports streaming callbacks (onStream, onBlockComplete)
|
||||
* - Returns ExecutionResult instead of NextResponse
|
||||
* - Handles pause/resume logic
|
||||
*
|
||||
* Used by:
|
||||
* - Chat execution (/api/chat/[identifier]/route.ts)
|
||||
* - Streaming responses (lib/workflows/streaming.ts)
|
||||
*/
|
||||
export async function executeWorkflow(
|
||||
workflow: any,
|
||||
requestId: string,
|
||||
input: any | undefined,
|
||||
actorUserId: string,
|
||||
streamConfig?: {
|
||||
enabled: boolean
|
||||
selectedOutputs?: string[]
|
||||
isSecureMode?: boolean
|
||||
workflowTriggerType?: 'api' | 'chat'
|
||||
onStream?: (streamingExec: any) => Promise<void>
|
||||
onBlockComplete?: (blockId: string, output: any) => Promise<void>
|
||||
skipLoggingComplete?: boolean
|
||||
},
|
||||
providedExecutionId?: string
|
||||
): Promise<any> {
|
||||
const workflowId = workflow.id
|
||||
const executionId = providedExecutionId || uuidv4()
|
||||
const triggerType = streamConfig?.workflowTriggerType || 'api'
|
||||
const loggingSession = new LoggingSession(workflowId, executionId, triggerType, requestId)
|
||||
|
||||
try {
|
||||
const metadata: ExecutionMetadata = {
|
||||
requestId,
|
||||
executionId,
|
||||
workflowId,
|
||||
workspaceId: workflow.workspaceId,
|
||||
userId: actorUserId,
|
||||
workflowUserId: workflow.userId,
|
||||
triggerType,
|
||||
useDraftState: false,
|
||||
startTime: new Date().toISOString(),
|
||||
isClientSession: false,
|
||||
}
|
||||
|
||||
const snapshot = new ExecutionSnapshot(
|
||||
metadata,
|
||||
workflow,
|
||||
input,
|
||||
workflow.variables || {},
|
||||
streamConfig?.selectedOutputs || []
|
||||
)
|
||||
|
||||
const result = await executeWorkflowCore({
|
||||
snapshot,
|
||||
callbacks: {
|
||||
onStream: streamConfig?.onStream,
|
||||
onBlockComplete: streamConfig?.onBlockComplete
|
||||
? async (blockId: string, _blockName: string, _blockType: string, output: any) => {
|
||||
await streamConfig.onBlockComplete!(blockId, output)
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
loggingSession,
|
||||
})
|
||||
|
||||
if (result.status === 'paused') {
|
||||
if (!result.snapshotSeed) {
|
||||
logger.error(`[${requestId}] Missing snapshot seed for paused execution`, {
|
||||
executionId,
|
||||
})
|
||||
} else {
|
||||
await PauseResumeManager.persistPauseResult({
|
||||
workflowId,
|
||||
executionId,
|
||||
pausePoints: result.pausePoints || [],
|
||||
snapshotSeed: result.snapshotSeed,
|
||||
executorUserId: result.metadata?.userId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
await PauseResumeManager.processQueuedResumes(executionId)
|
||||
}
|
||||
|
||||
if (streamConfig?.skipLoggingComplete) {
|
||||
return {
|
||||
...result,
|
||||
_streamingMetadata: {
|
||||
loggingSession,
|
||||
processedInput: input,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Workflow execution failed:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function createFilteredResult(result: any) {
|
||||
return {
|
||||
...result,
|
||||
logs: undefined,
|
||||
metadata: result.metadata
|
||||
? {
|
||||
...result.metadata,
|
||||
workflowConnections: undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOutputIds(
|
||||
selectedOutputs: string[] | undefined,
|
||||
blocks: Record<string, any>
|
||||
@@ -606,7 +486,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
isSecureMode: false,
|
||||
workflowTriggerType: triggerType === 'chat' ? 'chat' : 'api',
|
||||
},
|
||||
createFilteredResult,
|
||||
executionId,
|
||||
})
|
||||
|
||||
@@ -743,14 +622,17 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
const onStream = async (streamingExec: StreamingExecution) => {
|
||||
const blockId = (streamingExec.execution as any).blockId
|
||||
|
||||
const reader = streamingExec.stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let chunkCount = 0
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
chunkCount++
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
sendEvent({
|
||||
type: 'stream:chunk',
|
||||
|
||||
@@ -16,6 +16,10 @@ const QueryParamsSchema = z.object({
|
||||
folderIds: z.string().optional(),
|
||||
triggers: z.string().optional(),
|
||||
level: z.string().optional(), // Supports comma-separated values: 'error,running'
|
||||
allTime: z
|
||||
.enum(['true', 'false'])
|
||||
.optional()
|
||||
.transform((v) => v === 'true'),
|
||||
})
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -29,17 +33,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
const userId = session.user.id
|
||||
|
||||
const end = qp.endTime ? new Date(qp.endTime) : new Date()
|
||||
const start = qp.startTime
|
||||
let end = qp.endTime ? new Date(qp.endTime) : new Date()
|
||||
let 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) {
|
||||
|
||||
const isAllTime = qp.allTime === true
|
||||
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
||||
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()
|
||||
@@ -75,23 +80,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
workflows: [],
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
segmentMs,
|
||||
segmentMs: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const workflowIdList = workflows.map((w) => w.id)
|
||||
|
||||
const logWhere = [
|
||||
inArray(workflowExecutionLogs.workflowId, workflowIdList),
|
||||
gte(workflowExecutionLogs.startedAt, start),
|
||||
lte(workflowExecutionLogs.startedAt, end),
|
||||
] as SQL[]
|
||||
const baseLogWhere = [inArray(workflowExecutionLogs.workflowId, workflowIdList)] as SQL[]
|
||||
if (qp.triggers) {
|
||||
const t = qp.triggers.split(',').filter(Boolean)
|
||||
logWhere.push(inArray(workflowExecutionLogs.trigger, t))
|
||||
baseLogWhere.push(inArray(workflowExecutionLogs.trigger, t))
|
||||
}
|
||||
|
||||
// Handle level filtering with support for derived statuses and multiple selections
|
||||
if (qp.level && qp.level !== 'all') {
|
||||
const levels = qp.level.split(',').filter(Boolean)
|
||||
const levelConditions: SQL[] = []
|
||||
@@ -100,21 +100,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
if (level === 'error') {
|
||||
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
|
||||
} else if (level === 'info') {
|
||||
// Completed info logs only
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
isNotNull(workflowExecutionLogs.endedAt)
|
||||
)
|
||||
if (condition) levelConditions.push(condition)
|
||||
} else if (level === 'running') {
|
||||
// Running logs: info level with no endedAt
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
isNull(workflowExecutionLogs.endedAt)
|
||||
)
|
||||
if (condition) levelConditions.push(condition)
|
||||
} else if (level === 'pending') {
|
||||
// Pending logs: info level with pause status indicators
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
or(
|
||||
@@ -132,10 +129,55 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
if (levelConditions.length > 0) {
|
||||
const combinedCondition =
|
||||
levelConditions.length === 1 ? levelConditions[0] : or(...levelConditions)
|
||||
if (combinedCondition) logWhere.push(combinedCondition)
|
||||
if (combinedCondition) baseLogWhere.push(combinedCondition)
|
||||
}
|
||||
}
|
||||
|
||||
if (isAllTime) {
|
||||
const boundsQuery = db
|
||||
.select({
|
||||
minDate: sql<Date>`MIN(${workflowExecutionLogs.startedAt})`,
|
||||
maxDate: sql<Date>`MAX(${workflowExecutionLogs.startedAt})`,
|
||||
})
|
||||
.from(workflowExecutionLogs)
|
||||
.leftJoin(
|
||||
pausedExecutions,
|
||||
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
|
||||
)
|
||||
.where(and(...baseLogWhere))
|
||||
|
||||
const [bounds] = await boundsQuery
|
||||
|
||||
if (bounds?.minDate && bounds?.maxDate) {
|
||||
start = new Date(bounds.minDate)
|
||||
end = new Date(Math.max(new Date(bounds.maxDate).getTime(), Date.now()))
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
workflows: workflows.map((wf) => ({
|
||||
workflowId: wf.id,
|
||||
workflowName: wf.name,
|
||||
segments: [],
|
||||
})),
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date().toISOString(),
|
||||
segmentMs: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (start >= end) {
|
||||
return NextResponse.json({ error: 'Invalid time range' }, { status: 400 })
|
||||
}
|
||||
|
||||
const totalMs = Math.max(1, end.getTime() - start.getTime())
|
||||
const segmentMs = Math.max(1, Math.floor(totalMs / Math.max(1, segments)))
|
||||
|
||||
const logWhere = [
|
||||
...baseLogWhere,
|
||||
gte(workflowExecutionLogs.startedAt, start),
|
||||
lte(workflowExecutionLogs.startedAt, end),
|
||||
]
|
||||
|
||||
const logs = await db
|
||||
.select({
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
|
||||
@@ -89,7 +89,7 @@ function buildTestPayload(subscription: typeof workspaceNotificationSubscription
|
||||
}
|
||||
|
||||
if (subscription.includeUsageData) {
|
||||
data.usage = { currentPeriodCost: 2.45, limit: 10, plan: 'pro', isExceeded: false }
|
||||
data.usage = { currentPeriodCost: 2.45, limit: 20, plan: 'pro', isExceeded: false }
|
||||
}
|
||||
|
||||
return { payload, timestamp }
|
||||
|
||||
@@ -314,22 +314,9 @@ export function useChatStreaming() {
|
||||
let finalContent = accumulatedText
|
||||
|
||||
if (formattedOutputs.length > 0) {
|
||||
const trimmedStreamingContent = accumulatedText.trim()
|
||||
|
||||
const uniqueOutputs = formattedOutputs.filter((output) => {
|
||||
const trimmedOutput = output.trim()
|
||||
if (!trimmedOutput) return false
|
||||
|
||||
// Skip outputs that exactly match the streamed content to avoid duplication
|
||||
if (trimmedStreamingContent && trimmedOutput === trimmedStreamingContent) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (uniqueOutputs.length > 0) {
|
||||
const combinedOutputs = uniqueOutputs.join('\n\n')
|
||||
const nonEmptyOutputs = formattedOutputs.filter((output) => output.trim())
|
||||
if (nonEmptyOutputs.length > 0) {
|
||||
const combinedOutputs = nonEmptyOutputs.join('\n\n')
|
||||
finalContent = finalContent
|
||||
? `${finalContent.trim()}\n\n${combinedOutputs}`
|
||||
: combinedOutputs
|
||||
|
||||
@@ -44,23 +44,33 @@ const FormSchema = z
|
||||
.max(100, 'Name must be less than 100 characters')
|
||||
.refine((value) => value.trim().length > 0, 'Name cannot be empty'),
|
||||
description: z.string().max(500, 'Description must be less than 500 characters').optional(),
|
||||
/** Minimum chunk size in characters */
|
||||
minChunkSize: z
|
||||
.number()
|
||||
.min(1, 'Min chunk size must be at least 1')
|
||||
.max(2000, 'Min chunk size must be less than 2000'),
|
||||
.min(1, 'Min chunk size must be at least 1 character')
|
||||
.max(2000, 'Min chunk size must be less than 2000 characters'),
|
||||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||||
maxChunkSize: z
|
||||
.number()
|
||||
.min(100, 'Max chunk size must be at least 100')
|
||||
.max(4000, 'Max chunk size must be less than 4000'),
|
||||
.min(100, 'Max chunk size must be at least 100 tokens')
|
||||
.max(4000, 'Max chunk size must be less than 4000 tokens'),
|
||||
/** Overlap between chunks in tokens */
|
||||
overlapSize: z
|
||||
.number()
|
||||
.min(0, 'Overlap size must be non-negative')
|
||||
.max(500, 'Overlap size must be less than 500'),
|
||||
})
|
||||
.refine((data) => data.minChunkSize < data.maxChunkSize, {
|
||||
message: 'Min chunk size must be less than max chunk size',
|
||||
path: ['minChunkSize'],
|
||||
.min(0, 'Overlap must be non-negative')
|
||||
.max(500, 'Overlap must be less than 500 tokens'),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Convert maxChunkSize from tokens to characters for comparison (1 token ≈ 4 chars)
|
||||
const maxChunkSizeInChars = data.maxChunkSize * 4
|
||||
return data.minChunkSize < maxChunkSizeInChars
|
||||
},
|
||||
{
|
||||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||||
path: ['minChunkSize'],
|
||||
}
|
||||
)
|
||||
|
||||
type FormValues = z.infer<typeof FormSchema>
|
||||
|
||||
@@ -123,7 +133,7 @@ export function CreateBaseModal({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
minChunkSize: 1,
|
||||
minChunkSize: 100,
|
||||
maxChunkSize: 1024,
|
||||
overlapSize: 200,
|
||||
},
|
||||
@@ -143,7 +153,7 @@ export function CreateBaseModal({
|
||||
reset({
|
||||
name: '',
|
||||
description: '',
|
||||
minChunkSize: 1,
|
||||
minChunkSize: 100,
|
||||
maxChunkSize: 1024,
|
||||
overlapSize: 200,
|
||||
})
|
||||
@@ -381,10 +391,10 @@ export function CreateBaseModal({
|
||||
<div className='space-y-[12px] rounded-[6px] bg-[var(--surface-6)] px-[12px] py-[14px]'>
|
||||
<div className='grid grid-cols-2 gap-[12px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='minChunkSize'>Min Chunk Size</Label>
|
||||
<Label htmlFor='minChunkSize'>Min Chunk Size (characters)</Label>
|
||||
<Input
|
||||
id='minChunkSize'
|
||||
placeholder='1'
|
||||
placeholder='100'
|
||||
{...register('minChunkSize', { valueAsNumber: true })}
|
||||
className={cn(errors.minChunkSize && 'border-[var(--text-error)]')}
|
||||
autoComplete='off'
|
||||
@@ -394,7 +404,7 @@ export function CreateBaseModal({
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='maxChunkSize'>Max Chunk Size</Label>
|
||||
<Label htmlFor='maxChunkSize'>Max Chunk Size (tokens)</Label>
|
||||
<Input
|
||||
id='maxChunkSize'
|
||||
placeholder='1024'
|
||||
@@ -408,7 +418,7 @@ export function CreateBaseModal({
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label htmlFor='overlapSize'>Overlap Size</Label>
|
||||
<Label htmlFor='overlapSize'>Overlap (tokens)</Label>
|
||||
<Input
|
||||
id='overlapSize'
|
||||
placeholder='200'
|
||||
@@ -419,6 +429,9 @@ export function CreateBaseModal({
|
||||
name='overlap-size'
|
||||
/>
|
||||
</div>
|
||||
<p className='text-[11px] text-[var(--text-muted)]'>
|
||||
1 token ≈ 4 characters. Max chunk size and overlap are in tokens.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export type { LineChartMultiSeries, LineChartPoint } from './line-chart'
|
||||
export { default, LineChart } from './line-chart'
|
||||
export { default, LineChart, type LineChartMultiSeries, type LineChartPoint } from './line-chart'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { formatDate, formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface LineChartMultiSeries {
|
||||
dashed?: boolean
|
||||
}
|
||||
|
||||
export function LineChart({
|
||||
function LineChartComponent({
|
||||
data,
|
||||
label,
|
||||
color,
|
||||
@@ -95,6 +95,133 @@ export function LineChart({
|
||||
|
||||
const hasExternalWrapper = !label || label === ''
|
||||
|
||||
const allSeries = useMemo(
|
||||
() =>
|
||||
(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) })),
|
||||
[series, label, color, data]
|
||||
)
|
||||
|
||||
const { maxValue, minValue, valueRange } = useMemo(() => {
|
||||
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)
|
||||
const unitSuffixPre = (unit || '').trim().toLowerCase()
|
||||
let maxVal = Math.ceil(paddedMax)
|
||||
let minVal = Math.floor(paddedMin)
|
||||
if (unitSuffixPre === 'ms' || unitSuffixPre === 'latency') {
|
||||
minVal = 0
|
||||
if (paddedMax < 10) {
|
||||
maxVal = Math.ceil(paddedMax)
|
||||
} else if (paddedMax < 100) {
|
||||
maxVal = Math.ceil(paddedMax / 10) * 10
|
||||
} else if (paddedMax < 1000) {
|
||||
maxVal = Math.ceil(paddedMax / 50) * 50
|
||||
} else if (paddedMax < 10000) {
|
||||
maxVal = Math.ceil(paddedMax / 500) * 500
|
||||
} else {
|
||||
maxVal = Math.ceil(paddedMax / 1000) * 1000
|
||||
}
|
||||
}
|
||||
return {
|
||||
maxValue: maxVal,
|
||||
minValue: minVal,
|
||||
valueRange: maxVal - minVal || 1,
|
||||
}
|
||||
}, [allSeries, unit])
|
||||
|
||||
const yMin = padding.top + 3
|
||||
const yMax = padding.top + chartHeight - 3
|
||||
|
||||
const scaledPoints = useMemo(
|
||||
() =>
|
||||
data.map((d, i) => {
|
||||
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
|
||||
const y = Math.max(yMin, Math.min(yMax, rawY))
|
||||
return { x, y }
|
||||
}),
|
||||
[data, chartWidth, chartHeight, minValue, valueRange, yMin, yMax, padding.left, padding.top]
|
||||
)
|
||||
|
||||
const scaledSeries = useMemo(
|
||||
() =>
|
||||
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 }
|
||||
}),
|
||||
[
|
||||
allSeries,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
minValue,
|
||||
valueRange,
|
||||
yMin,
|
||||
yMax,
|
||||
padding.left,
|
||||
padding.top,
|
||||
]
|
||||
)
|
||||
|
||||
const getSeriesById = (id?: string | null) => scaledSeries.find((s) => s.id === id)
|
||||
const visibleSeries = useMemo(
|
||||
() => (activeSeriesId ? scaledSeries.filter((s) => s.id === activeSeriesId) : scaledSeries),
|
||||
[activeSeriesId, scaledSeries]
|
||||
)
|
||||
|
||||
const pathD = useMemo(() => {
|
||||
if (scaledPoints.length <= 1) return ''
|
||||
const p = scaledPoints
|
||||
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
|
||||
}, [scaledPoints, yMin, yMax])
|
||||
|
||||
const getCompactDateLabel = (timestamp?: string) => {
|
||||
if (!timestamp) return ''
|
||||
try {
|
||||
const f = formatDate(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) : ''
|
||||
|
||||
if (containerWidth === null) {
|
||||
return (
|
||||
<div
|
||||
@@ -119,109 +246,6 @@ export function LineChart({
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
const unitSuffixPre = (unit || '').trim().toLowerCase()
|
||||
let maxValue = Math.ceil(paddedMax)
|
||||
let minValue = Math.floor(paddedMin)
|
||||
if (unitSuffixPre === 'ms' || unitSuffixPre === 'latency') {
|
||||
minValue = 0
|
||||
if (paddedMax < 10) {
|
||||
maxValue = Math.ceil(paddedMax)
|
||||
} else if (paddedMax < 100) {
|
||||
maxValue = Math.ceil(paddedMax / 10) * 10
|
||||
} else if (paddedMax < 1000) {
|
||||
maxValue = Math.ceil(paddedMax / 50) * 50
|
||||
} else if (paddedMax < 10000) {
|
||||
maxValue = Math.ceil(paddedMax / 500) * 500
|
||||
} else {
|
||||
maxValue = Math.ceil(paddedMax / 1000) * 1000
|
||||
}
|
||||
}
|
||||
const valueRange = maxValue - minValue || 1
|
||||
|
||||
const yMin = padding.top + 3
|
||||
const yMax = padding.top + chartHeight - 3
|
||||
|
||||
const scaledPoints = data.map((d, i) => {
|
||||
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
|
||||
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
|
||||
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
|
||||
})()
|
||||
|
||||
const getCompactDateLabel = (timestamp?: string) => {
|
||||
if (!timestamp) return ''
|
||||
try {
|
||||
const f = formatDate(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}
|
||||
@@ -386,7 +410,7 @@ export function LineChart({
|
||||
)
|
||||
})()}
|
||||
|
||||
{orderedSeries.map((s, idx) => {
|
||||
{visibleSeries.map((s, idx) => {
|
||||
const isActive = activeSeriesId ? activeSeriesId === s.id : true
|
||||
const isHovered = hoverSeriesId ? hoverSeriesId === s.id : false
|
||||
const baseOpacity = isActive ? 1 : 0.12
|
||||
@@ -682,4 +706,8 @@ export function LineChart({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoized LineChart component to prevent re-renders when parent updates.
|
||||
*/
|
||||
export const LineChart = memo(LineChartComponent)
|
||||
export default LineChart
|
||||
|
||||
@@ -36,7 +36,7 @@ export function StatusBar({
|
||||
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' })}`
|
||||
: `${start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })} – ${end.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}`
|
||||
return {
|
||||
rangeLabel,
|
||||
successLabel: `${segment.successRate.toFixed(1)}%`,
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface WorkflowExecutionItem {
|
||||
}
|
||||
|
||||
export function WorkflowsList({
|
||||
executions,
|
||||
filteredExecutions,
|
||||
expandedWorkflowId,
|
||||
onToggleWorkflow,
|
||||
@@ -20,7 +19,6 @@ export function WorkflowsList({
|
||||
searchQuery,
|
||||
segmentDurationMs,
|
||||
}: {
|
||||
executions: WorkflowExecutionItem[]
|
||||
filteredExecutions: WorkflowExecutionItem[]
|
||||
expandedWorkflowId: string | null
|
||||
onToggleWorkflow: (workflowId: string) => void
|
||||
|
||||
@@ -2,24 +2,13 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
formatLatency,
|
||||
mapToExecutionLog,
|
||||
mapToExecutionLogAlt,
|
||||
} from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import {
|
||||
useExecutionsMetrics,
|
||||
useGlobalDashboardLogs,
|
||||
useWorkflowDashboardLogs,
|
||||
} from '@/hooks/queries/logs'
|
||||
import { formatLatency, parseDuration } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import { useFilterStore } from '@/stores/logs/filters/store'
|
||||
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { LineChart, WorkflowsList } from './components'
|
||||
|
||||
type TimeFilter = '30m' | '1h' | '6h' | '12h' | '24h' | '3d' | '7d' | '14d' | '30d'
|
||||
|
||||
interface WorkflowExecution {
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
@@ -39,18 +28,12 @@ interface WorkflowExecution {
|
||||
|
||||
const DEFAULT_SEGMENTS = 72
|
||||
const MIN_SEGMENT_PX = 10
|
||||
const MIN_SEGMENT_MS = 60000
|
||||
|
||||
/**
|
||||
* Predetermined heights for skeleton bars to avoid hydration mismatch.
|
||||
* Using static values instead of Math.random() ensures server/client consistency.
|
||||
*/
|
||||
const SKELETON_BAR_HEIGHTS = [
|
||||
45, 72, 38, 85, 52, 68, 30, 90, 55, 42, 78, 35, 88, 48, 65, 28, 82, 58, 40, 75, 32, 95, 50, 70,
|
||||
]
|
||||
|
||||
/**
|
||||
* Skeleton loader for a single graph card
|
||||
*/
|
||||
function GraphCardSkeleton({ title }: { title: string }) {
|
||||
return (
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
|
||||
@@ -62,7 +45,6 @@ function GraphCardSkeleton({ title }: { title: string }) {
|
||||
</div>
|
||||
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
|
||||
<div className='flex h-[166px] flex-col justify-end gap-[4px]'>
|
||||
{/* Skeleton bars simulating chart */}
|
||||
<div className='flex items-end gap-[2px]'>
|
||||
{SKELETON_BAR_HEIGHTS.map((height, i) => (
|
||||
<Skeleton
|
||||
@@ -80,24 +62,16 @@ function GraphCardSkeleton({ title }: { title: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton loader for a workflow row in the workflows list
|
||||
*/
|
||||
function WorkflowRowSkeleton() {
|
||||
return (
|
||||
<div className='flex h-[44px] items-center gap-[16px] px-[24px]'>
|
||||
{/* Workflow name with color */}
|
||||
<div className='flex w-[160px] flex-shrink-0 items-center gap-[8px] pr-[8px]'>
|
||||
<Skeleton className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]' />
|
||||
<Skeleton className='h-[16px] flex-1' />
|
||||
</div>
|
||||
|
||||
{/* Status bar - takes most of the space */}
|
||||
<div className='flex-1'>
|
||||
<Skeleton className='h-[24px] w-full rounded-[4px]' />
|
||||
</div>
|
||||
|
||||
{/* Success rate */}
|
||||
<div className='w-[100px] flex-shrink-0 pl-[16px]'>
|
||||
<Skeleton className='h-[16px] w-[50px]' />
|
||||
</div>
|
||||
@@ -105,13 +79,9 @@ function WorkflowRowSkeleton() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton loader for the workflows list table
|
||||
*/
|
||||
function WorkflowsListSkeleton({ rowCount = 5 }: { rowCount?: number }) {
|
||||
return (
|
||||
<div className='flex h-full flex-col overflow-hidden rounded-[6px] bg-[var(--surface-1)]'>
|
||||
{/* Table header */}
|
||||
<div className='flex-shrink-0 rounded-t-[6px] bg-[var(--surface-3)] px-[24px] py-[10px]'>
|
||||
<div className='flex items-center gap-[16px]'>
|
||||
<span className='w-[160px] flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
@@ -123,8 +93,6 @@ function WorkflowsListSkeleton({ rowCount = 5 }: { rowCount?: number }) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table body - scrollable */}
|
||||
<div className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden'>
|
||||
{Array.from({ length: rowCount }).map((_, i) => (
|
||||
<WorkflowRowSkeleton key={i} />
|
||||
@@ -134,13 +102,9 @@ function WorkflowsListSkeleton({ rowCount = 5 }: { rowCount?: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete skeleton loader for the entire dashboard
|
||||
*/
|
||||
function DashboardSkeleton() {
|
||||
return (
|
||||
<div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'>
|
||||
{/* Graphs Section */}
|
||||
<div className='mb-[16px] flex-shrink-0'>
|
||||
<div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'>
|
||||
<GraphCardSkeleton title='Runs' />
|
||||
@@ -148,8 +112,6 @@ function DashboardSkeleton() {
|
||||
<GraphCardSkeleton title='Latency' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflows Table - takes remaining space */}
|
||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
||||
<WorkflowsListSkeleton rowCount={14} />
|
||||
</div>
|
||||
@@ -158,225 +120,237 @@ function DashboardSkeleton() {
|
||||
}
|
||||
|
||||
interface DashboardProps {
|
||||
isLive?: boolean
|
||||
refreshTrigger?: number
|
||||
onCustomTimeRangeChange?: (isCustom: boolean) => void
|
||||
logs: WorkflowLog[]
|
||||
isLoading: boolean
|
||||
error?: Error | null
|
||||
}
|
||||
|
||||
export default function Dashboard({
|
||||
isLive = false,
|
||||
refreshTrigger = 0,
|
||||
onCustomTimeRangeChange,
|
||||
}: DashboardProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const getTimeFilterFromRange = (range: string): TimeFilter => {
|
||||
switch (range) {
|
||||
case 'Past 30 minutes':
|
||||
return '30m'
|
||||
case 'Past hour':
|
||||
return '1h'
|
||||
case 'Past 6 hours':
|
||||
return '6h'
|
||||
case 'Past 12 hours':
|
||||
return '12h'
|
||||
case 'Past 24 hours':
|
||||
return '24h'
|
||||
case 'Past 3 days':
|
||||
return '3d'
|
||||
case 'Past 7 days':
|
||||
return '7d'
|
||||
case 'Past 14 days':
|
||||
return '14d'
|
||||
case 'Past 30 days':
|
||||
return '30d'
|
||||
default:
|
||||
return '30d'
|
||||
}
|
||||
}
|
||||
|
||||
const [endTime, setEndTime] = useState<Date>(new Date())
|
||||
const [expandedWorkflowId, setExpandedWorkflowId] = useState<string | null>(null)
|
||||
export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
|
||||
const [segmentCount, setSegmentCount] = useState<number>(DEFAULT_SEGMENTS)
|
||||
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
|
||||
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
|
||||
const [segmentCount, setSegmentCount] = useState<number>(DEFAULT_SEGMENTS)
|
||||
const barsAreaRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const {
|
||||
workflowIds,
|
||||
folderIds,
|
||||
triggers,
|
||||
timeRange: sidebarTimeRange,
|
||||
level,
|
||||
searchQuery,
|
||||
} = useFilterStore()
|
||||
const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore()
|
||||
|
||||
const { workflows } = useWorkflowRegistry()
|
||||
const allWorkflows = useWorkflowRegistry((state) => state.workflows)
|
||||
|
||||
const timeFilter = getTimeFilterFromRange(sidebarTimeRange)
|
||||
const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null
|
||||
|
||||
const getStartTime = useCallback(() => {
|
||||
const start = new Date(endTime)
|
||||
const lastExecutionByWorkflow = useMemo(() => {
|
||||
const map = new Map<string, number>()
|
||||
for (const log of logs) {
|
||||
const wfId = log.workflowId
|
||||
if (!wfId) continue
|
||||
const ts = new Date(log.createdAt).getTime()
|
||||
const existing = map.get(wfId)
|
||||
if (!existing || ts > existing) {
|
||||
map.set(wfId, ts)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [logs])
|
||||
|
||||
switch (timeFilter) {
|
||||
case '30m':
|
||||
start.setMinutes(endTime.getMinutes() - 30)
|
||||
break
|
||||
case '1h':
|
||||
start.setHours(endTime.getHours() - 1)
|
||||
break
|
||||
case '6h':
|
||||
start.setHours(endTime.getHours() - 6)
|
||||
break
|
||||
case '12h':
|
||||
start.setHours(endTime.getHours() - 12)
|
||||
break
|
||||
case '24h':
|
||||
start.setHours(endTime.getHours() - 24)
|
||||
break
|
||||
case '3d':
|
||||
start.setDate(endTime.getDate() - 3)
|
||||
break
|
||||
case '7d':
|
||||
start.setDate(endTime.getDate() - 7)
|
||||
break
|
||||
case '14d':
|
||||
start.setDate(endTime.getDate() - 14)
|
||||
break
|
||||
case '30d':
|
||||
start.setDate(endTime.getDate() - 30)
|
||||
break
|
||||
default:
|
||||
start.setHours(endTime.getHours() - 24)
|
||||
const timeBounds = useMemo(() => {
|
||||
if (logs.length === 0) {
|
||||
const now = new Date()
|
||||
return { start: now, end: now }
|
||||
}
|
||||
|
||||
return start
|
||||
}, [endTime, timeFilter])
|
||||
let minTime = Number.POSITIVE_INFINITY
|
||||
let maxTime = Number.NEGATIVE_INFINITY
|
||||
|
||||
const metricsFilters = useMemo(
|
||||
() => ({
|
||||
workspaceId,
|
||||
segments: segmentCount || DEFAULT_SEGMENTS,
|
||||
startTime: getStartTime().toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
workflowIds: workflowIds.length > 0 ? workflowIds : undefined,
|
||||
folderIds: folderIds.length > 0 ? folderIds : undefined,
|
||||
triggers: triggers.length > 0 ? triggers : undefined,
|
||||
level: level !== 'all' ? level : undefined,
|
||||
}),
|
||||
[workspaceId, segmentCount, getStartTime, endTime, workflowIds, folderIds, triggers, level]
|
||||
)
|
||||
for (const log of logs) {
|
||||
const ts = new Date(log.createdAt).getTime()
|
||||
if (ts < minTime) minTime = ts
|
||||
if (ts > maxTime) maxTime = ts
|
||||
}
|
||||
|
||||
const logsFilters = useMemo(
|
||||
() => ({
|
||||
workspaceId,
|
||||
startDate: getStartTime().toISOString(),
|
||||
endDate: endTime.toISOString(),
|
||||
workflowIds: workflowIds.length > 0 ? workflowIds : undefined,
|
||||
folderIds: folderIds.length > 0 ? folderIds : undefined,
|
||||
triggers: triggers.length > 0 ? triggers : undefined,
|
||||
level: level !== 'all' ? level : undefined,
|
||||
searchQuery: searchQuery.trim() || undefined,
|
||||
limit: 50,
|
||||
}),
|
||||
[workspaceId, getStartTime, endTime, workflowIds, folderIds, triggers, level, searchQuery]
|
||||
)
|
||||
const end = new Date(Math.max(maxTime, Date.now()))
|
||||
const start = new Date(minTime)
|
||||
|
||||
const metricsQuery = useExecutionsMetrics(metricsFilters, {
|
||||
enabled: Boolean(workspaceId),
|
||||
})
|
||||
return { start, end }
|
||||
}, [logs])
|
||||
|
||||
const globalLogsQuery = useGlobalDashboardLogs(logsFilters, {
|
||||
enabled: Boolean(workspaceId),
|
||||
})
|
||||
const { executions, aggregateSegments, segmentMs } = useMemo(() => {
|
||||
const allWorkflowsList = Object.values(allWorkflows)
|
||||
|
||||
const workflowLogsQuery = useWorkflowDashboardLogs(expandedWorkflowId ?? undefined, logsFilters, {
|
||||
enabled: Boolean(workspaceId) && Boolean(expandedWorkflowId),
|
||||
})
|
||||
if (allWorkflowsList.length === 0) {
|
||||
return { executions: [], aggregateSegments: [], segmentMs: 0 }
|
||||
}
|
||||
|
||||
const executions = metricsQuery.data?.workflows ?? []
|
||||
const aggregateSegments = metricsQuery.data?.aggregateSegments ?? []
|
||||
const error = metricsQuery.error?.message ?? null
|
||||
const { start, end } =
|
||||
logs.length > 0
|
||||
? timeBounds
|
||||
: { start: new Date(Date.now() - 24 * 60 * 60 * 1000), end: new Date() }
|
||||
|
||||
/**
|
||||
* Loading state logic using TanStack Query best practices:
|
||||
* - isPending: true when there's no cached data (initial load only)
|
||||
* - isFetching: true when any fetch is in progress
|
||||
* - isPlaceholderData: true when showing stale data from keepPreviousData
|
||||
*
|
||||
* We only show skeleton on initial load (isPending + no data).
|
||||
* For subsequent fetches, keepPreviousData shows stale content while fetching.
|
||||
*/
|
||||
const showSkeleton = metricsQuery.isPending && !metricsQuery.data
|
||||
const totalMs = Math.max(1, end.getTime() - start.getTime())
|
||||
const calculatedSegmentMs = Math.max(
|
||||
MIN_SEGMENT_MS,
|
||||
Math.floor(totalMs / Math.max(1, segmentCount))
|
||||
)
|
||||
|
||||
// Check if any filters are actually applied
|
||||
const hasActiveFilters = useMemo(
|
||||
() =>
|
||||
level !== 'all' ||
|
||||
workflowIds.length > 0 ||
|
||||
folderIds.length > 0 ||
|
||||
triggers.length > 0 ||
|
||||
searchQuery.trim() !== '',
|
||||
[level, workflowIds, folderIds, triggers, searchQuery]
|
||||
)
|
||||
const logsByWorkflow = new Map<string, WorkflowLog[]>()
|
||||
for (const log of logs) {
|
||||
const wfId = log.workflowId
|
||||
if (!logsByWorkflow.has(wfId)) {
|
||||
logsByWorkflow.set(wfId, [])
|
||||
}
|
||||
logsByWorkflow.get(wfId)!.push(log)
|
||||
}
|
||||
|
||||
// Filter workflows based on search query and whether they have any executions matching the filters
|
||||
const filteredExecutions = useMemo(() => {
|
||||
let filtered = executions
|
||||
const workflowExecutions: WorkflowExecution[] = []
|
||||
|
||||
// Only filter out workflows with no executions if filters are active
|
||||
if (hasActiveFilters) {
|
||||
filtered = filtered.filter((workflow) => {
|
||||
const hasExecutions = workflow.segments.some((seg) => seg.hasExecutions === true)
|
||||
return hasExecutions
|
||||
for (const workflow of allWorkflowsList) {
|
||||
const workflowLogs = logsByWorkflow.get(workflow.id) || []
|
||||
|
||||
const segments: WorkflowExecution['segments'] = Array.from(
|
||||
{ length: segmentCount },
|
||||
(_, i) => ({
|
||||
timestamp: new Date(start.getTime() + i * calculatedSegmentMs).toISOString(),
|
||||
hasExecutions: false,
|
||||
totalExecutions: 0,
|
||||
successfulExecutions: 0,
|
||||
successRate: 100,
|
||||
avgDurationMs: 0,
|
||||
})
|
||||
)
|
||||
|
||||
const durations: number[][] = Array.from({ length: segmentCount }, () => [])
|
||||
|
||||
for (const log of workflowLogs) {
|
||||
const logTime = new Date(log.createdAt).getTime()
|
||||
const idx = Math.min(
|
||||
segmentCount - 1,
|
||||
Math.max(0, Math.floor((logTime - start.getTime()) / calculatedSegmentMs))
|
||||
)
|
||||
|
||||
segments[idx].totalExecutions += 1
|
||||
segments[idx].hasExecutions = true
|
||||
|
||||
if (log.level !== 'error') {
|
||||
segments[idx].successfulExecutions += 1
|
||||
}
|
||||
|
||||
const duration = parseDuration({ duration: log.duration ?? undefined })
|
||||
if (duration !== null && duration > 0) {
|
||||
durations[idx].push(duration)
|
||||
}
|
||||
}
|
||||
|
||||
let totalExecs = 0
|
||||
let totalSuccess = 0
|
||||
|
||||
for (let i = 0; i < segmentCount; i++) {
|
||||
const seg = segments[i]
|
||||
totalExecs += seg.totalExecutions
|
||||
totalSuccess += seg.successfulExecutions
|
||||
|
||||
if (seg.totalExecutions > 0) {
|
||||
seg.successRate = (seg.successfulExecutions / seg.totalExecutions) * 100
|
||||
}
|
||||
|
||||
if (durations[i].length > 0) {
|
||||
seg.avgDurationMs = Math.round(
|
||||
durations[i].reduce((sum, d) => sum + d, 0) / durations[i].length
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const overallSuccessRate = totalExecs > 0 ? (totalSuccess / totalExecs) * 100 : 100
|
||||
|
||||
workflowExecutions.push({
|
||||
workflowId: workflow.id,
|
||||
workflowName: workflow.name,
|
||||
segments,
|
||||
overallSuccessRate,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply search query filter
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim()
|
||||
filtered = filtered.filter((workflow) => workflow.workflowName.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
// Sort by creation date (newest first) to match sidebar ordering
|
||||
filtered = filtered.sort((a, b) => {
|
||||
const workflowA = workflows[a.workflowId]
|
||||
const workflowB = workflows[b.workflowId]
|
||||
if (!workflowA || !workflowB) return 0
|
||||
return workflowB.createdAt.getTime() - workflowA.createdAt.getTime()
|
||||
workflowExecutions.sort((a, b) => {
|
||||
const errA = a.overallSuccessRate < 100 ? 1 - a.overallSuccessRate / 100 : 0
|
||||
const errB = b.overallSuccessRate < 100 ? 1 - b.overallSuccessRate / 100 : 0
|
||||
if (errA !== errB) return errB - errA
|
||||
return a.workflowName.localeCompare(b.workflowName)
|
||||
})
|
||||
|
||||
return filtered
|
||||
}, [executions, searchQuery, hasActiveFilters, workflows])
|
||||
const aggSegments: {
|
||||
timestamp: string
|
||||
totalExecutions: number
|
||||
successfulExecutions: number
|
||||
avgDurationMs: number
|
||||
}[] = Array.from({ length: segmentCount }, (_, i) => ({
|
||||
timestamp: new Date(start.getTime() + i * calculatedSegmentMs).toISOString(),
|
||||
totalExecutions: 0,
|
||||
successfulExecutions: 0,
|
||||
avgDurationMs: 0,
|
||||
}))
|
||||
|
||||
const globalLogs = useMemo(() => {
|
||||
if (!globalLogsQuery.data?.pages) return []
|
||||
return globalLogsQuery.data.pages.flatMap((page) => page.logs).map(mapToExecutionLog)
|
||||
}, [globalLogsQuery.data?.pages])
|
||||
const weightedDurationSums: number[] = Array(segmentCount).fill(0)
|
||||
const executionCounts: number[] = Array(segmentCount).fill(0)
|
||||
|
||||
const workflowLogs = useMemo(() => {
|
||||
if (!workflowLogsQuery.data?.pages) return []
|
||||
return workflowLogsQuery.data.pages.flatMap((page) => page.logs).map(mapToExecutionLogAlt)
|
||||
}, [workflowLogsQuery.data?.pages])
|
||||
for (const wf of workflowExecutions) {
|
||||
wf.segments.forEach((s, i) => {
|
||||
aggSegments[i].totalExecutions += s.totalExecutions
|
||||
aggSegments[i].successfulExecutions += s.successfulExecutions
|
||||
|
||||
if (s.avgDurationMs && s.avgDurationMs > 0 && s.totalExecutions > 0) {
|
||||
weightedDurationSums[i] += s.avgDurationMs * s.totalExecutions
|
||||
executionCounts[i] += s.totalExecutions
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
aggSegments.forEach((seg, i) => {
|
||||
if (executionCounts[i] > 0) {
|
||||
seg.avgDurationMs = weightedDurationSums[i] / executionCounts[i]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
executions: workflowExecutions,
|
||||
aggregateSegments: aggSegments,
|
||||
segmentMs: calculatedSegmentMs,
|
||||
}
|
||||
}, [logs, timeBounds, segmentCount, allWorkflows])
|
||||
|
||||
const filteredExecutions = useMemo(() => {
|
||||
let filtered = executions
|
||||
|
||||
if (workflowIds.length > 0) {
|
||||
filtered = filtered.filter((wf) => workflowIds.includes(wf.workflowId))
|
||||
}
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim()
|
||||
filtered = filtered.filter((wf) => wf.workflowName.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
return filtered.slice().sort((a, b) => {
|
||||
const timeA = lastExecutionByWorkflow.get(a.workflowId) ?? 0
|
||||
const timeB = lastExecutionByWorkflow.get(b.workflowId) ?? 0
|
||||
|
||||
if (!timeA && !timeB) return a.workflowName.localeCompare(b.workflowName)
|
||||
if (!timeA) return 1
|
||||
if (!timeB) return -1
|
||||
|
||||
return timeB - timeA
|
||||
})
|
||||
}, [executions, lastExecutionByWorkflow, workflowIds, searchQuery])
|
||||
|
||||
const globalDetails = useMemo(() => {
|
||||
if (!aggregateSegments.length) return null
|
||||
|
||||
const hasSelection = Object.keys(selectedSegments).length > 0
|
||||
const hasWorkflowFilter = expandedWorkflowId && expandedWorkflowId !== '__multi__'
|
||||
const hasWorkflowFilter = expandedWorkflowId !== null
|
||||
|
||||
// Stack filters: workflow filter + segment selection
|
||||
const segmentsToUse = hasSelection
|
||||
? (() => {
|
||||
// Get all selected segment indices across all workflows
|
||||
const allSelectedIndices = new Set<number>()
|
||||
Object.values(selectedSegments).forEach((indices) => {
|
||||
indices.forEach((idx) => allSelectedIndices.add(idx))
|
||||
})
|
||||
|
||||
// For each selected index, aggregate data from workflows that have that segment selected
|
||||
// If a workflow filter is active, only include that workflow's data
|
||||
return Array.from(allSelectedIndices)
|
||||
.sort((a, b) => a - b)
|
||||
.map((idx) => {
|
||||
@@ -386,11 +360,8 @@ export default function Dashboard({
|
||||
let latencyCount = 0
|
||||
const timestamp = aggregateSegments[idx]?.timestamp || ''
|
||||
|
||||
// Sum up data from workflows that have this segment selected
|
||||
Object.entries(selectedSegments).forEach(([workflowId, indices]) => {
|
||||
if (!indices.includes(idx)) return
|
||||
|
||||
// If workflow filter is active, skip other workflows
|
||||
if (hasWorkflowFilter && workflowId !== expandedWorkflowId) return
|
||||
|
||||
const workflow = filteredExecutions.find((w) => w.workflowId === workflowId)
|
||||
@@ -416,7 +387,6 @@ export default function Dashboard({
|
||||
})()
|
||||
: hasWorkflowFilter
|
||||
? (() => {
|
||||
// Filter to show only the expanded workflow's data
|
||||
const workflow = filteredExecutions.find((w) => w.workflowId === expandedWorkflowId)
|
||||
if (!workflow) return aggregateSegments
|
||||
|
||||
@@ -427,42 +397,7 @@ export default function Dashboard({
|
||||
avgDurationMs: segment.avgDurationMs ?? 0,
|
||||
}))
|
||||
})()
|
||||
: hasActiveFilters
|
||||
? (() => {
|
||||
// Always recalculate aggregate segments based on filtered workflows when filters are active
|
||||
return aggregateSegments.map((aggSeg, idx) => {
|
||||
let totalExecutions = 0
|
||||
let successfulExecutions = 0
|
||||
let weightedLatencySum = 0
|
||||
let latencyCount = 0
|
||||
|
||||
filteredExecutions.forEach((workflow) => {
|
||||
const segment = workflow.segments[idx]
|
||||
if (!segment) return
|
||||
|
||||
totalExecutions += segment.totalExecutions || 0
|
||||
successfulExecutions += segment.successfulExecutions || 0
|
||||
|
||||
if (segment.avgDurationMs && segment.totalExecutions) {
|
||||
weightedLatencySum += segment.avgDurationMs * segment.totalExecutions
|
||||
latencyCount += segment.totalExecutions
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
timestamp: aggSeg.timestamp,
|
||||
totalExecutions,
|
||||
successfulExecutions,
|
||||
avgDurationMs: latencyCount > 0 ? weightedLatencySum / latencyCount : 0,
|
||||
}
|
||||
})
|
||||
})()
|
||||
: aggregateSegments
|
||||
|
||||
const errorRates = segmentsToUse.map((s) => ({
|
||||
timestamp: s.timestamp,
|
||||
value: s.totalExecutions > 0 ? (1 - s.successfulExecutions / s.totalExecutions) * 100 : 0,
|
||||
}))
|
||||
: aggregateSegments
|
||||
|
||||
const executionCounts = segmentsToUse.map((s) => ({
|
||||
timestamp: s.timestamp,
|
||||
@@ -479,128 +414,46 @@ export default function Dashboard({
|
||||
value: s.avgDurationMs ?? 0,
|
||||
}))
|
||||
|
||||
const totalRuns = segmentsToUse.reduce((sum, s) => sum + s.totalExecutions, 0)
|
||||
const totalErrors = segmentsToUse.reduce(
|
||||
(sum, s) => sum + (s.totalExecutions - s.successfulExecutions),
|
||||
0
|
||||
)
|
||||
|
||||
let weightedLatencySum = 0
|
||||
let latencyCount = 0
|
||||
for (const s of segmentsToUse) {
|
||||
if (s.avgDurationMs && s.totalExecutions > 0) {
|
||||
weightedLatencySum += s.avgDurationMs * s.totalExecutions
|
||||
latencyCount += s.totalExecutions
|
||||
}
|
||||
}
|
||||
const avgLatency = latencyCount > 0 ? weightedLatencySum / latencyCount : 0
|
||||
|
||||
return {
|
||||
errorRates,
|
||||
durations: [],
|
||||
executionCounts,
|
||||
failureCounts,
|
||||
latencies,
|
||||
logs: globalLogs,
|
||||
allLogs: globalLogs,
|
||||
}
|
||||
}, [
|
||||
aggregateSegments,
|
||||
globalLogs,
|
||||
selectedSegments,
|
||||
filteredExecutions,
|
||||
expandedWorkflowId,
|
||||
hasActiveFilters,
|
||||
])
|
||||
|
||||
const workflowDetails = useMemo(() => {
|
||||
if (!expandedWorkflowId || !workflowLogs.length) return {}
|
||||
|
||||
return {
|
||||
[expandedWorkflowId]: {
|
||||
errorRates: [],
|
||||
durations: [],
|
||||
executionCounts: [],
|
||||
logs: workflowLogs,
|
||||
allLogs: workflowLogs,
|
||||
},
|
||||
}
|
||||
}, [expandedWorkflowId, workflowLogs])
|
||||
|
||||
const aggregate = useMemo(() => {
|
||||
const hasSelection = Object.keys(selectedSegments).length > 0
|
||||
const hasWorkflowFilter = expandedWorkflowId && expandedWorkflowId !== '__multi__'
|
||||
let totalExecutions = 0
|
||||
let successfulExecutions = 0
|
||||
let activeWorkflows = 0
|
||||
let weightedLatencySum = 0
|
||||
let latencyExecutionCount = 0
|
||||
|
||||
// Apply workflow filter first if present, otherwise use filtered executions
|
||||
const workflowsToProcess = hasWorkflowFilter
|
||||
? filteredExecutions.filter((wf) => wf.workflowId === expandedWorkflowId)
|
||||
: filteredExecutions
|
||||
|
||||
for (const wf of workflowsToProcess) {
|
||||
const selectedIndices = hasSelection ? selectedSegments[wf.workflowId] : null
|
||||
let workflowHasExecutions = false
|
||||
|
||||
wf.segments.forEach((seg, idx) => {
|
||||
// If segment selection exists, only count selected segments
|
||||
// Otherwise, count all segments
|
||||
if (!selectedIndices || selectedIndices.includes(idx)) {
|
||||
const execCount = seg.totalExecutions || 0
|
||||
totalExecutions += execCount
|
||||
successfulExecutions += seg.successfulExecutions || 0
|
||||
|
||||
if (
|
||||
seg.avgDurationMs !== undefined &&
|
||||
seg.avgDurationMs !== null &&
|
||||
seg.avgDurationMs > 0 &&
|
||||
execCount > 0
|
||||
) {
|
||||
weightedLatencySum += seg.avgDurationMs * execCount
|
||||
latencyExecutionCount += execCount
|
||||
}
|
||||
if (seg.hasExecutions) workflowHasExecutions = true
|
||||
}
|
||||
})
|
||||
|
||||
if (workflowHasExecutions) activeWorkflows += 1
|
||||
}
|
||||
|
||||
const failedExecutions = Math.max(totalExecutions - successfulExecutions, 0)
|
||||
const successRate = totalExecutions > 0 ? (successfulExecutions / totalExecutions) * 100 : 100
|
||||
const avgLatency = latencyExecutionCount > 0 ? weightedLatencySum / latencyExecutionCount : 0
|
||||
|
||||
return {
|
||||
totalExecutions,
|
||||
successfulExecutions,
|
||||
failedExecutions,
|
||||
activeWorkflows,
|
||||
successRate,
|
||||
totalRuns,
|
||||
totalErrors,
|
||||
avgLatency,
|
||||
}
|
||||
}, [filteredExecutions, selectedSegments, expandedWorkflowId])
|
||||
}, [aggregateSegments, selectedSegments, filteredExecutions, expandedWorkflowId])
|
||||
|
||||
const loadMoreLogs = useCallback(
|
||||
const handleToggleWorkflow = useCallback(
|
||||
(workflowId: string) => {
|
||||
if (
|
||||
workflowId === expandedWorkflowId &&
|
||||
workflowLogsQuery.hasNextPage &&
|
||||
!workflowLogsQuery.isFetchingNextPage
|
||||
) {
|
||||
workflowLogsQuery.fetchNextPage()
|
||||
}
|
||||
toggleWorkflowId(workflowId)
|
||||
},
|
||||
[expandedWorkflowId, workflowLogsQuery]
|
||||
)
|
||||
|
||||
const loadMoreGlobalLogs = useCallback(() => {
|
||||
if (globalLogsQuery.hasNextPage && !globalLogsQuery.isFetchingNextPage) {
|
||||
globalLogsQuery.fetchNextPage()
|
||||
}
|
||||
}, [globalLogsQuery])
|
||||
|
||||
const toggleWorkflow = useCallback(
|
||||
(workflowId: string) => {
|
||||
if (expandedWorkflowId === workflowId) {
|
||||
setExpandedWorkflowId(null)
|
||||
setSelectedSegments({})
|
||||
setLastAnchorIndices({})
|
||||
} else {
|
||||
setExpandedWorkflowId(workflowId)
|
||||
setSelectedSegments({})
|
||||
setLastAnchorIndices({})
|
||||
}
|
||||
},
|
||||
[expandedWorkflowId]
|
||||
[toggleWorkflowId]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles segment click for selecting time segments.
|
||||
* @param workflowId - The workflow containing the segment
|
||||
* @param segmentIndex - Index of the clicked segment
|
||||
* @param _timestamp - Timestamp of the segment (unused)
|
||||
* @param mode - Selection mode: 'single', 'toggle' (cmd+click), or 'range' (shift+click)
|
||||
*/
|
||||
const handleSegmentClick = useCallback(
|
||||
(
|
||||
workflowId: string,
|
||||
@@ -618,22 +471,10 @@ export default function Dashboard({
|
||||
|
||||
if (nextSegments.length === 0) {
|
||||
const { [workflowId]: _, ...rest } = prev
|
||||
if (Object.keys(rest).length === 0) {
|
||||
setExpandedWorkflowId(null)
|
||||
}
|
||||
return rest
|
||||
}
|
||||
|
||||
const newState = { ...prev, [workflowId]: nextSegments }
|
||||
|
||||
const selectedWorkflowIds = Object.keys(newState)
|
||||
if (selectedWorkflowIds.length > 1) {
|
||||
setExpandedWorkflowId('__multi__')
|
||||
} else if (selectedWorkflowIds.length === 1) {
|
||||
setExpandedWorkflowId(selectedWorkflowIds[0])
|
||||
}
|
||||
|
||||
return newState
|
||||
return { ...prev, [workflowId]: nextSegments }
|
||||
})
|
||||
|
||||
setLastAnchorIndices((prev) => ({ ...prev, [workflowId]: segmentIndex }))
|
||||
@@ -645,85 +486,32 @@ export default function Dashboard({
|
||||
const isOnlyWorkflowSelected = Object.keys(prev).length === 1 && prev[workflowId]
|
||||
|
||||
if (isOnlySelectedSegment && isOnlyWorkflowSelected) {
|
||||
setExpandedWorkflowId(null)
|
||||
setLastAnchorIndices({})
|
||||
return {}
|
||||
}
|
||||
|
||||
setExpandedWorkflowId(workflowId)
|
||||
setLastAnchorIndices({ [workflowId]: segmentIndex })
|
||||
return { [workflowId]: [segmentIndex] }
|
||||
})
|
||||
} else if (mode === 'range') {
|
||||
if (expandedWorkflowId === workflowId) {
|
||||
setSelectedSegments((prev) => {
|
||||
const currentSegments = prev[workflowId] || []
|
||||
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
|
||||
const [start, end] =
|
||||
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
|
||||
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
const union = new Set([...currentSegments, ...range])
|
||||
return { ...prev, [workflowId]: Array.from(union).sort((a, b) => a - b) }
|
||||
})
|
||||
} else {
|
||||
setExpandedWorkflowId(workflowId)
|
||||
setSelectedSegments({ [workflowId]: [segmentIndex] })
|
||||
setLastAnchorIndices({ [workflowId]: segmentIndex })
|
||||
}
|
||||
setSelectedSegments((prev) => {
|
||||
const currentSegments = prev[workflowId] || []
|
||||
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
|
||||
const [start, end] =
|
||||
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
|
||||
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
const union = new Set([...currentSegments, ...range])
|
||||
return { ...prev, [workflowId]: Array.from(union).sort((a, b) => a - b) }
|
||||
})
|
||||
}
|
||||
},
|
||||
[expandedWorkflowId, lastAnchorIndices]
|
||||
[lastAnchorIndices]
|
||||
)
|
||||
|
||||
// Update endTime when filters change to ensure consistent time ranges with logs view
|
||||
useEffect(() => {
|
||||
setEndTime(new Date())
|
||||
setSelectedSegments({})
|
||||
setLastAnchorIndices({})
|
||||
}, [timeFilter, workflowIds, folderIds, triggers, level, searchQuery])
|
||||
|
||||
// Clear expanded workflow if it's no longer in filtered executions
|
||||
useEffect(() => {
|
||||
if (expandedWorkflowId && expandedWorkflowId !== '__multi__') {
|
||||
const isStillVisible = filteredExecutions.some((wf) => wf.workflowId === expandedWorkflowId)
|
||||
if (!isStillVisible) {
|
||||
setExpandedWorkflowId(null)
|
||||
setSelectedSegments({})
|
||||
setLastAnchorIndices({})
|
||||
}
|
||||
} else if (expandedWorkflowId === '__multi__') {
|
||||
// Check if any of the selected workflows are still visible
|
||||
const selectedWorkflowIds = Object.keys(selectedSegments)
|
||||
const stillVisibleIds = selectedWorkflowIds.filter((id) =>
|
||||
filteredExecutions.some((wf) => wf.workflowId === id)
|
||||
)
|
||||
|
||||
if (stillVisibleIds.length === 0) {
|
||||
setExpandedWorkflowId(null)
|
||||
setSelectedSegments({})
|
||||
setLastAnchorIndices({})
|
||||
} else if (stillVisibleIds.length !== selectedWorkflowIds.length) {
|
||||
// Remove segments for workflows that are no longer visible
|
||||
const updatedSegments: Record<string, number[]> = {}
|
||||
stillVisibleIds.forEach((id) => {
|
||||
if (selectedSegments[id]) {
|
||||
updatedSegments[id] = selectedSegments[id]
|
||||
}
|
||||
})
|
||||
setSelectedSegments(updatedSegments)
|
||||
|
||||
if (stillVisibleIds.length === 1) {
|
||||
setExpandedWorkflowId(stillVisibleIds[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [filteredExecutions, expandedWorkflowId, selectedSegments])
|
||||
|
||||
// Notify parent when custom time range is active
|
||||
useEffect(() => {
|
||||
const hasCustomRange = Object.keys(selectedSegments).length > 0
|
||||
onCustomTimeRangeChange?.(hasCustomRange)
|
||||
}, [selectedSegments, onCustomTimeRangeChange])
|
||||
}, [logs, timeRange, workflowIds, searchQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (!barsAreaRef.current) return
|
||||
@@ -749,43 +537,30 @@ export default function Dashboard({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Live mode: refresh endTime periodically
|
||||
useEffect(() => {
|
||||
if (!isLive) return
|
||||
const interval = setInterval(() => {
|
||||
setEndTime(new Date())
|
||||
}, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [isLive])
|
||||
|
||||
// Refresh when trigger changes
|
||||
useEffect(() => {
|
||||
if (refreshTrigger > 0) {
|
||||
setEndTime(new Date())
|
||||
}
|
||||
}, [refreshTrigger])
|
||||
|
||||
if (showSkeleton) {
|
||||
if (isLoading) {
|
||||
return <DashboardSkeleton />
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className='mt-[24px] flex flex-1 items-center justify-center'>
|
||||
<div className='text-[var(--text-error)]'>
|
||||
<p className='font-medium text-[13px]'>Error loading data</p>
|
||||
<p className='text-[12px]'>{error}</p>
|
||||
<p className='text-[12px]'>{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (executions.length === 0) {
|
||||
if (Object.keys(allWorkflows).length === 0) {
|
||||
return (
|
||||
<div className='mt-[24px] flex flex-1 items-center justify-center'>
|
||||
<div className='text-center text-[var(--text-secondary)]'>
|
||||
<p className='font-medium text-[13px]'>No execution history</p>
|
||||
<p className='mt-[4px] text-[12px]'>Execute some workflows to see their history here</p>
|
||||
<p className='font-medium text-[13px]'>No workflows</p>
|
||||
<p className='mt-[4px] text-[12px]'>
|
||||
Create a workflow to see its execution history here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -793,18 +568,16 @@ export default function Dashboard({
|
||||
|
||||
return (
|
||||
<div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'>
|
||||
{/* Graphs Section */}
|
||||
<div className='mb-[16px] flex-shrink-0'>
|
||||
<div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'>
|
||||
{/* Runs Graph */}
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
|
||||
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
|
||||
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
|
||||
Runs
|
||||
</span>
|
||||
{globalDetails && globalDetails.executionCounts.length > 0 && (
|
||||
{globalDetails && (
|
||||
<span className='flex-shrink-0 font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{aggregate.totalExecutions}
|
||||
{globalDetails.totalRuns}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -824,15 +597,14 @@ export default function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Errors Graph */}
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
|
||||
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
|
||||
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
|
||||
Errors
|
||||
</span>
|
||||
{globalDetails && globalDetails.failureCounts.length > 0 && (
|
||||
{globalDetails && (
|
||||
<span className='flex-shrink-0 font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{aggregate.failedExecutions}
|
||||
{globalDetails.totalErrors}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -852,15 +624,14 @@ export default function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Latency Graph */}
|
||||
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
|
||||
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
|
||||
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
|
||||
Latency
|
||||
</span>
|
||||
{globalDetails && globalDetails.latencies.length > 0 && (
|
||||
{globalDetails && (
|
||||
<span className='flex-shrink-0 font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{formatLatency(aggregate.avgLatency)}
|
||||
{formatLatency(globalDetails.avgLatency)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -882,19 +653,15 @@ export default function Dashboard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflows Table - takes remaining space */}
|
||||
<div className='min-h-0 flex-1 overflow-hidden' ref={barsAreaRef}>
|
||||
<WorkflowsList
|
||||
executions={executions as WorkflowExecution[]}
|
||||
filteredExecutions={filteredExecutions as WorkflowExecution[]}
|
||||
expandedWorkflowId={expandedWorkflowId}
|
||||
onToggleWorkflow={toggleWorkflow}
|
||||
onToggleWorkflow={handleToggleWorkflow}
|
||||
selectedSegments={selectedSegments}
|
||||
onSegmentClick={handleSegmentClick}
|
||||
searchQuery={searchQuery}
|
||||
segmentDurationMs={
|
||||
(endTime.getTime() - getStartTime().getTime()) / Math.max(1, segmentCount)
|
||||
}
|
||||
segmentDurationMs={segmentMs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ export { LogDetails } from './log-details'
|
||||
export { FileCards } from './log-details/components/file-download'
|
||||
export { FrozenCanvas } from './log-details/components/frozen-canvas'
|
||||
export { TraceSpans } from './log-details/components/trace-spans'
|
||||
export { LogsList } from './logs-list'
|
||||
export {
|
||||
AutocompleteSearch,
|
||||
Controls,
|
||||
|
||||
@@ -34,9 +34,6 @@ interface FileCardProps {
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats file size to human readable format
|
||||
*/
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
@@ -45,9 +42,6 @@ function formatFileSize(bytes: number): string {
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual file card component
|
||||
*/
|
||||
function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) {
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const router = useRouter()
|
||||
@@ -142,10 +136,6 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Container component for displaying workflow execution files.
|
||||
* Each file is displayed as a separate card with consistent styling.
|
||||
*/
|
||||
export function FileCards({ files, isExecutionFile = false, workspaceId }: FileCardsProps) {
|
||||
if (!files || files.length === 0) {
|
||||
return null
|
||||
@@ -170,9 +160,6 @@ export function FileCards({ files, isExecutionFile = false, workspaceId }: FileC
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Single file download button (legacy export for backwards compatibility)
|
||||
*/
|
||||
export function FileDownload({
|
||||
file,
|
||||
isExecutionFile = false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronDown, Code } from '@/components/emcn'
|
||||
import { WorkflowIcon } from '@/components/icons'
|
||||
@@ -531,9 +531,10 @@ interface TraceSpanItemProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual trace span card component
|
||||
* Individual trace span card component.
|
||||
* Memoized to prevent re-renders when sibling spans change.
|
||||
*/
|
||||
function TraceSpanItem({
|
||||
const TraceSpanItem = memo(function TraceSpanItem({
|
||||
span,
|
||||
totalDuration,
|
||||
workflowStartTime,
|
||||
@@ -779,12 +780,16 @@ function TraceSpanItem({
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Displays workflow execution trace spans with nested structure
|
||||
* Displays workflow execution trace spans with nested structure.
|
||||
* Memoized to prevent re-renders when parent LogDetails updates.
|
||||
*/
|
||||
export function TraceSpans({ traceSpans, totalDuration = 0 }: TraceSpansProps) {
|
||||
export const TraceSpans = memo(function TraceSpans({
|
||||
traceSpans,
|
||||
totalDuration = 0,
|
||||
}: TraceSpansProps) {
|
||||
const { workflowStartTime, actualTotalDuration, normalizedSpans } = useMemo(() => {
|
||||
if (!traceSpans || traceSpans.length === 0) {
|
||||
return { workflowStartTime: 0, actualTotalDuration: totalDuration, normalizedSpans: [] }
|
||||
@@ -827,4 +832,4 @@ export function TraceSpans({ traceSpans, totalDuration = 0 }: TraceSpansProps) {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronUp, X } from 'lucide-react'
|
||||
import { Button, Eye } from '@/components/emcn'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
@@ -36,7 +36,7 @@ interface LogDetailsProps {
|
||||
* @param props - Component props
|
||||
* @returns Log details sidebar component
|
||||
*/
|
||||
export function LogDetails({
|
||||
export const LogDetails = memo(function LogDetails({
|
||||
log,
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -95,7 +95,10 @@ export function LogDetails({
|
||||
navigateFunction()
|
||||
}
|
||||
|
||||
const formattedTimestamp = log ? formatDate(log.createdAt) : null
|
||||
const formattedTimestamp = useMemo(
|
||||
() => (log ? formatDate(log.createdAt) : null),
|
||||
[log?.createdAt]
|
||||
)
|
||||
|
||||
const logStatus: LogStatus = useMemo(() => {
|
||||
if (!log) return 'info'
|
||||
@@ -140,7 +143,7 @@ export function LogDetails({
|
||||
disabled={!hasPrev}
|
||||
aria-label='Previous log'
|
||||
>
|
||||
<ChevronUp className='h-[14px] w-[14px] rotate-180' />
|
||||
<ChevronUp className='h-[14px] w-[14px]' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -149,7 +152,7 @@ export function LogDetails({
|
||||
disabled={!hasNext}
|
||||
aria-label='Next log'
|
||||
>
|
||||
<ChevronUp className='h-[14px] w-[14px]' />
|
||||
<ChevronUp className='h-[14px] w-[14px] rotate-180' />
|
||||
</Button>
|
||||
<Button variant='ghost' className='!p-[4px]' onClick={onClose} aria-label='Close'>
|
||||
<X className='h-[14px] w-[14px]' />
|
||||
@@ -374,4 +377,4 @@ export function LogDetails({
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { LogsList, type LogsListProps } from './logs-list'
|
||||
@@ -0,0 +1,273 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowUpRight, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { List, type RowComponentProps, useListRef } from 'react-window'
|
||||
import { Badge, buttonVariants } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||
import { formatDate, formatDuration, StatusBadge, TriggerBadge } from '../../utils'
|
||||
|
||||
const LOG_ROW_HEIGHT = 44 as const
|
||||
|
||||
interface LogRowProps {
|
||||
log: WorkflowLog
|
||||
isSelected: boolean
|
||||
onClick: (log: WorkflowLog) => void
|
||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoized log row component to prevent unnecessary re-renders.
|
||||
* Uses shallow comparison for the log object.
|
||||
*/
|
||||
const LogRow = memo(
|
||||
function LogRow({ log, isSelected, onClick, selectedRowRef }: LogRowProps) {
|
||||
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
||||
const baseLevel = (log.level || 'info').toLowerCase()
|
||||
const isError = baseLevel === 'error'
|
||||
const isPending = !isError && log.hasPendingPause === true
|
||||
const isRunning = !isError && !isPending && log.duration === null
|
||||
|
||||
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isSelected ? selectedRowRef : null}
|
||||
className={cn(
|
||||
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--c-2A2A2A)]',
|
||||
isSelected && 'bg-[var(--c-2A2A2A)]'
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className='flex flex-1 items-center'>
|
||||
{/* Date */}
|
||||
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{formattedDate.compactDate}
|
||||
</span>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{formattedDate.compactTime}
|
||||
</span>
|
||||
|
||||
{/* Status */}
|
||||
<div className='w-[12%] min-w-[100px]'>
|
||||
<StatusBadge
|
||||
status={isError ? 'error' : isPending ? 'pending' : isRunning ? 'running' : 'info'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Workflow */}
|
||||
<div className='flex w-[22%] min-w-[140px] items-center gap-[8px] pr-[8px]'>
|
||||
<div
|
||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: log.workflow?.color }}
|
||||
/>
|
||||
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{log.workflow?.name || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cost */}
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{typeof log.cost?.total === 'number' ? `$${log.cost.total.toFixed(4)}` : '—'}
|
||||
</span>
|
||||
|
||||
{/* Trigger */}
|
||||
<div className='w-[14%] min-w-[110px]'>
|
||||
{log.trigger ? (
|
||||
<TriggerBadge trigger={log.trigger} />
|
||||
) : (
|
||||
<span className='font-medium text-[12px] text-[var(--text-primary)]'>—</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className='w-[20%] min-w-[100px]'>
|
||||
<Badge variant='default' className='rounded-[6px] px-[9px] py-[2px] text-[12px]'>
|
||||
{formatDuration(log.duration) || '—'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume Link */}
|
||||
{isPending && log.executionId && (log.workflow?.id || log.workflowId) && (
|
||||
<Link
|
||||
href={`/resume/${log.workflow?.id || log.workflowId}/${log.executionId}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'active' }),
|
||||
'absolute right-[24px] h-[26px] w-[26px] rounded-[6px] p-0'
|
||||
)}
|
||||
aria-label='Open resume console'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ArrowUpRight className='h-[14px] w-[14px]' />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.log.id === nextProps.log.id &&
|
||||
prevProps.log.duration === nextProps.log.duration &&
|
||||
prevProps.log.level === nextProps.log.level &&
|
||||
prevProps.log.hasPendingPause === nextProps.log.hasPendingPause &&
|
||||
prevProps.isSelected === nextProps.isSelected
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
interface RowProps {
|
||||
logs: WorkflowLog[]
|
||||
selectedLogId: string | null
|
||||
onLogClick: (log: WorkflowLog) => void
|
||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||
isFetchingNextPage: boolean
|
||||
loaderRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* Row component for the virtualized list.
|
||||
* Receives row-specific props via rowProps.
|
||||
*/
|
||||
function Row({
|
||||
index,
|
||||
style,
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
selectedRowRef,
|
||||
isFetchingNextPage,
|
||||
loaderRef,
|
||||
}: RowComponentProps<RowProps>) {
|
||||
// Show loader for the last item if loading more
|
||||
if (index >= logs.length) {
|
||||
return (
|
||||
<div style={style} className='flex items-center justify-center'>
|
||||
<div ref={loaderRef} className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
|
||||
{isFetchingNextPage ? (
|
||||
<>
|
||||
<Loader2 className='h-[16px] w-[16px] animate-spin' />
|
||||
<span className='text-[13px]'>Loading more...</span>
|
||||
</>
|
||||
) : (
|
||||
<span className='text-[13px]'>Scroll to load more</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const log = logs[index]
|
||||
const isSelected = selectedLogId === log.id
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<LogRow
|
||||
log={log}
|
||||
isSelected={isSelected}
|
||||
onClick={onLogClick}
|
||||
selectedRowRef={isSelected ? selectedRowRef : null}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface LogsListProps {
|
||||
logs: WorkflowLog[]
|
||||
selectedLogId: string | null
|
||||
onLogClick: (log: WorkflowLog) => void
|
||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||
hasNextPage: boolean
|
||||
isFetchingNextPage: boolean
|
||||
onLoadMore: () => void
|
||||
loaderRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtualized logs list using react-window for optimal performance.
|
||||
* Renders only visible rows, enabling smooth scrolling with large datasets.
|
||||
* @param props - Component props
|
||||
* @returns The virtualized logs list
|
||||
*/
|
||||
export function LogsList({
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
selectedRowRef,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
onLoadMore,
|
||||
loaderRef,
|
||||
}: LogsListProps) {
|
||||
const listRef = useListRef(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [listHeight, setListHeight] = useState(400)
|
||||
|
||||
// Measure container height for virtualization
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const updateHeight = () => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
if (rect.height > 0) {
|
||||
setListHeight(rect.height)
|
||||
}
|
||||
}
|
||||
|
||||
updateHeight()
|
||||
const ro = new ResizeObserver(updateHeight)
|
||||
ro.observe(container)
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
|
||||
// Handle infinite scroll when nearing the end of the list
|
||||
const handleRowsRendered = useCallback(
|
||||
({ stopIndex }: { startIndex: number; stopIndex: number }) => {
|
||||
const threshold = logs.length - 10
|
||||
if (stopIndex >= threshold && hasNextPage && !isFetchingNextPage) {
|
||||
onLoadMore()
|
||||
}
|
||||
},
|
||||
[logs.length, hasNextPage, isFetchingNextPage, onLoadMore]
|
||||
)
|
||||
|
||||
// Calculate total item count including loader row
|
||||
const itemCount = hasNextPage ? logs.length + 1 : logs.length
|
||||
|
||||
// Row props passed to each row component
|
||||
const rowProps = useMemo<RowProps>(
|
||||
() => ({
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
selectedRowRef,
|
||||
isFetchingNextPage,
|
||||
loaderRef,
|
||||
}),
|
||||
[logs, selectedLogId, onLogClick, selectedRowRef, isFetchingNextPage, loaderRef]
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='h-full w-full'>
|
||||
<List
|
||||
listRef={listRef}
|
||||
defaultHeight={listHeight}
|
||||
rowCount={itemCount}
|
||||
rowHeight={LOG_ROW_HEIGHT}
|
||||
rowComponent={Row}
|
||||
rowProps={rowProps}
|
||||
overscanCount={5}
|
||||
onRowsRendered={handleRowsRendered}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogsList
|
||||
@@ -87,6 +87,7 @@ export function AutocompleteSearch({
|
||||
suggestions,
|
||||
sections,
|
||||
highlightedIndex,
|
||||
highlightedBadgeIndex,
|
||||
inputRef,
|
||||
dropdownRef,
|
||||
handleInputChange,
|
||||
@@ -162,7 +163,7 @@ export function AutocompleteSearch({
|
||||
}}
|
||||
>
|
||||
<PopoverAnchor asChild>
|
||||
<div className='relative flex h-[32px] w-[400px] items-center rounded-[8px] bg-[var(--surface-5)]'>
|
||||
<div className='relative flex h-[32px] w-full items-center rounded-[8px] bg-[var(--surface-5)]'>
|
||||
{/* Search Icon */}
|
||||
<Search className='mr-[6px] ml-[8px] h-[14px] w-[14px] flex-shrink-0 text-[var(--text-subtle)]' />
|
||||
|
||||
@@ -175,7 +176,11 @@ export function AutocompleteSearch({
|
||||
variant='outline'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='h-6 shrink-0 cursor-pointer whitespace-nowrap rounded-md px-2 text-[11px]'
|
||||
className={cn(
|
||||
'h-6 shrink-0 cursor-pointer whitespace-nowrap rounded-md px-2 text-[11px]',
|
||||
highlightedBadgeIndex === index &&
|
||||
'ring-1 ring-[var(--border-focus)] ring-offset-1 ring-offset-[var(--surface-5)]'
|
||||
)}
|
||||
onClick={() => removeBadge(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowUp, Bell, Library, Loader2, MoreHorizontal, RefreshCw } from 'lucide-react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { ArrowUp, Bell, Library, MoreHorizontal, RefreshCw } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
Loader,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverItem,
|
||||
PopoverScrollArea,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useFilterStore } from '@/stores/logs/filters/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { AutocompleteSearch } from './components/search'
|
||||
|
||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
|
||||
@@ -155,22 +156,15 @@ export function LogsToolbar({
|
||||
} = useFilterStore()
|
||||
const folders = useFolderStore((state) => state.folders)
|
||||
|
||||
const [workflows, setWorkflows] = useState<Array<{ id: string; name: string; color: string }>>([])
|
||||
const allWorkflows = useWorkflowRegistry((state) => state.workflows)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/workflows?workspaceId=${encodeURIComponent(workspaceId)}`)
|
||||
if (res.ok) {
|
||||
const body = await res.json()
|
||||
setWorkflows(Array.isArray(body?.data) ? body.data : [])
|
||||
}
|
||||
} catch {
|
||||
setWorkflows([])
|
||||
}
|
||||
}
|
||||
if (workspaceId) fetchWorkflows()
|
||||
}, [workspaceId])
|
||||
const workflows = useMemo(() => {
|
||||
return Object.values(allWorkflows).map((w) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
color: w.color,
|
||||
}))
|
||||
}, [allWorkflows])
|
||||
|
||||
const folderList = useMemo(() => {
|
||||
return Object.values(folders).filter((f) => f.workspaceId === workspaceId)
|
||||
@@ -178,7 +172,6 @@ export function LogsToolbar({
|
||||
|
||||
const isDashboardView = viewMode === 'dashboard'
|
||||
|
||||
// Status filter
|
||||
const selectedStatuses = useMemo((): string[] => {
|
||||
if (level === 'all' || !level) return []
|
||||
return level.split(',').filter(Boolean)
|
||||
@@ -199,7 +192,7 @@ export function LogsToolbar({
|
||||
if (values.length === 0) {
|
||||
setLevel('all')
|
||||
} else {
|
||||
setLevel(values.join(',') as any)
|
||||
setLevel(values.join(','))
|
||||
}
|
||||
},
|
||||
[setLevel]
|
||||
@@ -224,7 +217,6 @@ export function LogsToolbar({
|
||||
return null
|
||||
}, [selectedStatuses])
|
||||
|
||||
// Workflow filter
|
||||
const workflowOptions: ComboboxOption[] = useMemo(
|
||||
() => workflows.map((w) => ({ value: w.id, label: w.name, icon: getColorIcon(w.color) })),
|
||||
[workflows]
|
||||
@@ -242,7 +234,6 @@ export function LogsToolbar({
|
||||
const selectedWorkflow =
|
||||
workflowIds.length === 1 ? workflows.find((w) => w.id === workflowIds[0]) : null
|
||||
|
||||
// Folder filter
|
||||
const folderOptions: ComboboxOption[] = useMemo(
|
||||
() => folderList.map((f) => ({ value: f.id, label: f.name })),
|
||||
[folderList]
|
||||
@@ -257,7 +248,6 @@ export function LogsToolbar({
|
||||
return `${folderIds.length} folders`
|
||||
}, [folderIds, folderList])
|
||||
|
||||
// Trigger filter
|
||||
const triggerOptions: ComboboxOption[] = useMemo(
|
||||
() =>
|
||||
getTriggerOptions().map((t) => ({
|
||||
@@ -282,6 +272,24 @@ export function LogsToolbar({
|
||||
return timeRange
|
||||
}, [timeRange])
|
||||
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return (
|
||||
level !== 'all' ||
|
||||
workflowIds.length > 0 ||
|
||||
folderIds.length > 0 ||
|
||||
triggers.length > 0 ||
|
||||
timeRange !== 'All time'
|
||||
)
|
||||
}, [level, workflowIds, folderIds, triggers, timeRange])
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setLevel('all')
|
||||
setWorkflowIds([])
|
||||
setFolderIds([])
|
||||
setTriggers([])
|
||||
setTimeRange('All time')
|
||||
}, [setLevel, setWorkflowIds, setFolderIds, setTriggers, setTimeRange])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-[19px]'>
|
||||
{/* Header Section */}
|
||||
@@ -316,22 +324,18 @@ export function LogsToolbar({
|
||||
</Popover>
|
||||
|
||||
{/* Refresh button */}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='default'
|
||||
className={cn('h-[32px] w-[32px] rounded-[6px] p-0', isRefreshing && 'opacity-50')}
|
||||
onClick={isRefreshing ? undefined : onRefresh}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<Loader2 className='h-[14px] w-[14px] animate-spin' />
|
||||
) : (
|
||||
<RefreshCw className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{isRefreshing ? 'Refreshing...' : 'Refresh'}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Button
|
||||
variant='default'
|
||||
className='h-[32px] rounded-[6px] px-[10px]'
|
||||
onClick={isRefreshing ? undefined : onRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<Loader className='h-[14px] w-[14px]' animate />
|
||||
) : (
|
||||
<RefreshCw className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Live button */}
|
||||
<Button
|
||||
@@ -365,7 +369,7 @@ export function LogsToolbar({
|
||||
|
||||
{/* Filter Bar Section */}
|
||||
<div className='flex w-full items-center gap-[12px]'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='min-w-[200px] max-w-[400px] flex-1'>
|
||||
<AutocompleteSearch
|
||||
value={searchQuery}
|
||||
onChange={onSearchQueryChange}
|
||||
@@ -373,110 +377,269 @@ export function LogsToolbar({
|
||||
onOpenChange={onSearchOpenChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
{/* Status Filter */}
|
||||
<Combobox
|
||||
options={statusOptions}
|
||||
multiSelect
|
||||
multiSelectValues={selectedStatuses}
|
||||
onMultiSelectChange={handleStatusChange}
|
||||
placeholder='Status'
|
||||
overlayContent={
|
||||
<span className='flex items-center gap-[6px] truncate text-[var(--text-primary)]'>
|
||||
{selectedStatusColor && (
|
||||
<div
|
||||
className='flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: selectedStatusColor, width: 8, height: 8 }}
|
||||
<div className='ml-auto flex items-center gap-[8px]'>
|
||||
{/* Clear Filters Button */}
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant='active'
|
||||
onClick={handleClearFilters}
|
||||
className='h-[32px] rounded-[6px] px-[10px]'
|
||||
>
|
||||
<span>Clear</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Filters Popover - Small screens only */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='active'
|
||||
className='h-[32px] gap-[6px] rounded-[6px] px-[10px] xl:hidden'
|
||||
>
|
||||
<span>Filters</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end' sideOffset={4} className='w-[280px] p-[12px]'>
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{/* Status Filter */}
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
|
||||
Status
|
||||
</span>
|
||||
<Combobox
|
||||
options={statusOptions}
|
||||
multiSelect
|
||||
multiSelectValues={selectedStatuses}
|
||||
onMultiSelectChange={handleStatusChange}
|
||||
placeholder='All statuses'
|
||||
overlayContent={
|
||||
<span className='flex items-center gap-[6px] truncate text-[var(--text-primary)]'>
|
||||
{selectedStatusColor && (
|
||||
<div
|
||||
className='flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: selectedStatusColor, width: 8, height: 8 }}
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{statusDisplayLabel}</span>
|
||||
</span>
|
||||
}
|
||||
showAllOption
|
||||
allOptionLabel='All statuses'
|
||||
size='sm'
|
||||
className='h-[32px] w-full rounded-[6px]'
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{statusDisplayLabel}</span>
|
||||
</span>
|
||||
}
|
||||
showAllOption
|
||||
allOptionLabel='All statuses'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[100px] rounded-[6px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Workflow Filter */}
|
||||
<Combobox
|
||||
options={workflowOptions}
|
||||
multiSelect
|
||||
multiSelectValues={workflowIds}
|
||||
onMultiSelectChange={setWorkflowIds}
|
||||
placeholder='Workflow'
|
||||
overlayContent={
|
||||
<span className='flex items-center gap-[6px] truncate text-[var(--text-primary)]'>
|
||||
{selectedWorkflow && (
|
||||
<div
|
||||
className='h-[8px] w-[8px] flex-shrink-0 rounded-[2px]'
|
||||
style={{ backgroundColor: selectedWorkflow.color }}
|
||||
{/* Workflow Filter */}
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
|
||||
Workflow
|
||||
</span>
|
||||
<Combobox
|
||||
options={workflowOptions}
|
||||
multiSelect
|
||||
multiSelectValues={workflowIds}
|
||||
onMultiSelectChange={setWorkflowIds}
|
||||
placeholder='All workflows'
|
||||
overlayContent={
|
||||
<span className='flex items-center gap-[6px] truncate text-[var(--text-primary)]'>
|
||||
{selectedWorkflow && (
|
||||
<div
|
||||
className='h-[8px] w-[8px] flex-shrink-0 rounded-[2px]'
|
||||
style={{ backgroundColor: selectedWorkflow.color }}
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{workflowDisplayLabel}</span>
|
||||
</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
showAllOption
|
||||
allOptionLabel='All workflows'
|
||||
size='sm'
|
||||
className='h-[32px] w-full rounded-[6px]'
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{workflowDisplayLabel}</span>
|
||||
</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
showAllOption
|
||||
allOptionLabel='All workflows'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-[6px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Folder Filter */}
|
||||
<Combobox
|
||||
options={folderOptions}
|
||||
multiSelect
|
||||
multiSelectValues={folderIds}
|
||||
onMultiSelectChange={setFolderIds}
|
||||
placeholder='Folder'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>{folderDisplayLabel}</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search folders...'
|
||||
showAllOption
|
||||
allOptionLabel='All folders'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[100px] rounded-[6px]'
|
||||
/>
|
||||
{/* Folder Filter */}
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
|
||||
Folder
|
||||
</span>
|
||||
<Combobox
|
||||
options={folderOptions}
|
||||
multiSelect
|
||||
multiSelectValues={folderIds}
|
||||
onMultiSelectChange={setFolderIds}
|
||||
placeholder='All folders'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>
|
||||
{folderDisplayLabel}
|
||||
</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search folders...'
|
||||
showAllOption
|
||||
allOptionLabel='All folders'
|
||||
size='sm'
|
||||
className='h-[32px] w-full rounded-[6px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Trigger Filter */}
|
||||
<Combobox
|
||||
options={triggerOptions}
|
||||
multiSelect
|
||||
multiSelectValues={triggers}
|
||||
onMultiSelectChange={setTriggers}
|
||||
placeholder='Trigger'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>{triggerDisplayLabel}</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search triggers...'
|
||||
showAllOption
|
||||
allOptionLabel='All triggers'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[100px] rounded-[6px]'
|
||||
/>
|
||||
{/* Trigger Filter */}
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
|
||||
Trigger
|
||||
</span>
|
||||
<Combobox
|
||||
options={triggerOptions}
|
||||
multiSelect
|
||||
multiSelectValues={triggers}
|
||||
onMultiSelectChange={setTriggers}
|
||||
placeholder='All triggers'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>
|
||||
{triggerDisplayLabel}
|
||||
</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search triggers...'
|
||||
showAllOption
|
||||
allOptionLabel='All triggers'
|
||||
size='sm'
|
||||
className='h-[32px] w-full rounded-[6px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Timeline Filter */}
|
||||
<Combobox
|
||||
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
|
||||
value={timeRange}
|
||||
onChange={(val) => setTimeRange(val as typeof timeRange)}
|
||||
placeholder='Time'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>{timeDisplayLabel}</span>
|
||||
}
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[140px] rounded-[6px]'
|
||||
/>
|
||||
{/* Time Filter */}
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
|
||||
Time Range
|
||||
</span>
|
||||
<Combobox
|
||||
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
|
||||
value={timeRange}
|
||||
onChange={(val) => setTimeRange(val as typeof timeRange)}
|
||||
placeholder='All time'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>
|
||||
{timeDisplayLabel}
|
||||
</span>
|
||||
}
|
||||
size='sm'
|
||||
className='h-[32px] w-full rounded-[6px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Inline Filters - Large screens only */}
|
||||
<div className='hidden items-center gap-[8px] xl:flex'>
|
||||
{/* Status Filter */}
|
||||
<Combobox
|
||||
options={statusOptions}
|
||||
multiSelect
|
||||
multiSelectValues={selectedStatuses}
|
||||
onMultiSelectChange={handleStatusChange}
|
||||
placeholder='Status'
|
||||
overlayContent={
|
||||
<span className='flex items-center gap-[6px] truncate text-[var(--text-primary)]'>
|
||||
{selectedStatusColor && (
|
||||
<div
|
||||
className='flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: selectedStatusColor, width: 8, height: 8 }}
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{statusDisplayLabel}</span>
|
||||
</span>
|
||||
}
|
||||
showAllOption
|
||||
allOptionLabel='All statuses'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-[6px]'
|
||||
/>
|
||||
|
||||
{/* Workflow Filter */}
|
||||
<Combobox
|
||||
options={workflowOptions}
|
||||
multiSelect
|
||||
multiSelectValues={workflowIds}
|
||||
onMultiSelectChange={setWorkflowIds}
|
||||
placeholder='Workflow'
|
||||
overlayContent={
|
||||
<span className='flex items-center gap-[6px] truncate text-[var(--text-primary)]'>
|
||||
{selectedWorkflow && (
|
||||
<div
|
||||
className='h-[8px] w-[8px] flex-shrink-0 rounded-[2px]'
|
||||
style={{ backgroundColor: selectedWorkflow.color }}
|
||||
/>
|
||||
)}
|
||||
<span className='truncate'>{workflowDisplayLabel}</span>
|
||||
</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
showAllOption
|
||||
allOptionLabel='All workflows'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-[6px]'
|
||||
/>
|
||||
|
||||
{/* Folder Filter */}
|
||||
<Combobox
|
||||
options={folderOptions}
|
||||
multiSelect
|
||||
multiSelectValues={folderIds}
|
||||
onMultiSelectChange={setFolderIds}
|
||||
placeholder='Folder'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>{folderDisplayLabel}</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search folders...'
|
||||
showAllOption
|
||||
allOptionLabel='All folders'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-[6px]'
|
||||
/>
|
||||
|
||||
{/* Trigger Filter */}
|
||||
<Combobox
|
||||
options={triggerOptions}
|
||||
multiSelect
|
||||
multiSelectValues={triggers}
|
||||
onMultiSelectChange={setTriggers}
|
||||
placeholder='Trigger'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>{triggerDisplayLabel}</span>
|
||||
}
|
||||
searchable
|
||||
searchPlaceholder='Search triggers...'
|
||||
showAllOption
|
||||
allOptionLabel='All triggers'
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-[6px]'
|
||||
/>
|
||||
|
||||
{/* Timeline Filter */}
|
||||
<Combobox
|
||||
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
|
||||
value={timeRange}
|
||||
onChange={(val) => setTimeRange(val as typeof timeRange)}
|
||||
placeholder='Time'
|
||||
overlayContent={
|
||||
<span className='truncate text-[var(--text-primary)]'>{timeDisplayLabel}</span>
|
||||
}
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-[6px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,8 @@ export function useSearchState({
|
||||
const [sections, setSections] = useState<SuggestionSection[]>([])
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
|
||||
const [highlightedBadgeIndex, setHighlightedBadgeIndex] = useState<number | null>(null)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null)
|
||||
@@ -52,6 +54,7 @@ export function useSearchState({
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
setCurrentInput(value)
|
||||
setHighlightedBadgeIndex(null)
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
@@ -125,13 +128,24 @@ export function useSearchState({
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Backspace' && currentInput === '') {
|
||||
if (appliedFilters.length > 0) {
|
||||
event.preventDefault()
|
||||
removeBadge(appliedFilters.length - 1)
|
||||
event.preventDefault()
|
||||
|
||||
if (highlightedBadgeIndex !== null) {
|
||||
removeBadge(highlightedBadgeIndex)
|
||||
setHighlightedBadgeIndex(null)
|
||||
} else if (appliedFilters.length > 0) {
|
||||
setHighlightedBadgeIndex(appliedFilters.length - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
highlightedBadgeIndex !== null &&
|
||||
!['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)
|
||||
) {
|
||||
setHighlightedBadgeIndex(null)
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -180,6 +194,7 @@ export function useSearchState({
|
||||
[
|
||||
currentInput,
|
||||
appliedFilters,
|
||||
highlightedBadgeIndex,
|
||||
isOpen,
|
||||
highlightedIndex,
|
||||
suggestions,
|
||||
@@ -226,6 +241,7 @@ export function useSearchState({
|
||||
suggestions,
|
||||
sections,
|
||||
highlightedIndex,
|
||||
highlightedBadgeIndex,
|
||||
|
||||
inputRef,
|
||||
dropdownRef,
|
||||
@@ -238,7 +254,7 @@ export function useSearchState({
|
||||
removeBadge,
|
||||
clearAll,
|
||||
initializeFromQuery,
|
||||
|
||||
setHighlightedIndex,
|
||||
setHighlightedBadgeIndex,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* Logs layout - applies sidebar padding for all logs routes.
|
||||
*/
|
||||
export default function LogsLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className='flex h-full flex-1 flex-col overflow-hidden pl-60'>{children}</div>
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AlertCircle, ArrowUpRight, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Badge, buttonVariants } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
||||
import { useFolders } from '@/hooks/queries/folders'
|
||||
import { useLogDetail, useLogsList } from '@/hooks/queries/logs'
|
||||
import { useDashboardLogs, useLogDetail, useLogsList } from '@/hooks/queries/logs'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
import { useFilterStore } from '@/stores/logs/filters/store'
|
||||
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||
import { useUserPermissionsContext } from '../providers/workspace-permissions-provider'
|
||||
import { Dashboard, LogDetails, LogsToolbar, NotificationSettings } from './components'
|
||||
import { formatDate, formatDuration, StatusBadge, TriggerBadge } from './utils'
|
||||
import { Dashboard, LogDetails, LogsList, LogsToolbar, NotificationSettings } from './components'
|
||||
|
||||
const LOGS_PER_PAGE = 50 as const
|
||||
const REFRESH_SPINNER_DURATION_MS = 1000 as const
|
||||
@@ -56,20 +53,17 @@ export default function Logs() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300)
|
||||
|
||||
// Sync search query from URL on mount (client-side only)
|
||||
useEffect(() => {
|
||||
const urlSearch = new URLSearchParams(window.location.search).get('search') || ''
|
||||
if (urlSearch && urlSearch !== searchQuery) {
|
||||
setSearchQuery(urlSearch)
|
||||
}
|
||||
// Only run on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const [isLive, setIsLive] = useState(false)
|
||||
const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [dashboardRefreshTrigger, setDashboardRefreshTrigger] = useState(0)
|
||||
const isSearchOpenRef = useRef<boolean>(false)
|
||||
const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false)
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
@@ -92,8 +86,31 @@ export default function Logs() {
|
||||
refetchInterval: isLive ? 5000 : false,
|
||||
})
|
||||
|
||||
const dashboardFilters = useMemo(
|
||||
() => ({
|
||||
timeRange,
|
||||
level,
|
||||
workflowIds,
|
||||
folderIds,
|
||||
triggers,
|
||||
searchQuery: debouncedSearchQuery,
|
||||
}),
|
||||
[timeRange, level, workflowIds, folderIds, triggers, debouncedSearchQuery]
|
||||
)
|
||||
|
||||
const dashboardLogsQuery = useDashboardLogs(workspaceId, dashboardFilters, {
|
||||
enabled: Boolean(workspaceId) && isInitialized.current,
|
||||
refetchInterval: isLive ? 5000 : false,
|
||||
})
|
||||
|
||||
const logDetailQuery = useLogDetail(selectedLog?.id)
|
||||
|
||||
const mergedSelectedLog = useMemo(() => {
|
||||
if (!selectedLog) return null
|
||||
if (!logDetailQuery.data) return selectedLog
|
||||
return { ...selectedLog, ...logDetailQuery.data }
|
||||
}, [selectedLog, logDetailQuery.data])
|
||||
|
||||
const logs = useMemo(() => {
|
||||
if (!logsQuery.data?.pages) return []
|
||||
return logsQuery.data.pages.flatMap((page) => page.logs)
|
||||
@@ -107,10 +124,8 @@ export default function Logs() {
|
||||
}
|
||||
}, [debouncedSearchQuery, setStoreSearchQuery])
|
||||
|
||||
// Track previous log state for efficient change detection
|
||||
const prevSelectedLogRef = useRef<WorkflowLog | null>(null)
|
||||
|
||||
// Sync selected log with refreshed data from logs list
|
||||
useEffect(() => {
|
||||
if (!selectedLog?.id || logs.length === 0) return
|
||||
|
||||
@@ -119,32 +134,27 @@ export default function Logs() {
|
||||
|
||||
const prevLog = prevSelectedLogRef.current
|
||||
|
||||
// Check if status-related fields have changed (e.g., running -> done)
|
||||
const hasStatusChange =
|
||||
prevLog?.id === updatedLog.id &&
|
||||
(updatedLog.duration !== prevLog.duration ||
|
||||
updatedLog.level !== prevLog.level ||
|
||||
updatedLog.hasPendingPause !== prevLog.hasPendingPause)
|
||||
|
||||
// Only update if the log data actually changed
|
||||
if (updatedLog !== selectedLog) {
|
||||
setSelectedLog(updatedLog)
|
||||
prevSelectedLogRef.current = updatedLog
|
||||
}
|
||||
|
||||
// Update index in case position changed
|
||||
const newIndex = logs.findIndex((l) => l.id === selectedLog.id)
|
||||
if (newIndex !== selectedLogIndex) {
|
||||
setSelectedLogIndex(newIndex)
|
||||
}
|
||||
|
||||
// Refetch log details if status changed to keep details panel in sync
|
||||
if (hasStatusChange) {
|
||||
logDetailQuery.refetch()
|
||||
}
|
||||
}, [logs, selectedLog?.id, selectedLogIndex, logDetailQuery.refetch])
|
||||
}, [logs, selectedLog?.id, selectedLogIndex, logDetailQuery])
|
||||
|
||||
// Refetch log details during live mode
|
||||
useEffect(() => {
|
||||
if (!isLive || !selectedLog?.id) return
|
||||
|
||||
@@ -155,20 +165,24 @@ export default function Logs() {
|
||||
return () => clearInterval(interval)
|
||||
}, [isLive, selectedLog?.id, logDetailQuery])
|
||||
|
||||
const handleLogClick = (log: WorkflowLog) => {
|
||||
// If clicking on the same log that's already selected and sidebar is open, close it
|
||||
if (selectedLog?.id === log.id && isSidebarOpen) {
|
||||
handleCloseSidebar()
|
||||
return
|
||||
}
|
||||
const handleLogClick = useCallback(
|
||||
(log: WorkflowLog) => {
|
||||
if (selectedLog?.id === log.id && isSidebarOpen) {
|
||||
setIsSidebarOpen(false)
|
||||
setSelectedLog(null)
|
||||
setSelectedLogIndex(-1)
|
||||
prevSelectedLogRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, select the log and open the sidebar
|
||||
setSelectedLog(log)
|
||||
prevSelectedLogRef.current = log
|
||||
const index = logs.findIndex((l) => l.id === log.id)
|
||||
setSelectedLogIndex(index)
|
||||
setIsSidebarOpen(true)
|
||||
}
|
||||
setSelectedLog(log)
|
||||
prevSelectedLogRef.current = log
|
||||
const index = logs.findIndex((l) => l.id === log.id)
|
||||
setSelectedLogIndex(index)
|
||||
setIsSidebarOpen(true)
|
||||
},
|
||||
[selectedLog?.id, isSidebarOpen, logs]
|
||||
)
|
||||
|
||||
const handleNavigateNext = useCallback(() => {
|
||||
if (selectedLogIndex < logs.length - 1) {
|
||||
@@ -190,12 +204,12 @@ export default function Logs() {
|
||||
}
|
||||
}, [selectedLogIndex, logs])
|
||||
|
||||
const handleCloseSidebar = () => {
|
||||
const handleCloseSidebar = useCallback(() => {
|
||||
setIsSidebarOpen(false)
|
||||
setSelectedLog(null)
|
||||
setSelectedLogIndex(-1)
|
||||
prevSelectedLogRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowRef.current) {
|
||||
@@ -213,8 +227,6 @@ export default function Logs() {
|
||||
if (selectedLog?.id) {
|
||||
logDetailQuery.refetch()
|
||||
}
|
||||
// Also trigger dashboard refresh
|
||||
setDashboardRefreshTrigger((prev) => prev + 1)
|
||||
}, [logsQuery, logDetailQuery, selectedLog?.id])
|
||||
|
||||
const handleToggleLive = useCallback(() => {
|
||||
@@ -225,8 +237,6 @@ export default function Logs() {
|
||||
setIsVisuallyRefreshing(true)
|
||||
setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS)
|
||||
logsQuery.refetch()
|
||||
// Also trigger dashboard refresh
|
||||
setDashboardRefreshTrigger((prev) => prev + 1)
|
||||
}
|
||||
}, [isLive, logsQuery])
|
||||
|
||||
@@ -292,62 +302,6 @@ export default function Logs() {
|
||||
}
|
||||
}, [logsQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (logsQuery.isLoading || !logsQuery.hasNextPage) return
|
||||
|
||||
const scrollContainer = scrollContainerRef.current
|
||||
if (!scrollContainer) return
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainer) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer
|
||||
|
||||
const scrollPercentage = (scrollTop / (scrollHeight - clientHeight)) * 100
|
||||
|
||||
if (scrollPercentage > 60 && !logsQuery.isFetchingNextPage && logsQuery.hasNextPage) {
|
||||
loadMoreLogs()
|
||||
}
|
||||
}
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll)
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [logsQuery.isLoading, logsQuery.hasNextPage, logsQuery.isFetchingNextPage, loadMoreLogs])
|
||||
|
||||
useEffect(() => {
|
||||
const currentLoaderRef = loaderRef.current
|
||||
const scrollContainer = scrollContainerRef.current
|
||||
|
||||
if (!currentLoaderRef || !scrollContainer || logsQuery.isLoading || !logsQuery.hasNextPage)
|
||||
return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const e = entries[0]
|
||||
if (!e?.isIntersecting) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer
|
||||
const pct = (scrollTop / (scrollHeight - clientHeight)) * 100
|
||||
if (pct > 70 && !logsQuery.isFetchingNextPage) {
|
||||
loadMoreLogs()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainer,
|
||||
threshold: 0.1,
|
||||
rootMargin: '200px 0px 0px 0px',
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(currentLoaderRef)
|
||||
|
||||
return () => {
|
||||
observer.unobserve(currentLoaderRef)
|
||||
}
|
||||
}, [logsQuery.isLoading, logsQuery.hasNextPage, logsQuery.isFetchingNextPage, loadMoreLogs])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (isSearchOpenRef.current) return
|
||||
@@ -408,11 +362,15 @@ export default function Logs() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dashboard view - always mounted to preserve state and query cache */}
|
||||
{/* Dashboard view - uses all logs (non-paginated) for accurate metrics */}
|
||||
<div
|
||||
className={cn('flex min-h-0 flex-1 flex-col pr-[24px]', !isDashboardView && 'hidden')}
|
||||
>
|
||||
<Dashboard isLive={isLive} refreshTrigger={dashboardRefreshTrigger} />
|
||||
<Dashboard
|
||||
logs={dashboardLogsQuery.data ?? []}
|
||||
isLoading={!dashboardLogsQuery.data}
|
||||
error={dashboardLogsQuery.error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main content area with table - only show in logs view */}
|
||||
@@ -451,11 +409,8 @@ export default function Logs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table body - scrollable */}
|
||||
<div
|
||||
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden'
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
{/* Table body - virtualized */}
|
||||
<div className='min-h-0 flex-1 overflow-hidden' ref={scrollContainerRef}>
|
||||
{logsQuery.isLoading && !logsQuery.data ? (
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
|
||||
@@ -479,137 +434,23 @@ export default function Logs() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{logs.map((log) => {
|
||||
const formattedDate = formatDate(log.createdAt)
|
||||
const isSelected = selectedLog?.id === log.id
|
||||
const baseLevel = (log.level || 'info').toLowerCase()
|
||||
const isError = baseLevel === 'error'
|
||||
const isPending = !isError && log.hasPendingPause === true
|
||||
const isRunning = !isError && !isPending && log.duration === null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={log.id}
|
||||
ref={isSelected ? selectedRowRef : null}
|
||||
className={cn(
|
||||
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--c-2A2A2A)]',
|
||||
isSelected && 'bg-[var(--c-2A2A2A)]'
|
||||
)}
|
||||
onClick={() => handleLogClick(log)}
|
||||
>
|
||||
<div className='flex flex-1 items-center'>
|
||||
{/* Date */}
|
||||
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{formattedDate.compactDate}
|
||||
</span>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{formattedDate.compactTime}
|
||||
</span>
|
||||
|
||||
{/* Status */}
|
||||
<div className='w-[12%] min-w-[100px]'>
|
||||
<StatusBadge
|
||||
status={
|
||||
isError
|
||||
? 'error'
|
||||
: isPending
|
||||
? 'pending'
|
||||
: isRunning
|
||||
? 'running'
|
||||
: 'info'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Workflow */}
|
||||
<div className='flex w-[22%] min-w-[140px] items-center gap-[8px] pr-[8px]'>
|
||||
<div
|
||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: log.workflow?.color }}
|
||||
/>
|
||||
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{log.workflow?.name || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cost */}
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
{typeof log.cost?.total === 'number'
|
||||
? `$${log.cost.total.toFixed(4)}`
|
||||
: '—'}
|
||||
</span>
|
||||
|
||||
{/* Trigger */}
|
||||
<div className='w-[14%] min-w-[110px]'>
|
||||
{log.trigger ? (
|
||||
<TriggerBadge trigger={log.trigger} />
|
||||
) : (
|
||||
<span className='font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className='w-[20%] min-w-[100px]'>
|
||||
<Badge
|
||||
variant='default'
|
||||
className='rounded-[6px] px-[9px] py-[2px] text-[12px]'
|
||||
>
|
||||
{formatDuration(log.duration) || '—'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume Link */}
|
||||
{isPending && log.executionId && (log.workflow?.id || log.workflowId) && (
|
||||
<Link
|
||||
href={`/resume/${log.workflow?.id || log.workflowId}/${log.executionId}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'active' }),
|
||||
'absolute right-[24px] h-[26px] w-[26px] rounded-[6px] p-0'
|
||||
)}
|
||||
aria-label='Open resume console'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ArrowUpRight className='h-[14px] w-[14px]' />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Infinite scroll loader */}
|
||||
{logsQuery.hasNextPage && (
|
||||
<div className='flex items-center justify-center py-[16px]'>
|
||||
<div
|
||||
ref={loaderRef}
|
||||
className='flex items-center gap-[8px] text-[var(--text-secondary)]'
|
||||
>
|
||||
{logsQuery.isFetchingNextPage ? (
|
||||
<>
|
||||
<Loader2 className='h-[16px] w-[16px] animate-spin' />
|
||||
<span className='text-[13px]'>Loading more...</span>
|
||||
</>
|
||||
) : (
|
||||
<span className='text-[13px]'>Scroll to load more</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<LogsList
|
||||
logs={logs}
|
||||
selectedLogId={selectedLog?.id ?? null}
|
||||
onLogClick={handleLogClick}
|
||||
selectedRowRef={selectedRowRef}
|
||||
hasNextPage={logsQuery.hasNextPage ?? false}
|
||||
isFetchingNextPage={logsQuery.isFetchingNextPage}
|
||||
onLoadMore={loadMoreLogs}
|
||||
loaderRef={loaderRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Details - rendered inside table container */}
|
||||
<LogDetails
|
||||
log={logDetailQuery.data ? { ...selectedLog, ...logDetailQuery.data } : selectedLog}
|
||||
log={mergedSelectedLog}
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
onNavigateNext={handleNavigateNext}
|
||||
|
||||
@@ -448,7 +448,7 @@ export const formatDate = (dateString: string) => {
|
||||
formatted: format(date, 'HH:mm:ss'),
|
||||
compact: format(date, 'MMM d HH:mm:ss'),
|
||||
compactDate: format(date, 'MMM d').toUpperCase(),
|
||||
compactTime: format(date, 'h:mm:ss a'),
|
||||
compactTime: format(date, 'h:mm a'),
|
||||
relative: (() => {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
|
||||
@@ -16,6 +16,7 @@ const logger = createLogger('ProviderModelsLoader')
|
||||
function useSyncProvider(provider: ProviderName) {
|
||||
const setProviderModels = useProvidersStore((state) => state.setProviderModels)
|
||||
const setProviderLoading = useProvidersStore((state) => state.setProviderLoading)
|
||||
const setOpenRouterModelInfo = useProvidersStore((state) => state.setOpenRouterModelInfo)
|
||||
const { data, isLoading, isFetching, error } = useProviderModels(provider)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,18 +28,21 @@ function useSyncProvider(provider: ProviderName) {
|
||||
|
||||
try {
|
||||
if (provider === 'ollama') {
|
||||
updateOllamaProviderModels(data)
|
||||
updateOllamaProviderModels(data.models)
|
||||
} else if (provider === 'vllm') {
|
||||
updateVLLMProviderModels(data)
|
||||
updateVLLMProviderModels(data.models)
|
||||
} else if (provider === 'openrouter') {
|
||||
void updateOpenRouterProviderModels(data)
|
||||
void updateOpenRouterProviderModels(data.models)
|
||||
if (data.modelInfo) {
|
||||
setOpenRouterModelInfo(data.modelInfo)
|
||||
}
|
||||
}
|
||||
} catch (syncError) {
|
||||
logger.warn(`Failed to sync provider definitions for ${provider}`, syncError as Error)
|
||||
}
|
||||
|
||||
setProviderModels(provider, data)
|
||||
}, [provider, data, setProviderModels])
|
||||
setProviderModels(provider, data.models)
|
||||
}, [provider, data, setProviderModels, setOpenRouterModelInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
||||
@@ -43,6 +43,8 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'https://www.googleapis.com/auth/admin.directory.group.readonly': 'View Google Workspace groups',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member.readonly':
|
||||
'View Google Workspace group memberships',
|
||||
'https://www.googleapis.com/auth/cloud-platform':
|
||||
'Full access to Google Cloud resources for Vertex AI',
|
||||
'read:confluence-content.all': 'Read all Confluence content',
|
||||
'read:confluence-space.summary': 'Read Confluence space information',
|
||||
'read:space:confluence': 'View Confluence spaces',
|
||||
|
||||
@@ -398,6 +398,7 @@ export function useWorkflowExecution() {
|
||||
}
|
||||
|
||||
const streamCompletionTimes = new Map<string, number>()
|
||||
const processedFirstChunk = new Set<string>()
|
||||
|
||||
const onStream = async (streamingExecution: StreamingExecution) => {
|
||||
const promise = (async () => {
|
||||
@@ -405,16 +406,14 @@ export function useWorkflowExecution() {
|
||||
const reader = streamingExecution.stream.getReader()
|
||||
const blockId = (streamingExecution.execution as any)?.blockId
|
||||
|
||||
let isFirstChunk = true
|
||||
|
||||
if (blockId) {
|
||||
if (blockId && !streamedContent.has(blockId)) {
|
||||
streamedContent.set(blockId, '')
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
// Record when this stream completed
|
||||
if (blockId) {
|
||||
streamCompletionTimes.set(blockId, Date.now())
|
||||
}
|
||||
@@ -425,13 +424,12 @@ export function useWorkflowExecution() {
|
||||
streamedContent.set(blockId, (streamedContent.get(blockId) || '') + chunk)
|
||||
}
|
||||
|
||||
// Add separator before first chunk if this isn't the first block
|
||||
let chunkToSend = chunk
|
||||
if (isFirstChunk && streamedContent.size > 1) {
|
||||
chunkToSend = `\n\n${chunk}`
|
||||
isFirstChunk = false
|
||||
} else if (isFirstChunk) {
|
||||
isFirstChunk = false
|
||||
if (blockId && !processedFirstChunk.has(blockId)) {
|
||||
processedFirstChunk.add(blockId)
|
||||
if (streamedContent.size > 1) {
|
||||
chunkToSend = `\n\n${chunk}`
|
||||
}
|
||||
}
|
||||
|
||||
controller.enqueue(encodeSSE({ blockId, chunk: chunkToSend }))
|
||||
|
||||
@@ -2265,7 +2265,14 @@ const WorkflowContent = React.memo(() => {
|
||||
className={`absolute inset-0 z-[5] flex items-center justify-center bg-[var(--bg)] transition-opacity duration-150 ${isWorkflowReady ? 'pointer-events-none opacity-0' : 'opacity-100'}`}
|
||||
>
|
||||
<div
|
||||
className={`h-[18px] w-[18px] rounded-full border-[1.5px] border-muted-foreground border-t-transparent ${isWorkflowReady ? '' : 'animate-spin'}`}
|
||||
className={`h-[18px] w-[18px] rounded-full ${isWorkflowReady ? '' : 'animate-spin'}`}
|
||||
style={{
|
||||
background:
|
||||
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
|
||||
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
|
||||
WebkitMask:
|
||||
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -58,7 +58,16 @@ export default function WorkflowsPage() {
|
||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='relative h-full w-full flex-1 bg-[var(--bg)]'>
|
||||
<div className='workflow-container flex h-full items-center justify-center bg-[var(--bg)]'>
|
||||
<div className='h-[18px] w-[18px] animate-spin rounded-full border-[1.5px] border-muted-foreground border-t-transparent' />
|
||||
<div
|
||||
className='h-[18px] w-[18px] animate-spin rounded-full'
|
||||
style={{
|
||||
background:
|
||||
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
|
||||
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
|
||||
WebkitMask:
|
||||
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Panel />
|
||||
</div>
|
||||
|
||||
@@ -117,7 +117,16 @@ export default function WorkspacePage() {
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className='flex h-screen w-full items-center justify-center'>
|
||||
<div className='h-[18px] w-[18px] animate-spin rounded-full border-[1.5px] border-muted-foreground border-t-transparent' />
|
||||
<div
|
||||
className='h-[18px] w-[18px] animate-spin rounded-full'
|
||||
style={{
|
||||
background:
|
||||
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
|
||||
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
|
||||
WebkitMask:
|
||||
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
getMaxTemperature,
|
||||
getProviderIcon,
|
||||
getReasoningEffortValuesForModel,
|
||||
getThinkingLevelsForModel,
|
||||
getVerbosityValuesForModel,
|
||||
MODELS_WITH_REASONING_EFFORT,
|
||||
MODELS_WITH_THINKING,
|
||||
MODELS_WITH_VERBOSITY,
|
||||
providers,
|
||||
supportsTemperature,
|
||||
@@ -71,8 +73,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
longDescription:
|
||||
'The Agent block is a core workflow block that is a wrapper around an LLM. It takes in system/user prompts and calls an LLM provider. It can also make tool calls by directly containing tools inside of its tool input. It can additionally return structured output.',
|
||||
bestPractices: `
|
||||
- Cannot use core blocks like API, Webhook, Function, Workflow, Memory as tools. Only integrations or custom tools.
|
||||
- Check custom tools examples for YAML syntax. Only construct these if there isn't an existing integration for that purpose.
|
||||
- Prefer using integrations as tools within the agent block over separate integration blocks unless complete determinism needed.
|
||||
- Response Format should be a valid JSON Schema. This determines the output of the agent only if present. Fields can be accessed at root level by the following blocks: e.g. <agent1.field>. If response format is not present, the agent will return the standard outputs: content, model, tokens, toolCalls.
|
||||
`,
|
||||
docsLink: 'https://docs.sim.ai/blocks/agent',
|
||||
@@ -109,7 +110,19 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'vertexCredential',
|
||||
title: 'Google Cloud Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'vertex-ai',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
placeholder: 'Select Google Cloud account',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'model',
|
||||
value: providers.vertex.models,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'reasoningEffort',
|
||||
title: 'Reasoning Effort',
|
||||
@@ -216,6 +229,57 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
value: MODELS_WITH_VERBOSITY,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'thinkingLevel',
|
||||
title: 'Thinking Level',
|
||||
type: 'dropdown',
|
||||
placeholder: 'Select thinking level...',
|
||||
options: [
|
||||
{ label: 'minimal', id: 'minimal' },
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'medium', id: 'medium' },
|
||||
{ label: 'high', id: 'high' },
|
||||
],
|
||||
dependsOn: ['model'],
|
||||
fetchOptions: async (blockId: string) => {
|
||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) {
|
||||
return [
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
|
||||
const blockValues = workflowValues?.[blockId]
|
||||
const modelValue = blockValues?.model as string
|
||||
|
||||
if (!modelValue) {
|
||||
return [
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
const validOptions = getThinkingLevelsForModel(modelValue)
|
||||
if (!validOptions) {
|
||||
return [
|
||||
{ label: 'low', id: 'low' },
|
||||
{ label: 'high', id: 'high' },
|
||||
]
|
||||
}
|
||||
|
||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
||||
},
|
||||
value: () => 'high',
|
||||
condition: {
|
||||
field: 'model',
|
||||
value: MODELS_WITH_THINKING,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'azureEndpoint',
|
||||
@@ -276,17 +340,21 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
password: true,
|
||||
connectionDroppable: false,
|
||||
required: true,
|
||||
// Hide API key for hosted models, Ollama models, and vLLM models
|
||||
// Hide API key for hosted models, Ollama models, vLLM models, and Vertex models (uses OAuth)
|
||||
condition: isHosted
|
||||
? {
|
||||
field: 'model',
|
||||
value: getHostedModels(),
|
||||
value: [...getHostedModels(), ...providers.vertex.models],
|
||||
not: true, // Show for all models EXCEPT those listed
|
||||
}
|
||||
: () => ({
|
||||
field: 'model',
|
||||
value: [...getCurrentOllamaModels(), ...getCurrentVLLMModels()],
|
||||
not: true, // Show for all models EXCEPT Ollama and vLLM models
|
||||
value: [
|
||||
...getCurrentOllamaModels(),
|
||||
...getCurrentVLLMModels(),
|
||||
...providers.vertex.models,
|
||||
],
|
||||
not: true, // Show for all models EXCEPT Ollama, vLLM, and Vertex models
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -610,6 +678,7 @@ Example 3 (Array Input):
|
||||
temperature: { type: 'number', description: 'Response randomness level' },
|
||||
reasoningEffort: { type: 'string', description: 'Reasoning effort level for GPT-5 models' },
|
||||
verbosity: { type: 'string', description: 'Verbosity level for GPT-5 models' },
|
||||
thinkingLevel: { type: 'string', description: 'Thinking level for Gemini 3 models' },
|
||||
tools: { type: 'json', description: 'Available tools configuration' },
|
||||
},
|
||||
outputs: {
|
||||
|
||||
@@ -47,6 +47,19 @@ export const IntercomBlock: BlockConfig = {
|
||||
required: true,
|
||||
},
|
||||
// Contact fields
|
||||
{
|
||||
id: 'role',
|
||||
title: 'Role',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Lead', id: 'lead' },
|
||||
{ label: 'User', id: 'user' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_contact', 'update_contact'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contactId',
|
||||
title: 'Contact ID',
|
||||
@@ -75,7 +88,7 @@ export const IntercomBlock: BlockConfig = {
|
||||
placeholder: 'External identifier for the contact',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_contact'],
|
||||
value: ['create_contact', 'update_contact'],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -161,6 +174,16 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['create_contact', 'update_contact'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'contact_company_id',
|
||||
title: 'Company ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Company ID to associate with contact',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_contact', 'update_contact'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Search Query',
|
||||
@@ -172,6 +195,29 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['search_contacts', 'search_conversations'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sort_field',
|
||||
title: 'Sort Field',
|
||||
type: 'short-input',
|
||||
placeholder: 'Field to sort by (e.g., name, created_at)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['search_contacts', 'search_conversations'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sort_order',
|
||||
title: 'Sort Order',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Descending', id: 'descending' },
|
||||
{ label: 'Ascending', id: 'ascending' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['search_contacts', 'search_conversations'],
|
||||
},
|
||||
},
|
||||
// Company fields
|
||||
{
|
||||
id: 'companyId',
|
||||
@@ -255,6 +301,16 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['create_company'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'remote_created_at',
|
||||
title: 'Remote Created At',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix timestamp when company was created',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_company'],
|
||||
},
|
||||
},
|
||||
// Conversation fields
|
||||
{
|
||||
id: 'conversationId',
|
||||
@@ -280,6 +336,42 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['get_conversation'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'include_translations',
|
||||
title: 'Include Translations',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'False', id: 'false' },
|
||||
{ label: 'True', id: 'true' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_conversation'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sort',
|
||||
title: 'Sort By',
|
||||
type: 'short-input',
|
||||
placeholder: 'Field to sort by (e.g., waiting_since, updated_at)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_conversations'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
title: 'Order',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Descending', id: 'desc' },
|
||||
{ label: 'Ascending', id: 'asc' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_conversations'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'message_type',
|
||||
title: 'Message Type',
|
||||
@@ -326,6 +418,16 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['reply_conversation'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'reply_created_at',
|
||||
title: 'Created At',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix timestamp for reply creation time',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['reply_conversation'],
|
||||
},
|
||||
},
|
||||
// Ticket fields
|
||||
{
|
||||
id: 'ticketId',
|
||||
@@ -371,6 +473,49 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['create_ticket'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ticket_company_id',
|
||||
title: 'Company ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Company ID to associate with ticket',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_ticket'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ticket_created_at',
|
||||
title: 'Created At',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix timestamp for ticket creation time',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_ticket'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'conversation_to_link_id',
|
||||
title: 'Conversation to Link',
|
||||
type: 'short-input',
|
||||
placeholder: 'ID of conversation to link to ticket',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_ticket'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'disable_notifications',
|
||||
title: 'Disable Notifications',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'False', id: 'false' },
|
||||
{ label: 'True', id: 'true' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_ticket'],
|
||||
},
|
||||
},
|
||||
// Message fields
|
||||
{
|
||||
id: 'message_type_msg',
|
||||
@@ -386,6 +531,20 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['create_message'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'template',
|
||||
title: 'Template',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Plain', id: 'plain' },
|
||||
{ label: 'Personal', id: 'personal' },
|
||||
],
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_message'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subject',
|
||||
title: 'Subject',
|
||||
@@ -440,6 +599,16 @@ export const IntercomBlock: BlockConfig = {
|
||||
value: ['create_message'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'message_created_at',
|
||||
title: 'Created At',
|
||||
type: 'short-input',
|
||||
placeholder: 'Unix timestamp for message creation time',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_message'],
|
||||
},
|
||||
},
|
||||
// Pagination fields
|
||||
{
|
||||
id: 'per_page',
|
||||
@@ -464,7 +633,13 @@ export const IntercomBlock: BlockConfig = {
|
||||
placeholder: 'Cursor for pagination',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_contacts', 'search_contacts', 'list_conversations', 'search_conversations'],
|
||||
value: [
|
||||
'list_contacts',
|
||||
'search_contacts',
|
||||
'list_companies',
|
||||
'list_conversations',
|
||||
'search_conversations',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -537,7 +712,19 @@ export const IntercomBlock: BlockConfig = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, message_type_msg, company_name, ...rest } = params
|
||||
const {
|
||||
operation,
|
||||
message_type_msg,
|
||||
company_name,
|
||||
contact_company_id,
|
||||
reply_created_at,
|
||||
ticket_company_id,
|
||||
ticket_created_at,
|
||||
message_created_at,
|
||||
include_translations,
|
||||
disable_notifications,
|
||||
...rest
|
||||
} = params
|
||||
const cleanParams: Record<string, any> = {}
|
||||
|
||||
// Special mapping for message_type in create_message
|
||||
@@ -550,6 +737,42 @@ export const IntercomBlock: BlockConfig = {
|
||||
cleanParams.name = company_name
|
||||
}
|
||||
|
||||
// Map contact_company_id to company_id for contact operations
|
||||
if (
|
||||
(operation === 'create_contact' || operation === 'update_contact') &&
|
||||
contact_company_id
|
||||
) {
|
||||
cleanParams.company_id = contact_company_id
|
||||
}
|
||||
|
||||
// Map reply_created_at to created_at for reply_conversation
|
||||
if (operation === 'reply_conversation' && reply_created_at) {
|
||||
cleanParams.created_at = Number(reply_created_at)
|
||||
}
|
||||
|
||||
// Map ticket fields
|
||||
if (operation === 'create_ticket') {
|
||||
if (ticket_company_id) cleanParams.company_id = ticket_company_id
|
||||
if (ticket_created_at) cleanParams.created_at = Number(ticket_created_at)
|
||||
if (disable_notifications !== undefined && disable_notifications !== '') {
|
||||
cleanParams.disable_notifications = disable_notifications === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
// Map message_created_at to created_at for create_message
|
||||
if (operation === 'create_message' && message_created_at) {
|
||||
cleanParams.created_at = Number(message_created_at)
|
||||
}
|
||||
|
||||
// Convert include_translations string to boolean for get_conversation
|
||||
if (
|
||||
operation === 'get_conversation' &&
|
||||
include_translations !== undefined &&
|
||||
include_translations !== ''
|
||||
) {
|
||||
cleanParams.include_translations = include_translations === 'true'
|
||||
}
|
||||
|
||||
Object.entries(rest).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
cleanParams[key] = value
|
||||
|
||||
@@ -8,7 +8,6 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
longDescription:
|
||||
'Integrate Knowledge into the workflow. Can search, upload chunks, and create documents.',
|
||||
bestPractices: `
|
||||
- Search up examples with knowledge base blocks to understand YAML syntax.
|
||||
- Clarify which tags are available for the knowledge base to understand whether to use tag filters on a search.
|
||||
`,
|
||||
bgColor: '#00B0B0',
|
||||
|
||||
@@ -10,8 +10,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
bgColor: '#F64F9E',
|
||||
bestPractices: `
|
||||
- Do not use this block unless the user explicitly asks for it.
|
||||
- Search up examples with memory blocks to understand YAML syntax.
|
||||
- Used in conjunction with agent blocks to persist messages between runs. User messages should be added with role 'user' and assistant messages should be added with role 'assistant' with the agent sandwiched between.
|
||||
- Used in conjunction with agent blocks to inject artificial memory into the conversation. For natural conversations, use the agent block memories modes directly instead.
|
||||
`,
|
||||
icon: BrainIcon,
|
||||
category: 'blocks',
|
||||
@@ -41,17 +40,6 @@ export const MemoryBlock: BlockConfig = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'blockId',
|
||||
title: 'Block ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter block ID (optional, defaults to current block)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'add',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
title: 'Conversation ID',
|
||||
@@ -61,29 +49,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
field: 'operation',
|
||||
value: 'get',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'blockId',
|
||||
title: 'Block ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter block ID (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'get',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'blockName',
|
||||
title: 'Block Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter block name (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'get',
|
||||
},
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
@@ -94,29 +60,7 @@ export const MemoryBlock: BlockConfig = {
|
||||
field: 'operation',
|
||||
value: 'delete',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'blockId',
|
||||
title: 'Block ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter block ID (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'delete',
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'blockName',
|
||||
title: 'Block Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter block name (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'delete',
|
||||
},
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
@@ -186,10 +130,8 @@ export const MemoryBlock: BlockConfig = {
|
||||
}
|
||||
|
||||
if (params.operation === 'get' || params.operation === 'delete') {
|
||||
if (!conversationId && !params.blockId && !params.blockName) {
|
||||
errors.push(
|
||||
`At least one of ID, blockId, or blockName is required for ${params.operation} operation`
|
||||
)
|
||||
if (!conversationId) {
|
||||
errors.push(`Conversation ID is required for ${params.operation} operation`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,33 +142,26 @@ export const MemoryBlock: BlockConfig = {
|
||||
const baseResult: Record<string, any> = {}
|
||||
|
||||
if (params.operation === 'add') {
|
||||
const result: Record<string, any> = {
|
||||
return {
|
||||
...baseResult,
|
||||
conversationId: conversationId,
|
||||
role: params.role,
|
||||
content: params.content,
|
||||
}
|
||||
if (params.blockId) {
|
||||
result.blockId = params.blockId
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (params.operation === 'get') {
|
||||
const result: Record<string, any> = { ...baseResult }
|
||||
if (conversationId) result.conversationId = conversationId
|
||||
if (params.blockId) result.blockId = params.blockId
|
||||
if (params.blockName) result.blockName = params.blockName
|
||||
return result
|
||||
return {
|
||||
...baseResult,
|
||||
conversationId: conversationId,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.operation === 'delete') {
|
||||
const result: Record<string, any> = { ...baseResult }
|
||||
if (conversationId) result.conversationId = conversationId
|
||||
if (params.blockId) result.blockId = params.blockId
|
||||
if (params.blockName) result.blockName = params.blockName
|
||||
return result
|
||||
return {
|
||||
...baseResult,
|
||||
conversationId: conversationId,
|
||||
}
|
||||
}
|
||||
|
||||
return baseResult
|
||||
@@ -235,10 +170,8 @@ export const MemoryBlock: BlockConfig = {
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
id: { type: 'string', description: 'Memory identifier (for add operation)' },
|
||||
id: { type: 'string', description: 'Memory identifier (conversation ID)' },
|
||||
conversationId: { type: 'string', description: 'Conversation identifier' },
|
||||
blockId: { type: 'string', description: 'Block identifier' },
|
||||
blockName: { type: 'string', description: 'Block name' },
|
||||
role: { type: 'string', description: 'Agent role' },
|
||||
content: { type: 'string', description: 'Memory content' },
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ export const ScheduleBlock: BlockConfig = {
|
||||
longDescription:
|
||||
'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.',
|
||||
bestPractices: `
|
||||
- Search up examples with schedule blocks to understand YAML syntax.
|
||||
- Prefer the custom cron expression input method over the other schedule configuration methods.
|
||||
- Clarify the timezone if the user doesn't specify it.
|
||||
`,
|
||||
|
||||
20
apps/sim/components/emcn/icons/animate/loader.module.css
Normal file
20
apps/sim/components/emcn/icons/animate/loader.module.css
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Loader icon animation
|
||||
* Continuous spinning animation for loading states
|
||||
* Uses GPU acceleration for smooth performance
|
||||
*/
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animated-loader-svg {
|
||||
animation: spin 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export { HexSimple } from './hex-simple'
|
||||
export { Key } from './key'
|
||||
export { Layout } from './layout'
|
||||
export { Library } from './library'
|
||||
export { Loader } from './loader'
|
||||
export { MoreHorizontal } from './more-horizontal'
|
||||
export { NoWrap } from './no-wrap'
|
||||
export { PanelLeft } from './panel-left'
|
||||
|
||||
42
apps/sim/components/emcn/icons/loader.tsx
Normal file
42
apps/sim/components/emcn/icons/loader.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { SVGProps } from 'react'
|
||||
import styles from '@/components/emcn/icons/animate/loader.module.css'
|
||||
|
||||
export interface LoaderProps extends SVGProps<SVGSVGElement> {
|
||||
/**
|
||||
* Enable animation on the loader icon
|
||||
* @default false
|
||||
*/
|
||||
animate?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader icon component with optional CSS-based spinning animation
|
||||
* Based on refresh-cw but without the arrows, just the circular arcs.
|
||||
* When animate is false, this is a lightweight static icon with no animation overhead.
|
||||
* When animate is true, CSS module animations are applied for continuous spin.
|
||||
* @param props - SVG properties including className, animate, etc.
|
||||
*/
|
||||
export function Loader({ animate = false, className, ...props }: LoaderProps) {
|
||||
const svgClassName = animate
|
||||
? `${styles['animated-loader-svg']} ${className || ''}`.trim()
|
||||
: className
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={svgClassName}
|
||||
{...props}
|
||||
>
|
||||
<path d='M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74' />
|
||||
<path d='M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -128,6 +128,8 @@ export const DEFAULTS = {
|
||||
BLOCK_TITLE: 'Untitled Block',
|
||||
WORKFLOW_NAME: 'Workflow',
|
||||
MAX_LOOP_ITERATIONS: 1000,
|
||||
MAX_FOREACH_ITEMS: 1000,
|
||||
MAX_PARALLEL_BRANCHES: 20,
|
||||
MAX_WORKFLOW_DEPTH: 10,
|
||||
EXECUTION_TIME: 0,
|
||||
TOKENS: {
|
||||
@@ -158,11 +160,19 @@ export const HTTP = {
|
||||
|
||||
export const AGENT = {
|
||||
DEFAULT_MODEL: 'claude-sonnet-4-5',
|
||||
DEFAULT_FUNCTION_TIMEOUT: 600000, // 10 minutes for custom tool code execution
|
||||
REQUEST_TIMEOUT: 600000, // 10 minutes for LLM API requests
|
||||
DEFAULT_FUNCTION_TIMEOUT: 600000,
|
||||
REQUEST_TIMEOUT: 600000,
|
||||
CUSTOM_TOOL_PREFIX: 'custom_',
|
||||
} as const
|
||||
|
||||
export const MEMORY = {
|
||||
DEFAULT_SLIDING_WINDOW_SIZE: 10,
|
||||
DEFAULT_SLIDING_WINDOW_TOKENS: 4000,
|
||||
CONTEXT_WINDOW_UTILIZATION: 0.9,
|
||||
MAX_CONVERSATION_ID_LENGTH: 255,
|
||||
MAX_MESSAGE_CONTENT_BYTES: 100 * 1024,
|
||||
} as const
|
||||
|
||||
export const ROUTER = {
|
||||
DEFAULT_MODEL: 'gpt-4o',
|
||||
DEFAULT_TEMPERATURE: 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LoopConstructor } from '@/executor/dag/construction/loops'
|
||||
import { NodeConstructor } from '@/executor/dag/construction/nodes'
|
||||
import { PathConstructor } from '@/executor/dag/construction/paths'
|
||||
import type { DAGEdge, NodeMetadata } from '@/executor/dag/types'
|
||||
import { buildSentinelStartId, extractBaseBlockId } from '@/executor/utils/subflow-utils'
|
||||
import type {
|
||||
SerializedBlock,
|
||||
SerializedLoop,
|
||||
@@ -79,6 +80,9 @@ export class DAGBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate loop and parallel structure
|
||||
this.validateSubflowStructure(dag)
|
||||
|
||||
logger.info('DAG built', {
|
||||
totalNodes: dag.nodes.size,
|
||||
loopCount: dag.loopConfigs.size,
|
||||
@@ -105,4 +109,43 @@ export class DAGBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that loops and parallels have proper internal structure.
|
||||
* Throws an error if a loop/parallel has no blocks inside or no connections from start.
|
||||
*/
|
||||
private validateSubflowStructure(dag: DAG): void {
|
||||
for (const [id, config] of dag.loopConfigs) {
|
||||
this.validateSubflow(dag, id, config.nodes, 'Loop')
|
||||
}
|
||||
for (const [id, config] of dag.parallelConfigs) {
|
||||
this.validateSubflow(dag, id, config.nodes, 'Parallel')
|
||||
}
|
||||
}
|
||||
|
||||
private validateSubflow(
|
||||
dag: DAG,
|
||||
id: string,
|
||||
nodes: string[] | undefined,
|
||||
type: 'Loop' | 'Parallel'
|
||||
): void {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
throw new Error(
|
||||
`${type} has no blocks inside. Add at least one block to the ${type.toLowerCase()}.`
|
||||
)
|
||||
}
|
||||
|
||||
const sentinelStartNode = dag.nodes.get(buildSentinelStartId(id))
|
||||
if (!sentinelStartNode) return
|
||||
|
||||
const hasConnections = Array.from(sentinelStartNode.outgoingEdges.values()).some((edge) =>
|
||||
nodes.includes(extractBaseBlockId(edge.target))
|
||||
)
|
||||
|
||||
if (!hasConnections) {
|
||||
throw new Error(
|
||||
`${type} start is not connected to any blocks. Connect a block to the ${type.toLowerCase()} start.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,10 @@ export class DAGExecutor {
|
||||
|
||||
const resolver = new VariableResolver(this.workflow, this.workflowVariables, state)
|
||||
const loopOrchestrator = new LoopOrchestrator(dag, state, resolver)
|
||||
loopOrchestrator.setContextExtensions(this.contextExtensions)
|
||||
const parallelOrchestrator = new ParallelOrchestrator(dag, state)
|
||||
parallelOrchestrator.setResolver(resolver)
|
||||
parallelOrchestrator.setContextExtensions(this.contextExtensions)
|
||||
const allHandlers = createBlockHandlers()
|
||||
const blockExecutor = new BlockExecutor(allHandlers, resolver, this.contextExtensions, state)
|
||||
const edgeManager = new EdgeManager(dag)
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface LoopScope {
|
||||
condition?: string
|
||||
loopType?: 'for' | 'forEach' | 'while' | 'doWhile'
|
||||
skipFirstConditionCheck?: boolean
|
||||
/** Error message if loop validation failed (e.g., exceeded max iterations) */
|
||||
validationError?: string
|
||||
}
|
||||
|
||||
export interface ParallelScope {
|
||||
@@ -23,6 +25,8 @@ export interface ParallelScope {
|
||||
completedCount: number
|
||||
totalExpectedNodes: number
|
||||
items?: any[]
|
||||
/** Error message if parallel validation failed (e.g., exceeded max branches) */
|
||||
validationError?: string
|
||||
}
|
||||
|
||||
export class ExecutionState implements BlockStateController {
|
||||
|
||||
@@ -1140,7 +1140,7 @@ describe('AgentBlockHandler', () => {
|
||||
expect(systemMessages[0].content).toBe('You are a helpful assistant.')
|
||||
})
|
||||
|
||||
it('should prioritize messages array system message over system messages in memories', async () => {
|
||||
it('should prefix agent system message before legacy memories', async () => {
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
@@ -1163,25 +1163,26 @@ describe('AgentBlockHandler', () => {
|
||||
const requestBody = JSON.parse(fetchCall[1].body)
|
||||
|
||||
// Verify messages were built correctly
|
||||
// Agent system (1) + legacy memories (3) + user from messages (1) = 5
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
expect(requestBody.messages.length).toBe(5) // memory system + 2 non-system memories + 2 from messages array
|
||||
expect(requestBody.messages.length).toBe(5)
|
||||
|
||||
// All messages should be present (memories first, then messages array)
|
||||
// Memory messages come first
|
||||
// Agent's system message is prefixed first
|
||||
expect(requestBody.messages[0].role).toBe('system')
|
||||
expect(requestBody.messages[0].content).toBe('Old system message from memories.')
|
||||
expect(requestBody.messages[1].role).toBe('user')
|
||||
expect(requestBody.messages[1].content).toBe('Hello!')
|
||||
expect(requestBody.messages[2].role).toBe('assistant')
|
||||
expect(requestBody.messages[2].content).toBe('Hi there!')
|
||||
// Then messages array
|
||||
expect(requestBody.messages[3].role).toBe('system')
|
||||
expect(requestBody.messages[3].content).toBe('You are a helpful assistant.')
|
||||
expect(requestBody.messages[0].content).toBe('You are a helpful assistant.')
|
||||
// Then legacy memories (with their system message preserved)
|
||||
expect(requestBody.messages[1].role).toBe('system')
|
||||
expect(requestBody.messages[1].content).toBe('Old system message from memories.')
|
||||
expect(requestBody.messages[2].role).toBe('user')
|
||||
expect(requestBody.messages[2].content).toBe('Hello!')
|
||||
expect(requestBody.messages[3].role).toBe('assistant')
|
||||
expect(requestBody.messages[3].content).toBe('Hi there!')
|
||||
// Then user message from messages array
|
||||
expect(requestBody.messages[4].role).toBe('user')
|
||||
expect(requestBody.messages[4].content).toBe('What should I do?')
|
||||
})
|
||||
|
||||
it('should handle multiple system messages in memories with messages array', async () => {
|
||||
it('should prefix agent system message and preserve legacy memory system messages', async () => {
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
@@ -1207,21 +1208,23 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
// Verify messages were built correctly
|
||||
expect(requestBody.messages).toBeDefined()
|
||||
expect(requestBody.messages.length).toBe(7) // 5 memory messages (3 system + 2 conversation) + 2 from messages array
|
||||
expect(requestBody.messages.length).toBe(7)
|
||||
|
||||
// All messages should be present in order
|
||||
// Agent's system message prefixed first
|
||||
expect(requestBody.messages[0].role).toBe('system')
|
||||
expect(requestBody.messages[0].content).toBe('First system message.')
|
||||
expect(requestBody.messages[1].role).toBe('user')
|
||||
expect(requestBody.messages[1].content).toBe('Hello!')
|
||||
expect(requestBody.messages[2].role).toBe('system')
|
||||
expect(requestBody.messages[2].content).toBe('Second system message.')
|
||||
expect(requestBody.messages[3].role).toBe('assistant')
|
||||
expect(requestBody.messages[3].content).toBe('Hi there!')
|
||||
expect(requestBody.messages[4].role).toBe('system')
|
||||
expect(requestBody.messages[4].content).toBe('Third system message.')
|
||||
expect(requestBody.messages[0].content).toBe('You are a helpful assistant.')
|
||||
// Then legacy memories with their system messages preserved in order
|
||||
expect(requestBody.messages[1].role).toBe('system')
|
||||
expect(requestBody.messages[1].content).toBe('First system message.')
|
||||
expect(requestBody.messages[2].role).toBe('user')
|
||||
expect(requestBody.messages[2].content).toBe('Hello!')
|
||||
expect(requestBody.messages[3].role).toBe('system')
|
||||
expect(requestBody.messages[3].content).toBe('Second system message.')
|
||||
expect(requestBody.messages[4].role).toBe('assistant')
|
||||
expect(requestBody.messages[4].content).toBe('Hi there!')
|
||||
expect(requestBody.messages[5].role).toBe('system')
|
||||
expect(requestBody.messages[5].content).toBe('You are a helpful assistant.')
|
||||
expect(requestBody.messages[5].content).toBe('Third system message.')
|
||||
// Then user message from messages array
|
||||
expect(requestBody.messages[6].role).toBe('user')
|
||||
expect(requestBody.messages[6].content).toBe('Continue our conversation.')
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { db } from '@sim/db'
|
||||
import { mcpServers } from '@sim/db/schema'
|
||||
import { account, mcpServers } from '@sim/db/schema'
|
||||
import { and, eq, inArray, isNull } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { createMcpToolId } from '@/lib/mcp/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/constants'
|
||||
@@ -47,7 +48,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
const providerId = getProviderFromModel(model)
|
||||
const formattedTools = await this.formatTools(ctx, filteredInputs.tools || [])
|
||||
const streamingConfig = this.getStreamingConfig(ctx, block)
|
||||
const messages = await this.buildMessages(ctx, filteredInputs, block.id)
|
||||
const messages = await this.buildMessages(ctx, filteredInputs)
|
||||
|
||||
const providerRequest = this.buildProviderRequest({
|
||||
ctx,
|
||||
@@ -68,7 +69,20 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
filteredInputs
|
||||
)
|
||||
|
||||
await this.persistResponseToMemory(ctx, filteredInputs, result, block.id)
|
||||
if (this.isStreamingExecution(result)) {
|
||||
if (filteredInputs.memoryType && filteredInputs.memoryType !== 'none') {
|
||||
return this.wrapStreamForMemoryPersistence(
|
||||
ctx,
|
||||
filteredInputs,
|
||||
result as StreamingExecution
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
if (filteredInputs.memoryType && filteredInputs.memoryType !== 'none') {
|
||||
await this.persistResponseToMemory(ctx, filteredInputs, result as BlockOutput)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -686,81 +700,102 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
private async buildMessages(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
blockId: string
|
||||
inputs: AgentInputs
|
||||
): Promise<Message[] | undefined> {
|
||||
const messages: Message[] = []
|
||||
const memoryEnabled = inputs.memoryType && inputs.memoryType !== 'none'
|
||||
|
||||
// 1. Fetch memory history if configured (industry standard: chronological order)
|
||||
if (inputs.memoryType && inputs.memoryType !== 'none') {
|
||||
const memoryMessages = await memoryService.fetchMemoryMessages(ctx, inputs, blockId)
|
||||
messages.push(...memoryMessages)
|
||||
// 1. Extract and validate messages from messages-input subblock
|
||||
const inputMessages = this.extractValidMessages(inputs.messages)
|
||||
const systemMessages = inputMessages.filter((m) => m.role === 'system')
|
||||
const conversationMessages = inputMessages.filter((m) => m.role !== 'system')
|
||||
|
||||
// 2. Handle native memory: seed on first run, then fetch and append new user input
|
||||
if (memoryEnabled && ctx.workspaceId) {
|
||||
const memoryMessages = await memoryService.fetchMemoryMessages(ctx, inputs)
|
||||
const hasExisting = memoryMessages.length > 0
|
||||
|
||||
if (!hasExisting && conversationMessages.length > 0) {
|
||||
const taggedMessages = conversationMessages.map((m) =>
|
||||
m.role === 'user' ? { ...m, executionId: ctx.executionId } : m
|
||||
)
|
||||
await memoryService.seedMemory(ctx, inputs, taggedMessages)
|
||||
messages.push(...taggedMessages)
|
||||
} else {
|
||||
messages.push(...memoryMessages)
|
||||
|
||||
if (hasExisting && conversationMessages.length > 0) {
|
||||
const latestUserFromInput = conversationMessages.filter((m) => m.role === 'user').pop()
|
||||
if (latestUserFromInput) {
|
||||
const userMessageInThisRun = memoryMessages.some(
|
||||
(m) => m.role === 'user' && m.executionId === ctx.executionId
|
||||
)
|
||||
if (!userMessageInThisRun) {
|
||||
const taggedMessage = { ...latestUserFromInput, executionId: ctx.executionId }
|
||||
messages.push(taggedMessage)
|
||||
await memoryService.appendToMemory(ctx, inputs, taggedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process legacy memories (backward compatibility - from Memory block)
|
||||
// 3. Process legacy memories (backward compatibility - from Memory block)
|
||||
// These may include system messages which are preserved in their position
|
||||
if (inputs.memories) {
|
||||
messages.push(...this.processMemories(inputs.memories))
|
||||
}
|
||||
|
||||
// 3. Add messages array (new approach - from messages-input subblock)
|
||||
if (inputs.messages && Array.isArray(inputs.messages)) {
|
||||
const validMessages = inputs.messages.filter(
|
||||
(msg) =>
|
||||
msg &&
|
||||
typeof msg === 'object' &&
|
||||
'role' in msg &&
|
||||
'content' in msg &&
|
||||
['system', 'user', 'assistant'].includes(msg.role)
|
||||
)
|
||||
messages.push(...validMessages)
|
||||
// 4. Add conversation messages from inputs.messages (if not using native memory)
|
||||
// When memory is enabled, these are already seeded/fetched above
|
||||
if (!memoryEnabled && conversationMessages.length > 0) {
|
||||
messages.push(...conversationMessages)
|
||||
}
|
||||
|
||||
// Warn if using both new and legacy input formats
|
||||
if (
|
||||
inputs.messages &&
|
||||
inputs.messages.length > 0 &&
|
||||
(inputs.systemPrompt || inputs.userPrompt)
|
||||
) {
|
||||
logger.warn('Agent block using both messages array and legacy prompts', {
|
||||
hasMessages: true,
|
||||
hasSystemPrompt: !!inputs.systemPrompt,
|
||||
hasUserPrompt: !!inputs.userPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Handle legacy systemPrompt (backward compatibility)
|
||||
// Only add if no system message exists yet
|
||||
if (inputs.systemPrompt && !messages.some((m) => m.role === 'system')) {
|
||||
this.addSystemPrompt(messages, inputs.systemPrompt)
|
||||
}
|
||||
|
||||
// 5. Handle legacy userPrompt (backward compatibility)
|
||||
if (inputs.userPrompt) {
|
||||
this.addUserPrompt(messages, inputs.userPrompt)
|
||||
}
|
||||
|
||||
// 6. Persist user message(s) to memory if configured
|
||||
// This ensures conversation history is complete before agent execution
|
||||
if (inputs.memoryType && inputs.memoryType !== 'none' && messages.length > 0) {
|
||||
// Find new user messages that need to be persisted
|
||||
// (messages added via messages array or userPrompt)
|
||||
const userMessages = messages.filter((m) => m.role === 'user')
|
||||
const lastUserMessage = userMessages[userMessages.length - 1]
|
||||
|
||||
// Only persist if there's a user message AND it's from userPrompt or messages input
|
||||
// (not from memory history which was already persisted)
|
||||
if (
|
||||
lastUserMessage &&
|
||||
(inputs.userPrompt || (inputs.messages && inputs.messages.length > 0))
|
||||
) {
|
||||
await memoryService.persistUserMessage(ctx, inputs, lastUserMessage, blockId)
|
||||
// 5. Handle legacy systemPrompt (backward compatibility)
|
||||
// Only add if no system message exists from any source
|
||||
if (inputs.systemPrompt) {
|
||||
const hasSystem = systemMessages.length > 0 || messages.some((m) => m.role === 'system')
|
||||
if (!hasSystem) {
|
||||
this.addSystemPrompt(messages, inputs.systemPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
// Return messages or undefined if empty (maintains API compatibility)
|
||||
// 6. Handle legacy userPrompt - this is NEW input each run
|
||||
if (inputs.userPrompt) {
|
||||
this.addUserPrompt(messages, inputs.userPrompt)
|
||||
|
||||
if (memoryEnabled) {
|
||||
const userMessages = messages.filter((m) => m.role === 'user')
|
||||
const lastUserMessage = userMessages[userMessages.length - 1]
|
||||
if (lastUserMessage) {
|
||||
await memoryService.appendToMemory(ctx, inputs, lastUserMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Prefix system messages from inputs.messages at the start (runtime only)
|
||||
// These are the agent's configured system prompts
|
||||
if (systemMessages.length > 0) {
|
||||
messages.unshift(...systemMessages)
|
||||
}
|
||||
|
||||
return messages.length > 0 ? messages : undefined
|
||||
}
|
||||
|
||||
private extractValidMessages(messages?: Message[]): Message[] {
|
||||
if (!messages || !Array.isArray(messages)) return []
|
||||
|
||||
return messages.filter(
|
||||
(msg): msg is Message =>
|
||||
msg &&
|
||||
typeof msg === 'object' &&
|
||||
'role' in msg &&
|
||||
'content' in msg &&
|
||||
['system', 'user', 'assistant'].includes(msg.role)
|
||||
)
|
||||
}
|
||||
|
||||
private processMemories(memories: any): Message[] {
|
||||
if (!memories) return []
|
||||
|
||||
@@ -885,6 +920,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
azureApiVersion: inputs.azureApiVersion,
|
||||
vertexProject: inputs.vertexProject,
|
||||
vertexLocation: inputs.vertexLocation,
|
||||
vertexCredential: inputs.vertexCredential,
|
||||
responseFormat,
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
@@ -963,7 +999,17 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
responseFormat: any,
|
||||
providerStartTime: number
|
||||
) {
|
||||
const finalApiKey = this.getApiKey(providerId, model, providerRequest.apiKey)
|
||||
let finalApiKey: string
|
||||
|
||||
// For Vertex AI, resolve OAuth credential to access token
|
||||
if (providerId === 'vertex' && providerRequest.vertexCredential) {
|
||||
finalApiKey = await this.resolveVertexCredential(
|
||||
providerRequest.vertexCredential,
|
||||
ctx.workflowId
|
||||
)
|
||||
} else {
|
||||
finalApiKey = this.getApiKey(providerId, model, providerRequest.apiKey)
|
||||
}
|
||||
|
||||
const { blockData, blockNameMapping } = collectBlockData(ctx)
|
||||
|
||||
@@ -990,7 +1036,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
blockNameMapping,
|
||||
})
|
||||
|
||||
this.logExecutionSuccess(providerId, model, ctx, block, providerStartTime, response)
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
}
|
||||
|
||||
@@ -1015,15 +1060,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
this.logExecutionSuccess(
|
||||
providerRequest.provider,
|
||||
providerRequest.model,
|
||||
ctx,
|
||||
block,
|
||||
providerStartTime,
|
||||
'HTTP response'
|
||||
)
|
||||
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
if (contentType?.includes(HTTP.CONTENT_TYPE.EVENT_STREAM)) {
|
||||
return this.handleStreamingResponse(response, block, ctx, inputs)
|
||||
@@ -1036,29 +1072,14 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
private async handleStreamingResponse(
|
||||
response: Response,
|
||||
block: SerializedBlock,
|
||||
ctx?: ExecutionContext,
|
||||
inputs?: AgentInputs
|
||||
_ctx?: ExecutionContext,
|
||||
_inputs?: AgentInputs
|
||||
): Promise<StreamingExecution> {
|
||||
const executionDataHeader = response.headers.get('X-Execution-Data')
|
||||
|
||||
if (executionDataHeader) {
|
||||
try {
|
||||
const executionData = JSON.parse(executionDataHeader)
|
||||
|
||||
// If execution data contains full content, persist to memory
|
||||
if (ctx && inputs && executionData.output?.content) {
|
||||
const assistantMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: executionData.output.content,
|
||||
}
|
||||
// Fire and forget - don't await
|
||||
memoryService
|
||||
.persistMemoryMessage(ctx, inputs, assistantMessage, block.id)
|
||||
.catch((error) =>
|
||||
logger.error('Failed to persist streaming response to memory:', error)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
stream: response.body!,
|
||||
execution: {
|
||||
@@ -1098,21 +1119,33 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private logExecutionSuccess(
|
||||
provider: string,
|
||||
model: string,
|
||||
ctx: ExecutionContext,
|
||||
block: SerializedBlock,
|
||||
startTime: number,
|
||||
response: any
|
||||
) {
|
||||
const executionTime = Date.now() - startTime
|
||||
const responseType =
|
||||
response instanceof ReadableStream
|
||||
? 'stream'
|
||||
: response && typeof response === 'object' && 'stream' in response
|
||||
? 'streaming-execution'
|
||||
: 'json'
|
||||
/**
|
||||
* Resolves a Vertex AI OAuth credential to an access token
|
||||
*/
|
||||
private async resolveVertexCredential(credentialId: string, workflowId: string): Promise<string> {
|
||||
const requestId = `vertex-${Date.now()}`
|
||||
|
||||
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)
|
||||
|
||||
// Get the credential - we need to find the owner
|
||||
// Since we're in a workflow context, we can query the credential directly
|
||||
const credential = await db.query.account.findFirst({
|
||||
where: eq(account.id, credentialId),
|
||||
})
|
||||
|
||||
if (!credential) {
|
||||
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
||||
}
|
||||
|
||||
// Refresh the token if needed
|
||||
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('Failed to get Vertex AI access token')
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully resolved Vertex AI credential`)
|
||||
return accessToken
|
||||
}
|
||||
|
||||
private handleExecutionError(
|
||||
@@ -1158,46 +1191,35 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private wrapStreamForMemoryPersistence(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
streamingExec: StreamingExecution
|
||||
): StreamingExecution {
|
||||
return {
|
||||
stream: memoryService.wrapStreamForPersistence(streamingExec.stream, ctx, inputs),
|
||||
execution: streamingExec.execution,
|
||||
}
|
||||
}
|
||||
|
||||
private async persistResponseToMemory(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
result: BlockOutput | StreamingExecution,
|
||||
blockId: string
|
||||
result: BlockOutput
|
||||
): Promise<void> {
|
||||
// Only persist if memoryType is configured
|
||||
if (!inputs.memoryType || inputs.memoryType === 'none') {
|
||||
const content = (result as any)?.content
|
||||
if (!content || typeof content !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Don't persist streaming responses here - they're handled separately
|
||||
if (this.isStreamingExecution(result)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract content from regular response
|
||||
const blockOutput = result as any
|
||||
const content = blockOutput?.content
|
||||
|
||||
if (!content || typeof content !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const assistantMessage: Message = {
|
||||
role: 'assistant',
|
||||
content,
|
||||
}
|
||||
|
||||
await memoryService.persistMemoryMessage(ctx, inputs, assistantMessage, blockId)
|
||||
|
||||
await memoryService.appendToMemory(ctx, inputs, { role: 'assistant', content })
|
||||
logger.debug('Persisted assistant response to memory', {
|
||||
workflowId: ctx.workflowId,
|
||||
memoryType: inputs.memoryType,
|
||||
conversationId: inputs.conversationId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist response to memory:', error)
|
||||
// Don't throw - memory persistence failure shouldn't break workflow execution
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { MEMORY } from '@/executor/constants'
|
||||
import { Memory } from '@/executor/handlers/agent/memory'
|
||||
import type { AgentInputs, Message } from '@/executor/handlers/agent/types'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { Message } from '@/executor/handlers/agent/types'
|
||||
|
||||
vi.mock('@/lib/logs/console/logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -20,21 +20,14 @@ vi.mock('@/lib/tokenization/estimators', () => ({
|
||||
|
||||
describe('Memory', () => {
|
||||
let memoryService: Memory
|
||||
let mockContext: ExecutionContext
|
||||
|
||||
beforeEach(() => {
|
||||
memoryService = new Memory()
|
||||
mockContext = {
|
||||
workflowId: 'test-workflow-id',
|
||||
executionId: 'test-execution-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
} as ExecutionContext
|
||||
})
|
||||
|
||||
describe('applySlidingWindow (message-based)', () => {
|
||||
it('should keep last N conversation messages', () => {
|
||||
describe('applyWindow (message-based)', () => {
|
||||
it('should keep last N messages', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'System prompt' },
|
||||
{ role: 'user', content: 'Message 1' },
|
||||
{ role: 'assistant', content: 'Response 1' },
|
||||
{ role: 'user', content: 'Message 2' },
|
||||
@@ -43,55 +36,51 @@ describe('Memory', () => {
|
||||
{ role: 'assistant', content: 'Response 3' },
|
||||
]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindow(messages, '4')
|
||||
const result = (memoryService as any).applyWindow(messages, 4)
|
||||
|
||||
expect(result.length).toBe(5)
|
||||
expect(result[0].role).toBe('system')
|
||||
expect(result[0].content).toBe('System prompt')
|
||||
expect(result[1].content).toBe('Message 2')
|
||||
expect(result[4].content).toBe('Response 3')
|
||||
expect(result.length).toBe(4)
|
||||
expect(result[0].content).toBe('Message 2')
|
||||
expect(result[3].content).toBe('Response 3')
|
||||
})
|
||||
|
||||
it('should preserve only first system message', () => {
|
||||
it('should return all messages if limit exceeds array length', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'First system' },
|
||||
{ role: 'user', content: 'User message' },
|
||||
{ role: 'system', content: 'Second system' },
|
||||
{ role: 'assistant', content: 'Assistant message' },
|
||||
{ role: 'user', content: 'Test' },
|
||||
{ role: 'assistant', content: 'Response' },
|
||||
]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindow(messages, '10')
|
||||
|
||||
const systemMessages = result.filter((m: Message) => m.role === 'system')
|
||||
expect(systemMessages.length).toBe(1)
|
||||
expect(systemMessages[0].content).toBe('First system')
|
||||
const result = (memoryService as any).applyWindow(messages, 10)
|
||||
expect(result.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle invalid window size', () => {
|
||||
const messages: Message[] = [{ role: 'user', content: 'Test' }]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindow(messages, 'invalid')
|
||||
const result = (memoryService as any).applyWindow(messages, Number.NaN)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it('should handle zero limit', () => {
|
||||
const messages: Message[] = [{ role: 'user', content: 'Test' }]
|
||||
|
||||
const result = (memoryService as any).applyWindow(messages, 0)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
})
|
||||
|
||||
describe('applySlidingWindowByTokens (token-based)', () => {
|
||||
describe('applyTokenWindow (token-based)', () => {
|
||||
it('should keep messages within token limit', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'This is a system message' }, // ~6 tokens
|
||||
{ role: 'user', content: 'Short' }, // ~2 tokens
|
||||
{ role: 'assistant', content: 'This is a longer response message' }, // ~8 tokens
|
||||
{ role: 'user', content: 'Another user message here' }, // ~6 tokens
|
||||
{ role: 'assistant', content: 'Final response' }, // ~3 tokens
|
||||
{ role: 'user', content: 'Short' },
|
||||
{ role: 'assistant', content: 'This is a longer response message' },
|
||||
{ role: 'user', content: 'Another user message here' },
|
||||
{ role: 'assistant', content: 'Final response' },
|
||||
]
|
||||
|
||||
// Set limit to ~15 tokens - should include last 2-3 messages
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(messages, '15', 'gpt-4o')
|
||||
const result = (memoryService as any).applyTokenWindow(messages, 15, 'gpt-4o')
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.length).toBeLessThan(messages.length)
|
||||
|
||||
// Should include newest messages
|
||||
expect(result[result.length - 1].content).toBe('Final response')
|
||||
})
|
||||
|
||||
@@ -104,30 +93,12 @@ describe('Memory', () => {
|
||||
},
|
||||
]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(messages, '5', 'gpt-4o')
|
||||
const result = (memoryService as any).applyTokenWindow(messages, 5, 'gpt-4o')
|
||||
|
||||
expect(result.length).toBe(1)
|
||||
expect(result[0].content).toBe(messages[0].content)
|
||||
})
|
||||
|
||||
it('should preserve first system message and exclude it from token count', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'A' }, // System message - always preserved
|
||||
{ role: 'user', content: 'B' }, // ~1 token
|
||||
{ role: 'assistant', content: 'C' }, // ~1 token
|
||||
{ role: 'user', content: 'D' }, // ~1 token
|
||||
]
|
||||
|
||||
// Limit to 2 tokens - should fit system message + last 2 conversation messages (D, C)
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(messages, '2', 'gpt-4o')
|
||||
|
||||
// Should have: system message + 2 conversation messages = 3 total
|
||||
expect(result.length).toBe(3)
|
||||
expect(result[0].role).toBe('system') // First system message preserved
|
||||
expect(result[1].content).toBe('C') // Second most recent conversation message
|
||||
expect(result[2].content).toBe('D') // Most recent conversation message
|
||||
})
|
||||
|
||||
it('should process messages from newest to oldest', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'user', content: 'Old message' },
|
||||
@@ -136,141 +107,101 @@ describe('Memory', () => {
|
||||
{ role: 'assistant', content: 'New response' },
|
||||
]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(messages, '10', 'gpt-4o')
|
||||
const result = (memoryService as any).applyTokenWindow(messages, 10, 'gpt-4o')
|
||||
|
||||
// Should prioritize newer messages
|
||||
expect(result[result.length - 1].content).toBe('New response')
|
||||
})
|
||||
|
||||
it('should handle invalid token limit', () => {
|
||||
const messages: Message[] = [{ role: 'user', content: 'Test' }]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(
|
||||
messages,
|
||||
'invalid',
|
||||
'gpt-4o'
|
||||
)
|
||||
expect(result).toEqual(messages) // Should return all messages
|
||||
const result = (memoryService as any).applyTokenWindow(messages, Number.NaN, 'gpt-4o')
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it('should handle zero or negative token limit', () => {
|
||||
const messages: Message[] = [{ role: 'user', content: 'Test' }]
|
||||
|
||||
const result1 = (memoryService as any).applySlidingWindowByTokens(messages, '0', 'gpt-4o')
|
||||
const result1 = (memoryService as any).applyTokenWindow(messages, 0, 'gpt-4o')
|
||||
expect(result1).toEqual(messages)
|
||||
|
||||
const result2 = (memoryService as any).applySlidingWindowByTokens(messages, '-5', 'gpt-4o')
|
||||
const result2 = (memoryService as any).applyTokenWindow(messages, -5, 'gpt-4o')
|
||||
expect(result2).toEqual(messages)
|
||||
})
|
||||
|
||||
it('should work with different model names', () => {
|
||||
it('should work without model specified', () => {
|
||||
const messages: Message[] = [{ role: 'user', content: 'Test message' }]
|
||||
|
||||
const result1 = (memoryService as any).applySlidingWindowByTokens(messages, '100', 'gpt-4o')
|
||||
expect(result1.length).toBe(1)
|
||||
|
||||
const result2 = (memoryService as any).applySlidingWindowByTokens(
|
||||
messages,
|
||||
'100',
|
||||
'claude-3-5-sonnet-20241022'
|
||||
)
|
||||
expect(result2.length).toBe(1)
|
||||
|
||||
const result3 = (memoryService as any).applySlidingWindowByTokens(messages, '100', undefined)
|
||||
expect(result3.length).toBe(1)
|
||||
const result = (memoryService as any).applyTokenWindow(messages, 100, undefined)
|
||||
expect(result.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should handle empty messages array', () => {
|
||||
const messages: Message[] = []
|
||||
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(messages, '100', 'gpt-4o')
|
||||
const result = (memoryService as any).applyTokenWindow(messages, 100, 'gpt-4o')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildMemoryKey', () => {
|
||||
it('should build correct key with conversationId:blockId format', () => {
|
||||
const inputs: AgentInputs = {
|
||||
memoryType: 'conversation',
|
||||
conversationId: 'emir',
|
||||
}
|
||||
|
||||
const key = (memoryService as any).buildMemoryKey(mockContext, inputs, 'test-block-id')
|
||||
expect(key).toBe('emir:test-block-id')
|
||||
})
|
||||
|
||||
it('should use same key format regardless of memory type', () => {
|
||||
const conversationId = 'user-123'
|
||||
const blockId = 'block-abc'
|
||||
|
||||
const conversationKey = (memoryService as any).buildMemoryKey(
|
||||
mockContext,
|
||||
{ memoryType: 'conversation', conversationId },
|
||||
blockId
|
||||
)
|
||||
const slidingWindowKey = (memoryService as any).buildMemoryKey(
|
||||
mockContext,
|
||||
{ memoryType: 'sliding_window', conversationId },
|
||||
blockId
|
||||
)
|
||||
const slidingTokensKey = (memoryService as any).buildMemoryKey(
|
||||
mockContext,
|
||||
{ memoryType: 'sliding_window_tokens', conversationId },
|
||||
blockId
|
||||
)
|
||||
|
||||
// All should produce the same key - memory type only affects processing
|
||||
expect(conversationKey).toBe('user-123:block-abc')
|
||||
expect(slidingWindowKey).toBe('user-123:block-abc')
|
||||
expect(slidingTokensKey).toBe('user-123:block-abc')
|
||||
})
|
||||
|
||||
describe('validateConversationId', () => {
|
||||
it('should throw error for missing conversationId', () => {
|
||||
const inputs: AgentInputs = {
|
||||
memoryType: 'conversation',
|
||||
// conversationId missing
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
;(memoryService as any).buildMemoryKey(mockContext, inputs, 'test-block-id')
|
||||
}).toThrow('Conversation ID is required for all memory types')
|
||||
;(memoryService as any).validateConversationId(undefined)
|
||||
}).toThrow('Conversation ID is required')
|
||||
})
|
||||
|
||||
it('should throw error for empty conversationId', () => {
|
||||
const inputs: AgentInputs = {
|
||||
memoryType: 'conversation',
|
||||
conversationId: ' ', // Only whitespace
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
;(memoryService as any).buildMemoryKey(mockContext, inputs, 'test-block-id')
|
||||
}).toThrow('Conversation ID is required for all memory types')
|
||||
;(memoryService as any).validateConversationId(' ')
|
||||
}).toThrow('Conversation ID is required')
|
||||
})
|
||||
|
||||
it('should throw error for too long conversationId', () => {
|
||||
const longId = 'a'.repeat(MEMORY.MAX_CONVERSATION_ID_LENGTH + 1)
|
||||
expect(() => {
|
||||
;(memoryService as any).validateConversationId(longId)
|
||||
}).toThrow('Conversation ID too long')
|
||||
})
|
||||
|
||||
it('should accept valid conversationId', () => {
|
||||
expect(() => {
|
||||
;(memoryService as any).validateConversationId('user-123')
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateContent', () => {
|
||||
it('should throw error for content exceeding max size', () => {
|
||||
const largeContent = 'x'.repeat(MEMORY.MAX_MESSAGE_CONTENT_BYTES + 1)
|
||||
expect(() => {
|
||||
;(memoryService as any).validateContent(largeContent)
|
||||
}).toThrow('Message content too large')
|
||||
})
|
||||
|
||||
it('should accept content within limit', () => {
|
||||
const content = 'Normal sized content'
|
||||
expect(() => {
|
||||
;(memoryService as any).validateContent(content)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Token-based vs Message-based comparison', () => {
|
||||
it('should produce different results for same message count limit', () => {
|
||||
it('should produce different results for same limit concept', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'user', content: 'A' }, // Short message (~1 token)
|
||||
{ role: 'user', content: 'A' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'This is a much longer response that takes many more tokens',
|
||||
}, // Long message (~15 tokens)
|
||||
{ role: 'user', content: 'B' }, // Short message (~1 token)
|
||||
},
|
||||
{ role: 'user', content: 'B' },
|
||||
]
|
||||
|
||||
// Message-based: last 2 messages
|
||||
const messageResult = (memoryService as any).applySlidingWindow(messages, '2')
|
||||
const messageResult = (memoryService as any).applyWindow(messages, 2)
|
||||
expect(messageResult.length).toBe(2)
|
||||
|
||||
// Token-based: with limit of 10 tokens, might fit all 3 messages or just last 2
|
||||
const tokenResult = (memoryService as any).applySlidingWindowByTokens(
|
||||
messages,
|
||||
'10',
|
||||
'gpt-4o'
|
||||
)
|
||||
|
||||
// The long message should affect what fits
|
||||
const tokenResult = (memoryService as any).applyTokenWindow(messages, 10, 'gpt-4o')
|
||||
expect(tokenResult.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,661 +1,281 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { memory } from '@sim/db/schema'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getAccurateTokenCount } from '@/lib/tokenization/estimators'
|
||||
import { MEMORY } from '@/executor/constants'
|
||||
import type { AgentInputs, Message } from '@/executor/handlers/agent/types'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http'
|
||||
import { stringifyJSON } from '@/executor/utils/json'
|
||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||
|
||||
const logger = createLogger('Memory')
|
||||
|
||||
/**
|
||||
* Class for managing agent conversation memory
|
||||
* Handles fetching and persisting messages to the memory table
|
||||
*/
|
||||
export class Memory {
|
||||
/**
|
||||
* Fetch messages from memory based on memoryType configuration
|
||||
*/
|
||||
async fetchMemoryMessages(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
blockId: string
|
||||
): Promise<Message[]> {
|
||||
async fetchMemoryMessages(ctx: ExecutionContext, inputs: AgentInputs): Promise<Message[]> {
|
||||
if (!inputs.memoryType || inputs.memoryType === 'none') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!ctx.workflowId) {
|
||||
logger.warn('Cannot fetch memory without workflowId')
|
||||
return []
|
||||
}
|
||||
const workspaceId = this.requireWorkspaceId(ctx)
|
||||
this.validateConversationId(inputs.conversationId)
|
||||
|
||||
try {
|
||||
this.validateInputs(inputs.conversationId)
|
||||
const messages = await this.fetchMemory(workspaceId, inputs.conversationId!)
|
||||
|
||||
const memoryKey = this.buildMemoryKey(ctx, inputs, blockId)
|
||||
let messages = await this.fetchFromMemoryAPI(ctx.workflowId, memoryKey)
|
||||
switch (inputs.memoryType) {
|
||||
case 'conversation':
|
||||
return this.applyContextWindowLimit(messages, inputs.model)
|
||||
|
||||
switch (inputs.memoryType) {
|
||||
case 'conversation':
|
||||
messages = this.applyContextWindowLimit(messages, inputs.model)
|
||||
break
|
||||
|
||||
case 'sliding_window': {
|
||||
// Default to 10 messages if not specified (matches agent block default)
|
||||
const windowSize = inputs.slidingWindowSize || '10'
|
||||
messages = this.applySlidingWindow(messages, windowSize)
|
||||
break
|
||||
}
|
||||
|
||||
case 'sliding_window_tokens': {
|
||||
// Default to 4000 tokens if not specified (matches agent block default)
|
||||
const maxTokens = inputs.slidingWindowTokens || '4000'
|
||||
messages = this.applySlidingWindowByTokens(messages, maxTokens, inputs.model)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch memory messages:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist assistant response to memory
|
||||
* Uses atomic append operations to prevent race conditions
|
||||
*/
|
||||
async persistMemoryMessage(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
assistantMessage: Message,
|
||||
blockId: string
|
||||
): Promise<void> {
|
||||
if (!inputs.memoryType || inputs.memoryType === 'none') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.workflowId) {
|
||||
logger.warn('Cannot persist memory without workflowId')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.validateInputs(inputs.conversationId, assistantMessage.content)
|
||||
|
||||
const memoryKey = this.buildMemoryKey(ctx, inputs, blockId)
|
||||
|
||||
if (inputs.memoryType === 'sliding_window') {
|
||||
// Default to 10 messages if not specified (matches agent block default)
|
||||
const windowSize = inputs.slidingWindowSize || '10'
|
||||
|
||||
const existingMessages = await this.fetchFromMemoryAPI(ctx.workflowId, memoryKey)
|
||||
const updatedMessages = [...existingMessages, assistantMessage]
|
||||
const messagesToPersist = this.applySlidingWindow(updatedMessages, windowSize)
|
||||
|
||||
await this.persistToMemoryAPI(ctx.workflowId, memoryKey, messagesToPersist)
|
||||
} else if (inputs.memoryType === 'sliding_window_tokens') {
|
||||
// Default to 4000 tokens if not specified (matches agent block default)
|
||||
const maxTokens = inputs.slidingWindowTokens || '4000'
|
||||
|
||||
const existingMessages = await this.fetchFromMemoryAPI(ctx.workflowId, memoryKey)
|
||||
const updatedMessages = [...existingMessages, assistantMessage]
|
||||
const messagesToPersist = this.applySlidingWindowByTokens(
|
||||
updatedMessages,
|
||||
maxTokens,
|
||||
inputs.model
|
||||
case 'sliding_window': {
|
||||
const limit = this.parsePositiveInt(
|
||||
inputs.slidingWindowSize,
|
||||
MEMORY.DEFAULT_SLIDING_WINDOW_SIZE
|
||||
)
|
||||
|
||||
await this.persistToMemoryAPI(ctx.workflowId, memoryKey, messagesToPersist)
|
||||
} else {
|
||||
// Conversation mode: use atomic append for better concurrency
|
||||
await this.atomicAppendToMemory(ctx.workflowId, memoryKey, assistantMessage)
|
||||
return this.applyWindow(messages, limit)
|
||||
}
|
||||
|
||||
logger.debug('Successfully persisted memory message', {
|
||||
workflowId: ctx.workflowId,
|
||||
key: memoryKey,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist memory message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist user message to memory before agent execution
|
||||
*/
|
||||
async persistUserMessage(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
userMessage: Message,
|
||||
blockId: string
|
||||
): Promise<void> {
|
||||
if (!inputs.memoryType || inputs.memoryType === 'none') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.workflowId) {
|
||||
logger.warn('Cannot persist user message without workflowId')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const memoryKey = this.buildMemoryKey(ctx, inputs, blockId)
|
||||
|
||||
if (inputs.slidingWindowSize && inputs.memoryType === 'sliding_window') {
|
||||
const existingMessages = await this.fetchFromMemoryAPI(ctx.workflowId, memoryKey)
|
||||
const updatedMessages = [...existingMessages, userMessage]
|
||||
const messagesToPersist = this.applySlidingWindow(updatedMessages, inputs.slidingWindowSize)
|
||||
await this.persistToMemoryAPI(ctx.workflowId, memoryKey, messagesToPersist)
|
||||
} else if (inputs.slidingWindowTokens && inputs.memoryType === 'sliding_window_tokens') {
|
||||
const existingMessages = await this.fetchFromMemoryAPI(ctx.workflowId, memoryKey)
|
||||
const updatedMessages = [...existingMessages, userMessage]
|
||||
const messagesToPersist = this.applySlidingWindowByTokens(
|
||||
updatedMessages,
|
||||
case 'sliding_window_tokens': {
|
||||
const maxTokens = this.parsePositiveInt(
|
||||
inputs.slidingWindowTokens,
|
||||
inputs.model
|
||||
MEMORY.DEFAULT_SLIDING_WINDOW_TOKENS
|
||||
)
|
||||
await this.persistToMemoryAPI(ctx.workflowId, memoryKey, messagesToPersist)
|
||||
} else {
|
||||
await this.atomicAppendToMemory(ctx.workflowId, memoryKey, userMessage)
|
||||
return this.applyTokenWindow(messages, maxTokens, inputs.model)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist user message:', error)
|
||||
|
||||
default:
|
||||
return messages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build memory key based on conversationId and blockId
|
||||
* BlockId provides block-level memory isolation
|
||||
*/
|
||||
private buildMemoryKey(_ctx: ExecutionContext, inputs: AgentInputs, blockId: string): string {
|
||||
const { conversationId } = inputs
|
||||
async appendToMemory(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs,
|
||||
message: Message
|
||||
): Promise<void> {
|
||||
if (!inputs.memoryType || inputs.memoryType === 'none') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!conversationId || conversationId.trim() === '') {
|
||||
throw new Error(
|
||||
'Conversation ID is required for all memory types. ' +
|
||||
'Please provide a unique identifier (e.g., user-123, session-abc, customer-456).'
|
||||
const workspaceId = this.requireWorkspaceId(ctx)
|
||||
this.validateConversationId(inputs.conversationId)
|
||||
this.validateContent(message.content)
|
||||
|
||||
const key = inputs.conversationId!
|
||||
|
||||
await this.appendMessage(workspaceId, key, message)
|
||||
|
||||
logger.debug('Appended message to memory', {
|
||||
workspaceId,
|
||||
key,
|
||||
role: message.role,
|
||||
})
|
||||
}
|
||||
|
||||
async seedMemory(ctx: ExecutionContext, inputs: AgentInputs, messages: Message[]): Promise<void> {
|
||||
if (!inputs.memoryType || inputs.memoryType === 'none') {
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceId = this.requireWorkspaceId(ctx)
|
||||
|
||||
const conversationMessages = messages.filter((m) => m.role !== 'system')
|
||||
if (conversationMessages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.validateConversationId(inputs.conversationId)
|
||||
|
||||
const key = inputs.conversationId!
|
||||
|
||||
let messagesToStore = conversationMessages
|
||||
if (inputs.memoryType === 'sliding_window') {
|
||||
const limit = this.parsePositiveInt(
|
||||
inputs.slidingWindowSize,
|
||||
MEMORY.DEFAULT_SLIDING_WINDOW_SIZE
|
||||
)
|
||||
messagesToStore = this.applyWindow(conversationMessages, limit)
|
||||
} else if (inputs.memoryType === 'sliding_window_tokens') {
|
||||
const maxTokens = this.parsePositiveInt(
|
||||
inputs.slidingWindowTokens,
|
||||
MEMORY.DEFAULT_SLIDING_WINDOW_TOKENS
|
||||
)
|
||||
messagesToStore = this.applyTokenWindow(conversationMessages, maxTokens, inputs.model)
|
||||
}
|
||||
|
||||
return `${conversationId}:${blockId}`
|
||||
await this.seedMemoryRecord(workspaceId, key, messagesToStore)
|
||||
|
||||
logger.debug('Seeded memory', {
|
||||
workspaceId,
|
||||
key,
|
||||
count: messagesToStore.length,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sliding window to limit number of conversation messages
|
||||
*
|
||||
* System message handling:
|
||||
* - System messages are excluded from the sliding window count
|
||||
* - Only the first system message is preserved and placed at the start
|
||||
* - This ensures system prompts remain available while limiting conversation history
|
||||
*/
|
||||
private applySlidingWindow(messages: Message[], windowSize: string): Message[] {
|
||||
const limit = Number.parseInt(windowSize, 10)
|
||||
wrapStreamForPersistence(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs
|
||||
): ReadableStream<Uint8Array> {
|
||||
let accumulatedContent = ''
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
if (Number.isNaN(limit) || limit <= 0) {
|
||||
logger.warn('Invalid sliding window size, returning all messages', { windowSize })
|
||||
return messages
|
||||
}
|
||||
const transformStream = new TransformStream<Uint8Array, Uint8Array>({
|
||||
transform: (chunk, controller) => {
|
||||
controller.enqueue(chunk)
|
||||
const decoded = decoder.decode(chunk, { stream: true })
|
||||
accumulatedContent += decoded
|
||||
},
|
||||
|
||||
const systemMessages = messages.filter((msg) => msg.role === 'system')
|
||||
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
|
||||
|
||||
const recentMessages = conversationMessages.slice(-limit)
|
||||
|
||||
const firstSystemMessage = systemMessages.length > 0 ? [systemMessages[0]] : []
|
||||
|
||||
return [...firstSystemMessage, ...recentMessages]
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply token-based sliding window to limit conversation by token count
|
||||
*
|
||||
* System message handling:
|
||||
* - For consistency with message-based sliding window, the first system message is preserved
|
||||
* - System messages are excluded from the token count
|
||||
* - This ensures system prompts are always available while limiting conversation history
|
||||
*/
|
||||
private applySlidingWindowByTokens(
|
||||
messages: Message[],
|
||||
maxTokens: string,
|
||||
model?: string
|
||||
): Message[] {
|
||||
const tokenLimit = Number.parseInt(maxTokens, 10)
|
||||
|
||||
if (Number.isNaN(tokenLimit) || tokenLimit <= 0) {
|
||||
logger.warn('Invalid token limit, returning all messages', { maxTokens })
|
||||
return messages
|
||||
}
|
||||
|
||||
// Separate system messages from conversation messages for consistent handling
|
||||
const systemMessages = messages.filter((msg) => msg.role === 'system')
|
||||
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
|
||||
|
||||
const result: Message[] = []
|
||||
let currentTokenCount = 0
|
||||
|
||||
// Add conversation messages from most recent backwards
|
||||
for (let i = conversationMessages.length - 1; i >= 0; i--) {
|
||||
const message = conversationMessages[i]
|
||||
const messageTokens = getAccurateTokenCount(message.content, model)
|
||||
|
||||
if (currentTokenCount + messageTokens <= tokenLimit) {
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
} else if (result.length === 0) {
|
||||
logger.warn('Single message exceeds token limit, including anyway', {
|
||||
messageTokens,
|
||||
tokenLimit,
|
||||
messageRole: message.role,
|
||||
})
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
break
|
||||
} else {
|
||||
// Token limit reached, stop processing
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Applied token-based sliding window', {
|
||||
totalMessages: messages.length,
|
||||
conversationMessages: conversationMessages.length,
|
||||
includedMessages: result.length,
|
||||
totalTokens: currentTokenCount,
|
||||
tokenLimit,
|
||||
flush: () => {
|
||||
if (accumulatedContent.trim()) {
|
||||
this.appendToMemory(ctx, inputs, {
|
||||
role: 'assistant',
|
||||
content: accumulatedContent,
|
||||
}).catch((error) => logger.error('Failed to persist streaming response:', error))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Preserve first system message and prepend to results (consistent with message-based window)
|
||||
const firstSystemMessage = systemMessages.length > 0 ? [systemMessages[0]] : []
|
||||
return [...firstSystemMessage, ...result]
|
||||
return stream.pipeThrough(transformStream)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply context window limit based on model's maximum context window
|
||||
* Auto-trims oldest conversation messages when approaching the model's context limit
|
||||
* Uses 90% of context window (10% buffer for response)
|
||||
* Only applies if model has contextWindow defined and contextInformationAvailable !== false
|
||||
*/
|
||||
private applyContextWindowLimit(messages: Message[], model?: string): Message[] {
|
||||
if (!model) {
|
||||
return messages
|
||||
private requireWorkspaceId(ctx: ExecutionContext): string {
|
||||
if (!ctx.workspaceId) {
|
||||
throw new Error('workspaceId is required for memory operations')
|
||||
}
|
||||
return ctx.workspaceId
|
||||
}
|
||||
|
||||
private applyWindow(messages: Message[], limit: number): Message[] {
|
||||
return messages.slice(-limit)
|
||||
}
|
||||
|
||||
private applyTokenWindow(messages: Message[], maxTokens: number, model?: string): Message[] {
|
||||
const result: Message[] = []
|
||||
let tokenCount = 0
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i]
|
||||
const msgTokens = getAccurateTokenCount(msg.content, model)
|
||||
|
||||
if (tokenCount + msgTokens <= maxTokens) {
|
||||
result.unshift(msg)
|
||||
tokenCount += msgTokens
|
||||
} else if (result.length === 0) {
|
||||
result.unshift(msg)
|
||||
break
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let contextWindow: number | undefined
|
||||
return result
|
||||
}
|
||||
|
||||
private applyContextWindowLimit(messages: Message[], model?: string): Message[] {
|
||||
if (!model) return messages
|
||||
|
||||
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
|
||||
if (provider.contextInformationAvailable === false) {
|
||||
continue
|
||||
}
|
||||
if (provider.contextInformationAvailable === false) continue
|
||||
|
||||
const matchesPattern = provider.modelPatterns?.some((pattern) => pattern.test(model))
|
||||
const matchesPattern = provider.modelPatterns?.some((p) => p.test(model))
|
||||
const matchesModel = provider.models.some((m) => m.id === model)
|
||||
|
||||
if (matchesPattern || matchesModel) {
|
||||
const modelDef = provider.models.find((m) => m.id === model)
|
||||
if (modelDef?.contextWindow) {
|
||||
contextWindow = modelDef.contextWindow
|
||||
break
|
||||
const maxTokens = Math.floor(modelDef.contextWindow * MEMORY.CONTEXT_WINDOW_UTILIZATION)
|
||||
return this.applyTokenWindow(messages, maxTokens, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!contextWindow) {
|
||||
logger.debug('No context window information available for model, skipping auto-trim', {
|
||||
model,
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
const maxTokens = Math.floor(contextWindow * 0.9)
|
||||
|
||||
logger.debug('Applying context window limit', {
|
||||
model,
|
||||
contextWindow,
|
||||
maxTokens,
|
||||
totalMessages: messages.length,
|
||||
})
|
||||
|
||||
const systemMessages = messages.filter((msg) => msg.role === 'system')
|
||||
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
|
||||
|
||||
// Count tokens used by system messages first
|
||||
let systemTokenCount = 0
|
||||
for (const msg of systemMessages) {
|
||||
systemTokenCount += getAccurateTokenCount(msg.content, model)
|
||||
}
|
||||
|
||||
// Calculate remaining tokens available for conversation messages
|
||||
const remainingTokens = Math.max(0, maxTokens - systemTokenCount)
|
||||
|
||||
if (systemTokenCount >= maxTokens) {
|
||||
logger.warn('System messages exceed context window limit, including anyway', {
|
||||
systemTokenCount,
|
||||
maxTokens,
|
||||
systemMessageCount: systemMessages.length,
|
||||
})
|
||||
return systemMessages
|
||||
}
|
||||
|
||||
const result: Message[] = []
|
||||
let currentTokenCount = 0
|
||||
|
||||
for (let i = conversationMessages.length - 1; i >= 0; i--) {
|
||||
const message = conversationMessages[i]
|
||||
const messageTokens = getAccurateTokenCount(message.content, model)
|
||||
|
||||
if (currentTokenCount + messageTokens <= remainingTokens) {
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
} else if (result.length === 0) {
|
||||
logger.warn('Single message exceeds remaining context window, including anyway', {
|
||||
messageTokens,
|
||||
remainingTokens,
|
||||
systemTokenCount,
|
||||
messageRole: message.role,
|
||||
})
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
break
|
||||
} else {
|
||||
logger.info('Auto-trimmed conversation history to fit context window', {
|
||||
originalMessages: conversationMessages.length,
|
||||
trimmedMessages: result.length,
|
||||
conversationTokens: currentTokenCount,
|
||||
systemTokens: systemTokenCount,
|
||||
totalTokens: currentTokenCount + systemTokenCount,
|
||||
maxTokens,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [...systemMessages, ...result]
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch messages from memory API
|
||||
*/
|
||||
private async fetchFromMemoryAPI(workflowId: string, key: string): Promise<Message[]> {
|
||||
try {
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
private async fetchMemory(workspaceId: string, key: string): Promise<Message[]> {
|
||||
const result = await db
|
||||
.select({ data: memory.data })
|
||||
.from(memory)
|
||||
.where(and(eq(memory.workspaceId, workspaceId), eq(memory.key, key)))
|
||||
.limit(1)
|
||||
|
||||
if (!isBrowser) {
|
||||
return await this.fetchFromMemoryDirect(workflowId, key)
|
||||
}
|
||||
if (result.length === 0) return []
|
||||
|
||||
const headers = await buildAuthHeaders()
|
||||
const url = buildAPIUrl(`/api/memory/${encodeURIComponent(key)}`, { workflowId })
|
||||
const data = result[0].data
|
||||
if (!Array.isArray(data)) return []
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return []
|
||||
}
|
||||
throw new Error(`Failed to fetch memory: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch memory')
|
||||
}
|
||||
|
||||
const memoryData = result.data?.data || result.data
|
||||
if (Array.isArray(memoryData)) {
|
||||
return memoryData.filter(
|
||||
(msg) => msg && typeof msg === 'object' && 'role' in msg && 'content' in msg
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (error) {
|
||||
logger.error('Error fetching from memory API:', error)
|
||||
return []
|
||||
}
|
||||
return data.filter(
|
||||
(msg): msg is Message => msg && typeof msg === 'object' && 'role' in msg && 'content' in msg
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct database access
|
||||
*/
|
||||
private async fetchFromMemoryDirect(workflowId: string, key: string): Promise<Message[]> {
|
||||
try {
|
||||
const { db } = await import('@sim/db')
|
||||
const { memory } = await import('@sim/db/schema')
|
||||
const { and, eq } = await import('drizzle-orm')
|
||||
|
||||
const result = await db
|
||||
.select({
|
||||
data: memory.data,
|
||||
})
|
||||
.from(memory)
|
||||
.where(and(eq(memory.workflowId, workflowId), eq(memory.key, key)))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const memoryData = result[0].data as any
|
||||
if (Array.isArray(memoryData)) {
|
||||
return memoryData.filter(
|
||||
(msg) => msg && typeof msg === 'object' && 'role' in msg && 'content' in msg
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (error) {
|
||||
logger.error('Error fetching from memory database:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist messages to memory API
|
||||
*/
|
||||
private async persistToMemoryAPI(
|
||||
workflowId: string,
|
||||
private async seedMemoryRecord(
|
||||
workspaceId: string,
|
||||
key: string,
|
||||
messages: Message[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
const now = new Date()
|
||||
|
||||
if (!isBrowser) {
|
||||
await this.persistToMemoryDirect(workflowId, key, messages)
|
||||
return
|
||||
}
|
||||
|
||||
const headers = await buildAuthHeaders()
|
||||
const url = buildAPIUrl('/api/memory')
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: stringifyJSON({
|
||||
workflowId,
|
||||
key,
|
||||
data: messages,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to persist memory: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to persist memory')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error persisting to memory API:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically append a message to memory
|
||||
*/
|
||||
private async atomicAppendToMemory(
|
||||
workflowId: string,
|
||||
key: string,
|
||||
message: Message
|
||||
): Promise<void> {
|
||||
try {
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
if (!isBrowser) {
|
||||
await this.atomicAppendToMemoryDirect(workflowId, key, message)
|
||||
} else {
|
||||
const headers = await buildAuthHeaders()
|
||||
const url = buildAPIUrl('/api/memory')
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: stringifyJSON({
|
||||
workflowId,
|
||||
key,
|
||||
data: message,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to append memory: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to append memory')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error appending to memory:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct database atomic append for server-side
|
||||
* Uses PostgreSQL JSONB concatenation operator for atomic operations
|
||||
*/
|
||||
private async atomicAppendToMemoryDirect(
|
||||
workflowId: string,
|
||||
key: string,
|
||||
message: Message
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { db } = await import('@sim/db')
|
||||
const { memory } = await import('@sim/db/schema')
|
||||
const { sql } = await import('drizzle-orm')
|
||||
const { randomUUID } = await import('node:crypto')
|
||||
|
||||
const now = new Date()
|
||||
const id = randomUUID()
|
||||
|
||||
await db
|
||||
.insert(memory)
|
||||
.values({
|
||||
id,
|
||||
workflowId,
|
||||
key,
|
||||
data: [message],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [memory.workflowId, memory.key],
|
||||
set: {
|
||||
data: sql`${memory.data} || ${JSON.stringify([message])}::jsonb`,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
|
||||
logger.debug('Atomically appended message to memory', {
|
||||
workflowId,
|
||||
await db
|
||||
.insert(memory)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
workspaceId,
|
||||
key,
|
||||
data: messages,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error in atomic append to memory database:', error)
|
||||
throw error
|
||||
}
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct database access for server-side persistence
|
||||
* Uses UPSERT to handle race conditions atomically
|
||||
*/
|
||||
private async persistToMemoryDirect(
|
||||
workflowId: string,
|
||||
key: string,
|
||||
messages: Message[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { db } = await import('@sim/db')
|
||||
const { memory } = await import('@sim/db/schema')
|
||||
const { randomUUID } = await import('node:crypto')
|
||||
private async appendMessage(workspaceId: string, key: string, message: Message): Promise<void> {
|
||||
const now = new Date()
|
||||
|
||||
const now = new Date()
|
||||
const id = randomUUID()
|
||||
|
||||
await db
|
||||
.insert(memory)
|
||||
.values({
|
||||
id,
|
||||
workflowId,
|
||||
key,
|
||||
data: messages,
|
||||
createdAt: now,
|
||||
await db
|
||||
.insert(memory)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
workspaceId,
|
||||
key,
|
||||
data: [message],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [memory.workspaceId, memory.key],
|
||||
set: {
|
||||
data: sql`${memory.data} || ${JSON.stringify([message])}::jsonb`,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [memory.workflowId, memory.key],
|
||||
set: {
|
||||
data: messages,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error persisting to memory database:', error)
|
||||
throw error
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private parsePositiveInt(value: string | undefined, defaultValue: number): number {
|
||||
if (!value) return defaultValue
|
||||
const parsed = Number.parseInt(value, 10)
|
||||
if (Number.isNaN(parsed) || parsed <= 0) return defaultValue
|
||||
return parsed
|
||||
}
|
||||
|
||||
private validateConversationId(conversationId?: string): void {
|
||||
if (!conversationId || conversationId.trim() === '') {
|
||||
throw new Error('Conversation ID is required')
|
||||
}
|
||||
if (conversationId.length > MEMORY.MAX_CONVERSATION_ID_LENGTH) {
|
||||
throw new Error(
|
||||
`Conversation ID too long (max ${MEMORY.MAX_CONVERSATION_ID_LENGTH} characters)`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate inputs to prevent malicious data or performance issues
|
||||
*/
|
||||
private validateInputs(conversationId?: string, content?: string): void {
|
||||
if (conversationId) {
|
||||
if (conversationId.length > 255) {
|
||||
throw new Error('Conversation ID too long (max 255 characters)')
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_\-:.@]+$/.test(conversationId)) {
|
||||
logger.warn('Conversation ID contains special characters', { conversationId })
|
||||
}
|
||||
}
|
||||
|
||||
if (content) {
|
||||
const contentSize = Buffer.byteLength(content, 'utf8')
|
||||
const MAX_CONTENT_SIZE = 100 * 1024 // 100KB
|
||||
|
||||
if (contentSize > MAX_CONTENT_SIZE) {
|
||||
throw new Error(`Message content too large (${contentSize} bytes, max ${MAX_CONTENT_SIZE})`)
|
||||
}
|
||||
private validateContent(content: string): void {
|
||||
const size = Buffer.byteLength(content, 'utf8')
|
||||
if (size > MEMORY.MAX_MESSAGE_CONTENT_BYTES) {
|
||||
throw new Error(
|
||||
`Message content too large (${size} bytes, max ${MEMORY.MAX_MESSAGE_CONTENT_BYTES})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface AgentInputs {
|
||||
azureApiVersion?: string
|
||||
vertexProject?: string
|
||||
vertexLocation?: string
|
||||
vertexCredential?: string
|
||||
reasoningEffort?: string
|
||||
verbosity?: string
|
||||
}
|
||||
@@ -41,6 +42,7 @@ export interface ToolInput {
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant'
|
||||
content: string
|
||||
executionId?: string
|
||||
function_call?: any
|
||||
tool_calls?: any[]
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@ import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/constants'
|
||||
import type { DAG } from '@/executor/dag/builder'
|
||||
import type { EdgeManager } from '@/executor/execution/edge-manager'
|
||||
import type { LoopScope } from '@/executor/execution/state'
|
||||
import type { BlockStateController } from '@/executor/execution/types'
|
||||
import type { BlockStateController, ContextExtensions } from '@/executor/execution/types'
|
||||
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
|
||||
import type { LoopConfigWithNodes } from '@/executor/types/loop'
|
||||
import { replaceValidReferences } from '@/executor/utils/reference-validation'
|
||||
import {
|
||||
addSubflowErrorLog,
|
||||
buildSentinelEndId,
|
||||
buildSentinelStartId,
|
||||
extractBaseBlockId,
|
||||
resolveArrayInput,
|
||||
validateMaxCount,
|
||||
} from '@/executor/utils/subflow-utils'
|
||||
import type { VariableResolver } from '@/executor/variables/resolver'
|
||||
import type { SerializedLoop } from '@/serializer/types'
|
||||
@@ -32,6 +35,7 @@ export interface LoopContinuationResult {
|
||||
|
||||
export class LoopOrchestrator {
|
||||
private edgeManager: EdgeManager | null = null
|
||||
private contextExtensions: ContextExtensions | null = null
|
||||
|
||||
constructor(
|
||||
private dag: DAG,
|
||||
@@ -39,6 +43,10 @@ export class LoopOrchestrator {
|
||||
private resolver: VariableResolver
|
||||
) {}
|
||||
|
||||
setContextExtensions(contextExtensions: ContextExtensions): void {
|
||||
this.contextExtensions = contextExtensions
|
||||
}
|
||||
|
||||
setEdgeManager(edgeManager: EdgeManager): void {
|
||||
this.edgeManager = edgeManager
|
||||
}
|
||||
@@ -48,7 +56,6 @@ export class LoopOrchestrator {
|
||||
if (!loopConfig) {
|
||||
throw new Error(`Loop config not found: ${loopId}`)
|
||||
}
|
||||
|
||||
const scope: LoopScope = {
|
||||
iteration: 0,
|
||||
currentIterationOutputs: new Map(),
|
||||
@@ -58,15 +65,70 @@ export class LoopOrchestrator {
|
||||
const loopType = loopConfig.loopType
|
||||
|
||||
switch (loopType) {
|
||||
case 'for':
|
||||
case 'for': {
|
||||
scope.loopType = 'for'
|
||||
scope.maxIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
|
||||
const requestedIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
|
||||
|
||||
const iterationError = validateMaxCount(
|
||||
requestedIterations,
|
||||
DEFAULTS.MAX_LOOP_ITERATIONS,
|
||||
'For loop iterations'
|
||||
)
|
||||
if (iterationError) {
|
||||
logger.error(iterationError, { loopId, requestedIterations })
|
||||
this.addLoopErrorLog(ctx, loopId, loopType, iterationError, {
|
||||
iterations: requestedIterations,
|
||||
})
|
||||
scope.maxIterations = 0
|
||||
scope.validationError = iterationError
|
||||
scope.condition = buildLoopIndexCondition(0)
|
||||
ctx.loopExecutions?.set(loopId, scope)
|
||||
throw new Error(iterationError)
|
||||
}
|
||||
|
||||
scope.maxIterations = requestedIterations
|
||||
scope.condition = buildLoopIndexCondition(scope.maxIterations)
|
||||
break
|
||||
}
|
||||
|
||||
case 'forEach': {
|
||||
scope.loopType = 'forEach'
|
||||
const items = this.resolveForEachItems(ctx, loopConfig.forEachItems)
|
||||
let items: any[]
|
||||
try {
|
||||
items = this.resolveForEachItems(ctx, loopConfig.forEachItems)
|
||||
} catch (error) {
|
||||
const errorMessage = `ForEach loop resolution failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
logger.error(errorMessage, { loopId, forEachItems: loopConfig.forEachItems })
|
||||
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
|
||||
forEachItems: loopConfig.forEachItems,
|
||||
})
|
||||
scope.items = []
|
||||
scope.maxIterations = 0
|
||||
scope.validationError = errorMessage
|
||||
scope.condition = buildLoopIndexCondition(0)
|
||||
ctx.loopExecutions?.set(loopId, scope)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const sizeError = validateMaxCount(
|
||||
items.length,
|
||||
DEFAULTS.MAX_FOREACH_ITEMS,
|
||||
'ForEach loop collection size'
|
||||
)
|
||||
if (sizeError) {
|
||||
logger.error(sizeError, { loopId, collectionSize: items.length })
|
||||
this.addLoopErrorLog(ctx, loopId, loopType, sizeError, {
|
||||
forEachItems: loopConfig.forEachItems,
|
||||
collectionSize: items.length,
|
||||
})
|
||||
scope.items = []
|
||||
scope.maxIterations = 0
|
||||
scope.validationError = sizeError
|
||||
scope.condition = buildLoopIndexCondition(0)
|
||||
ctx.loopExecutions?.set(loopId, scope)
|
||||
throw new Error(sizeError)
|
||||
}
|
||||
|
||||
scope.items = items
|
||||
scope.maxIterations = items.length
|
||||
scope.item = items[0]
|
||||
@@ -79,15 +141,35 @@ export class LoopOrchestrator {
|
||||
scope.condition = loopConfig.whileCondition
|
||||
break
|
||||
|
||||
case 'doWhile':
|
||||
case 'doWhile': {
|
||||
scope.loopType = 'doWhile'
|
||||
if (loopConfig.doWhileCondition) {
|
||||
scope.condition = loopConfig.doWhileCondition
|
||||
} else {
|
||||
scope.maxIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
|
||||
const requestedIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
|
||||
|
||||
const iterationError = validateMaxCount(
|
||||
requestedIterations,
|
||||
DEFAULTS.MAX_LOOP_ITERATIONS,
|
||||
'Do-While loop iterations'
|
||||
)
|
||||
if (iterationError) {
|
||||
logger.error(iterationError, { loopId, requestedIterations })
|
||||
this.addLoopErrorLog(ctx, loopId, loopType, iterationError, {
|
||||
iterations: requestedIterations,
|
||||
})
|
||||
scope.maxIterations = 0
|
||||
scope.validationError = iterationError
|
||||
scope.condition = buildLoopIndexCondition(0)
|
||||
ctx.loopExecutions?.set(loopId, scope)
|
||||
throw new Error(iterationError)
|
||||
}
|
||||
|
||||
scope.maxIterations = requestedIterations
|
||||
scope.condition = buildLoopIndexCondition(scope.maxIterations)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown loop type: ${loopType}`)
|
||||
@@ -100,6 +182,23 @@ export class LoopOrchestrator {
|
||||
return scope
|
||||
}
|
||||
|
||||
private addLoopErrorLog(
|
||||
ctx: ExecutionContext,
|
||||
loopId: string,
|
||||
loopType: string,
|
||||
errorMessage: string,
|
||||
inputData?: any
|
||||
): void {
|
||||
addSubflowErrorLog(
|
||||
ctx,
|
||||
loopId,
|
||||
'loop',
|
||||
errorMessage,
|
||||
{ loopType, ...inputData },
|
||||
this.contextExtensions
|
||||
)
|
||||
}
|
||||
|
||||
storeLoopNodeOutput(
|
||||
ctx: ExecutionContext,
|
||||
loopId: string,
|
||||
@@ -412,54 +511,6 @@ export class LoopOrchestrator {
|
||||
}
|
||||
|
||||
private resolveForEachItems(ctx: ExecutionContext, items: any): any[] {
|
||||
if (Array.isArray(items)) {
|
||||
return items
|
||||
}
|
||||
|
||||
if (typeof items === 'object' && items !== null) {
|
||||
return Object.entries(items)
|
||||
}
|
||||
|
||||
if (typeof items === 'string') {
|
||||
if (items.startsWith('<') && items.endsWith('>')) {
|
||||
const resolved = this.resolver.resolveSingleReference(ctx, '', items)
|
||||
if (Array.isArray(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const normalized = items.replace(/'/g, '"')
|
||||
const parsed = JSON.parse(normalized)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse forEach items', { items, error })
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = this.resolver.resolveInputs(ctx, 'loop_foreach_items', { items }).items
|
||||
|
||||
if (Array.isArray(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
|
||||
logger.warn('ForEach items did not resolve to array', {
|
||||
items,
|
||||
resolved,
|
||||
})
|
||||
|
||||
return []
|
||||
} catch (error: any) {
|
||||
logger.error('Error resolving forEach items, returning empty array:', {
|
||||
error: error.message,
|
||||
})
|
||||
return []
|
||||
}
|
||||
return resolveArrayInput(ctx, items, this.resolver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { DEFAULTS } from '@/executor/constants'
|
||||
import type { DAG, DAGNode } from '@/executor/dag/builder'
|
||||
import type { ParallelScope } from '@/executor/execution/state'
|
||||
import type { BlockStateWriter } from '@/executor/execution/types'
|
||||
import type { BlockStateWriter, ContextExtensions } from '@/executor/execution/types'
|
||||
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
|
||||
import type { ParallelConfigWithNodes } from '@/executor/types/parallel'
|
||||
import {
|
||||
addSubflowErrorLog,
|
||||
buildBranchNodeId,
|
||||
calculateBranchCount,
|
||||
extractBaseBlockId,
|
||||
extractBranchIndex,
|
||||
parseDistributionItems,
|
||||
resolveArrayInput,
|
||||
validateMaxCount,
|
||||
} from '@/executor/utils/subflow-utils'
|
||||
import type { VariableResolver } from '@/executor/variables/resolver'
|
||||
import type { SerializedParallel } from '@/serializer/types'
|
||||
@@ -32,6 +36,7 @@ export interface ParallelAggregationResult {
|
||||
|
||||
export class ParallelOrchestrator {
|
||||
private resolver: VariableResolver | null = null
|
||||
private contextExtensions: ContextExtensions | null = null
|
||||
|
||||
constructor(
|
||||
private dag: DAG,
|
||||
@@ -42,6 +47,10 @@ export class ParallelOrchestrator {
|
||||
this.resolver = resolver
|
||||
}
|
||||
|
||||
setContextExtensions(contextExtensions: ContextExtensions): void {
|
||||
this.contextExtensions = contextExtensions
|
||||
}
|
||||
|
||||
initializeParallelScope(
|
||||
ctx: ExecutionContext,
|
||||
parallelId: string,
|
||||
@@ -49,11 +58,42 @@ export class ParallelOrchestrator {
|
||||
terminalNodesCount = 1
|
||||
): ParallelScope {
|
||||
const parallelConfig = this.dag.parallelConfigs.get(parallelId)
|
||||
const items = parallelConfig ? this.resolveDistributionItems(ctx, parallelConfig) : undefined
|
||||
|
||||
// If we have more items than pre-built branches, expand the DAG
|
||||
let items: any[] | undefined
|
||||
if (parallelConfig) {
|
||||
try {
|
||||
items = this.resolveDistributionItems(ctx, parallelConfig)
|
||||
} catch (error) {
|
||||
const errorMessage = `Parallel Items did not resolve: ${error instanceof Error ? error.message : String(error)}`
|
||||
logger.error(errorMessage, {
|
||||
parallelId,
|
||||
distribution: parallelConfig.distribution,
|
||||
})
|
||||
this.addParallelErrorLog(ctx, parallelId, errorMessage, {
|
||||
distribution: parallelConfig.distribution,
|
||||
})
|
||||
this.setErrorScope(ctx, parallelId, errorMessage)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const actualBranchCount = items && items.length > totalBranches ? items.length : totalBranches
|
||||
|
||||
const branchError = validateMaxCount(
|
||||
actualBranchCount,
|
||||
DEFAULTS.MAX_PARALLEL_BRANCHES,
|
||||
'Parallel branch count'
|
||||
)
|
||||
if (branchError) {
|
||||
logger.error(branchError, { parallelId, actualBranchCount })
|
||||
this.addParallelErrorLog(ctx, parallelId, branchError, {
|
||||
distribution: parallelConfig?.distribution,
|
||||
branchCount: actualBranchCount,
|
||||
})
|
||||
this.setErrorScope(ctx, parallelId, branchError)
|
||||
throw new Error(branchError)
|
||||
}
|
||||
|
||||
const scope: ParallelScope = {
|
||||
parallelId,
|
||||
totalBranches: actualBranchCount,
|
||||
@@ -108,6 +148,38 @@ export class ParallelOrchestrator {
|
||||
return scope
|
||||
}
|
||||
|
||||
private addParallelErrorLog(
|
||||
ctx: ExecutionContext,
|
||||
parallelId: string,
|
||||
errorMessage: string,
|
||||
inputData?: any
|
||||
): void {
|
||||
addSubflowErrorLog(
|
||||
ctx,
|
||||
parallelId,
|
||||
'parallel',
|
||||
errorMessage,
|
||||
inputData || {},
|
||||
this.contextExtensions
|
||||
)
|
||||
}
|
||||
|
||||
private setErrorScope(ctx: ExecutionContext, parallelId: string, errorMessage: string): void {
|
||||
const scope: ParallelScope = {
|
||||
parallelId,
|
||||
totalBranches: 0,
|
||||
branchOutputs: new Map(),
|
||||
completedCount: 0,
|
||||
totalExpectedNodes: 0,
|
||||
items: [],
|
||||
validationError: errorMessage,
|
||||
}
|
||||
if (!ctx.parallelExecutions) {
|
||||
ctx.parallelExecutions = new Map()
|
||||
}
|
||||
ctx.parallelExecutions.set(parallelId, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically expand the DAG to include additional branch nodes when
|
||||
* the resolved item count exceeds the pre-built branch count.
|
||||
@@ -291,63 +363,19 @@ export class ParallelOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve distribution items at runtime, handling references like <previousBlock.items>
|
||||
* This mirrors how LoopOrchestrator.resolveForEachItems works.
|
||||
*/
|
||||
private resolveDistributionItems(ctx: ExecutionContext, config: SerializedParallel): any[] {
|
||||
const rawItems = config.distribution
|
||||
|
||||
if (rawItems === undefined || rawItems === null) {
|
||||
if (config.parallelType === 'count') {
|
||||
return []
|
||||
}
|
||||
|
||||
// Already an array - return as-is
|
||||
if (Array.isArray(rawItems)) {
|
||||
return rawItems
|
||||
if (
|
||||
config.distribution === undefined ||
|
||||
config.distribution === null ||
|
||||
config.distribution === ''
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Object - convert to entries array (consistent with loop forEach behavior)
|
||||
if (typeof rawItems === 'object') {
|
||||
return Object.entries(rawItems)
|
||||
}
|
||||
|
||||
// String handling
|
||||
if (typeof rawItems === 'string') {
|
||||
// Resolve references at runtime using the variable resolver
|
||||
if (rawItems.startsWith('<') && rawItems.endsWith('>') && this.resolver) {
|
||||
const resolved = this.resolver.resolveSingleReference(ctx, '', rawItems)
|
||||
if (Array.isArray(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
if (typeof resolved === 'object' && resolved !== null) {
|
||||
return Object.entries(resolved)
|
||||
}
|
||||
logger.warn('Distribution reference did not resolve to array or object', {
|
||||
rawItems,
|
||||
resolved,
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
const normalized = rawItems.replace(/'/g, '"')
|
||||
const parsed = JSON.parse(normalized)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return Object.entries(parsed)
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse distribution items', { rawItems, error })
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
return resolveArrayInput(ctx, config.distribution, this.resolver)
|
||||
}
|
||||
|
||||
handleParallelBranchCompletion(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { LOOP, PARALLEL, PARSING, REFERENCE } from '@/executor/constants'
|
||||
import type { ContextExtensions } from '@/executor/execution/types'
|
||||
import type { BlockLog, ExecutionContext } from '@/executor/types'
|
||||
import type { VariableResolver } from '@/executor/variables/resolver'
|
||||
import type { SerializedParallel } from '@/serializer/types'
|
||||
|
||||
const logger = createLogger('SubflowUtils')
|
||||
@@ -132,3 +135,131 @@ export function normalizeNodeId(nodeId: string): string {
|
||||
}
|
||||
return nodeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a count doesn't exceed a maximum limit.
|
||||
* Returns an error message if validation fails, undefined otherwise.
|
||||
*/
|
||||
export function validateMaxCount(count: number, max: number, itemType: string): string | undefined {
|
||||
if (count > max) {
|
||||
return `${itemType} (${count}) exceeds maximum allowed (${max}). Execution blocked.`
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves array input at runtime. Handles arrays, objects, references, and JSON strings.
|
||||
* Used by both loop forEach and parallel distribution resolution.
|
||||
* Throws an error if resolution fails.
|
||||
*/
|
||||
export function resolveArrayInput(
|
||||
ctx: ExecutionContext,
|
||||
items: any,
|
||||
resolver: VariableResolver | null
|
||||
): any[] {
|
||||
if (Array.isArray(items)) {
|
||||
return items
|
||||
}
|
||||
|
||||
if (typeof items === 'object' && items !== null) {
|
||||
return Object.entries(items)
|
||||
}
|
||||
|
||||
if (typeof items === 'string') {
|
||||
if (items.startsWith(REFERENCE.START) && items.endsWith(REFERENCE.END) && resolver) {
|
||||
try {
|
||||
const resolved = resolver.resolveSingleReference(ctx, '', items)
|
||||
if (Array.isArray(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
if (typeof resolved === 'object' && resolved !== null) {
|
||||
return Object.entries(resolved)
|
||||
}
|
||||
throw new Error(`Reference "${items}" did not resolve to an array or object`)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('Reference "')) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to resolve reference "${items}": ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const normalized = items.replace(/'/g, '"')
|
||||
const parsed = JSON.parse(normalized)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return Object.entries(parsed)
|
||||
}
|
||||
throw new Error(`Parsed value is not an array or object`)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('Parsed value')) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Failed to parse items as JSON: "${items}"`)
|
||||
}
|
||||
}
|
||||
|
||||
if (resolver) {
|
||||
try {
|
||||
const resolved = resolver.resolveInputs(ctx, 'subflow_items', { items }).items
|
||||
if (Array.isArray(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
throw new Error(`Resolved items is not an array`)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('Resolved items')) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to resolve items: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and logs an error for a subflow (loop or parallel).
|
||||
*/
|
||||
export function addSubflowErrorLog(
|
||||
ctx: ExecutionContext,
|
||||
blockId: string,
|
||||
blockType: 'loop' | 'parallel',
|
||||
errorMessage: string,
|
||||
inputData: Record<string, any>,
|
||||
contextExtensions: ContextExtensions | null
|
||||
): void {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const block = ctx.workflow?.blocks?.find((b) => b.id === blockId)
|
||||
const blockName = block?.metadata?.name || (blockType === 'loop' ? 'Loop' : 'Parallel')
|
||||
|
||||
const blockLog: BlockLog = {
|
||||
blockId,
|
||||
blockName,
|
||||
blockType,
|
||||
startedAt: now,
|
||||
endedAt: now,
|
||||
durationMs: 0,
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
input: inputData,
|
||||
output: { error: errorMessage },
|
||||
...(blockType === 'loop' ? { loopId: blockId } : { parallelId: blockId }),
|
||||
}
|
||||
ctx.blockLogs.push(blockLog)
|
||||
|
||||
if (contextExtensions?.onBlockComplete) {
|
||||
contextExtensions.onBlockComplete(blockId, blockName, blockType, {
|
||||
input: inputData,
|
||||
output: { error: errorMessage },
|
||||
executionTime: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,8 @@ export const logKeys = {
|
||||
[...logKeys.lists(), workspaceId ?? '', filters] as const,
|
||||
details: () => [...logKeys.all, 'detail'] as const,
|
||||
detail: (logId: string | undefined) => [...logKeys.details(), logId ?? ''] as const,
|
||||
metrics: () => [...logKeys.all, 'metrics'] as const,
|
||||
executions: (workspaceId: string | undefined, filters: Record<string, any>) =>
|
||||
[...logKeys.metrics(), 'executions', workspaceId ?? '', filters] as const,
|
||||
workflowLogs: (
|
||||
workspaceId: string | undefined,
|
||||
workflowId: string | undefined,
|
||||
filters: Record<string, any>
|
||||
) => [...logKeys.all, 'workflow-logs', workspaceId ?? '', workflowId ?? '', filters] as const,
|
||||
globalLogs: (workspaceId: string | undefined, filters: Record<string, any>) =>
|
||||
[...logKeys.all, 'global-logs', workspaceId ?? '', filters] as const,
|
||||
dashboard: (workspaceId: string | undefined, filters: Record<string, unknown>) =>
|
||||
[...logKeys.all, 'dashboard', workspaceId ?? '', filters] as const,
|
||||
}
|
||||
|
||||
interface LogFilters {
|
||||
@@ -31,6 +23,87 @@ interface LogFilters {
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates start date from a time range string.
|
||||
* Returns null for 'All time' to indicate no date filtering.
|
||||
*/
|
||||
function getStartDateFromTimeRange(timeRange: string): Date | null {
|
||||
if (timeRange === 'All time') return null
|
||||
|
||||
const now = new Date()
|
||||
|
||||
switch (timeRange) {
|
||||
case 'Past 30 minutes':
|
||||
return new Date(now.getTime() - 30 * 60 * 1000)
|
||||
case 'Past hour':
|
||||
return new Date(now.getTime() - 60 * 60 * 1000)
|
||||
case 'Past 6 hours':
|
||||
return new Date(now.getTime() - 6 * 60 * 60 * 1000)
|
||||
case 'Past 12 hours':
|
||||
return new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
||||
case 'Past 24 hours':
|
||||
return new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
case 'Past 3 days':
|
||||
return new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
|
||||
case 'Past 7 days':
|
||||
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
case 'Past 14 days':
|
||||
return new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
|
||||
case 'Past 30 days':
|
||||
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||
default:
|
||||
return new Date(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies common filter parameters to a URLSearchParams object.
|
||||
* Shared between paginated and non-paginated log fetches.
|
||||
*/
|
||||
function applyFilterParams(params: URLSearchParams, filters: Omit<LogFilters, 'limit'>): void {
|
||||
if (filters.level !== 'all') {
|
||||
params.set('level', filters.level)
|
||||
}
|
||||
|
||||
if (filters.triggers.length > 0) {
|
||||
params.set('triggers', filters.triggers.join(','))
|
||||
}
|
||||
|
||||
if (filters.workflowIds.length > 0) {
|
||||
params.set('workflowIds', filters.workflowIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.folderIds.length > 0) {
|
||||
params.set('folderIds', filters.folderIds.join(','))
|
||||
}
|
||||
|
||||
const startDate = getStartDateFromTimeRange(filters.timeRange)
|
||||
if (startDate) {
|
||||
params.set('startDate', startDate.toISOString())
|
||||
}
|
||||
|
||||
if (filters.searchQuery.trim()) {
|
||||
const parsedQuery = parseQuery(filters.searchQuery.trim())
|
||||
const searchParams = queryToApiParams(parsedQuery)
|
||||
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
params.set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryParams(workspaceId: string, filters: LogFilters, page: number): string {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
params.set('workspaceId', workspaceId)
|
||||
params.set('limit', filters.limit.toString())
|
||||
params.set('offset', ((page - 1) * filters.limit).toString())
|
||||
|
||||
applyFilterParams(params, filters)
|
||||
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
async function fetchLogsPage(
|
||||
workspaceId: string,
|
||||
filters: LogFilters,
|
||||
@@ -64,80 +137,6 @@ async function fetchLogDetail(logId: string): Promise<WorkflowLog> {
|
||||
return data
|
||||
}
|
||||
|
||||
function buildQueryParams(workspaceId: string, filters: LogFilters, page: number): string {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
params.set('workspaceId', workspaceId)
|
||||
params.set('limit', filters.limit.toString())
|
||||
params.set('offset', ((page - 1) * filters.limit).toString())
|
||||
|
||||
if (filters.level !== 'all') {
|
||||
params.set('level', filters.level)
|
||||
}
|
||||
|
||||
if (filters.triggers.length > 0) {
|
||||
params.set('triggers', filters.triggers.join(','))
|
||||
}
|
||||
|
||||
if (filters.workflowIds.length > 0) {
|
||||
params.set('workflowIds', filters.workflowIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.folderIds.length > 0) {
|
||||
params.set('folderIds', filters.folderIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.timeRange !== 'All time') {
|
||||
const now = new Date()
|
||||
let startDate: Date
|
||||
|
||||
switch (filters.timeRange) {
|
||||
case 'Past 30 minutes':
|
||||
startDate = new Date(now.getTime() - 30 * 60 * 1000)
|
||||
break
|
||||
case 'Past hour':
|
||||
startDate = new Date(now.getTime() - 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 6 hours':
|
||||
startDate = new Date(now.getTime() - 6 * 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 12 hours':
|
||||
startDate = new Date(now.getTime() - 12 * 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 24 hours':
|
||||
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 3 days':
|
||||
startDate = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 7 days':
|
||||
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 14 days':
|
||||
startDate = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
|
||||
break
|
||||
case 'Past 30 days':
|
||||
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||
break
|
||||
default:
|
||||
startDate = new Date(0)
|
||||
}
|
||||
|
||||
params.set('startDate', startDate.toISOString())
|
||||
}
|
||||
|
||||
if (filters.searchQuery.trim()) {
|
||||
const parsedQuery = parseQuery(filters.searchQuery.trim())
|
||||
const searchParams = queryToApiParams(parsedQuery)
|
||||
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
params.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
interface UseLogsListOptions {
|
||||
enabled?: boolean
|
||||
refetchInterval?: number | false
|
||||
@@ -153,8 +152,8 @@ export function useLogsList(
|
||||
queryFn: ({ pageParam }) => fetchLogsPage(workspaceId as string, filters, pageParam),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
refetchInterval: options?.refetchInterval ?? false,
|
||||
staleTime: 0, // Always consider stale for real-time logs
|
||||
placeholderData: keepPreviousData, // Keep showing old data while fetching new data
|
||||
staleTime: 0,
|
||||
placeholderData: keepPreviousData,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
})
|
||||
@@ -165,303 +164,60 @@ export function useLogDetail(logId: string | undefined) {
|
||||
queryKey: logKeys.detail(logId),
|
||||
queryFn: () => fetchLogDetail(logId as string),
|
||||
enabled: Boolean(logId),
|
||||
staleTime: 30 * 1000, // Details can be slightly stale (30 seconds)
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
interface WorkflowSegment {
|
||||
timestamp: string
|
||||
hasExecutions: boolean
|
||||
totalExecutions: number
|
||||
successfulExecutions: number
|
||||
successRate: number
|
||||
avgDurationMs?: number
|
||||
p50Ms?: number
|
||||
p90Ms?: number
|
||||
p99Ms?: number
|
||||
}
|
||||
const DASHBOARD_LOGS_LIMIT = 10000
|
||||
|
||||
interface WorkflowExecution {
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
segments: WorkflowSegment[]
|
||||
overallSuccessRate: number
|
||||
}
|
||||
/**
|
||||
* Fetches all logs for dashboard metrics (non-paginated).
|
||||
* Uses same filters as the logs list but with a high limit to get all data.
|
||||
*/
|
||||
async function fetchAllLogs(
|
||||
workspaceId: string,
|
||||
filters: Omit<LogFilters, 'limit'>
|
||||
): Promise<WorkflowLog[]> {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
interface AggregateSegment {
|
||||
timestamp: string
|
||||
totalExecutions: number
|
||||
successfulExecutions: number
|
||||
avgDurationMs?: number
|
||||
}
|
||||
params.set('workspaceId', workspaceId)
|
||||
params.set('limit', DASHBOARD_LOGS_LIMIT.toString())
|
||||
params.set('offset', '0')
|
||||
|
||||
interface ExecutionsMetricsResponse {
|
||||
workflows: WorkflowExecution[]
|
||||
aggregateSegments: AggregateSegment[]
|
||||
}
|
||||
|
||||
interface DashboardMetricsFilters {
|
||||
workspaceId: string
|
||||
segments: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
workflowIds?: string[]
|
||||
folderIds?: string[]
|
||||
triggers?: string[]
|
||||
level?: string
|
||||
}
|
||||
|
||||
async function fetchExecutionsMetrics(
|
||||
filters: DashboardMetricsFilters
|
||||
): Promise<ExecutionsMetricsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
segments: String(filters.segments),
|
||||
startTime: filters.startTime,
|
||||
endTime: filters.endTime,
|
||||
})
|
||||
|
||||
if (filters.workflowIds && filters.workflowIds.length > 0) {
|
||||
params.set('workflowIds', filters.workflowIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.folderIds && filters.folderIds.length > 0) {
|
||||
params.set('folderIds', filters.folderIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.triggers && filters.triggers.length > 0) {
|
||||
params.set('triggers', filters.triggers.join(','))
|
||||
}
|
||||
|
||||
if (filters.level && filters.level !== 'all') {
|
||||
params.set('level', filters.level)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/workspaces/${filters.workspaceId}/metrics/executions?${params.toString()}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch execution metrics')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const workflows: WorkflowExecution[] = (data.workflows || []).map((wf: any) => {
|
||||
const segments = (wf.segments || []).map((s: any) => {
|
||||
const total = s.totalExecutions || 0
|
||||
const success = s.successfulExecutions || 0
|
||||
const hasExecutions = total > 0
|
||||
const successRate = hasExecutions ? (success / total) * 100 : 100
|
||||
return {
|
||||
timestamp: s.timestamp,
|
||||
hasExecutions,
|
||||
totalExecutions: total,
|
||||
successfulExecutions: success,
|
||||
successRate,
|
||||
avgDurationMs: typeof s.avgDurationMs === 'number' ? s.avgDurationMs : 0,
|
||||
p50Ms: typeof s.p50Ms === 'number' ? s.p50Ms : 0,
|
||||
p90Ms: typeof s.p90Ms === 'number' ? s.p90Ms : 0,
|
||||
p99Ms: typeof s.p99Ms === 'number' ? s.p99Ms : 0,
|
||||
}
|
||||
})
|
||||
|
||||
const totals = segments.reduce(
|
||||
(acc: { total: number; success: number }, seg: WorkflowSegment) => {
|
||||
acc.total += seg.totalExecutions
|
||||
acc.success += seg.successfulExecutions
|
||||
return acc
|
||||
},
|
||||
{ total: 0, success: 0 }
|
||||
)
|
||||
|
||||
const overallSuccessRate = totals.total > 0 ? (totals.success / totals.total) * 100 : 100
|
||||
|
||||
return {
|
||||
workflowId: wf.workflowId,
|
||||
workflowName: wf.workflowName,
|
||||
segments,
|
||||
overallSuccessRate,
|
||||
}
|
||||
})
|
||||
|
||||
const sortedWorkflows = workflows.sort((a, b) => {
|
||||
const errA = a.overallSuccessRate < 100 ? 1 - a.overallSuccessRate / 100 : 0
|
||||
const errB = b.overallSuccessRate < 100 ? 1 - b.overallSuccessRate / 100 : 0
|
||||
return errB - errA
|
||||
})
|
||||
|
||||
const segmentCount = filters.segments
|
||||
const startTime = new Date(filters.startTime)
|
||||
const endTime = new Date(filters.endTime)
|
||||
|
||||
const aggregateSegments: AggregateSegment[] = Array.from({ length: segmentCount }, (_, i) => {
|
||||
const base = startTime.getTime()
|
||||
const ts = new Date(base + Math.floor((i * (endTime.getTime() - base)) / segmentCount))
|
||||
return {
|
||||
timestamp: ts.toISOString(),
|
||||
totalExecutions: 0,
|
||||
successfulExecutions: 0,
|
||||
avgDurationMs: 0,
|
||||
}
|
||||
})
|
||||
|
||||
const weightedDurationSums: number[] = Array(segmentCount).fill(0)
|
||||
const executionCounts: number[] = Array(segmentCount).fill(0)
|
||||
|
||||
for (const wf of data.workflows as any[]) {
|
||||
wf.segments.forEach((s: any, i: number) => {
|
||||
const index = Math.min(i, segmentCount - 1)
|
||||
const execCount = s.totalExecutions || 0
|
||||
|
||||
aggregateSegments[index].totalExecutions += execCount
|
||||
aggregateSegments[index].successfulExecutions += s.successfulExecutions || 0
|
||||
|
||||
if (typeof s.avgDurationMs === 'number' && s.avgDurationMs > 0 && execCount > 0) {
|
||||
weightedDurationSums[index] += s.avgDurationMs * execCount
|
||||
executionCounts[index] += execCount
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
aggregateSegments.forEach((seg, i) => {
|
||||
if (executionCounts[i] > 0) {
|
||||
seg.avgDurationMs = weightedDurationSums[i] / executionCounts[i]
|
||||
} else {
|
||||
seg.avgDurationMs = 0
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
workflows: sortedWorkflows,
|
||||
aggregateSegments,
|
||||
}
|
||||
}
|
||||
|
||||
interface UseExecutionsMetricsOptions {
|
||||
enabled?: boolean
|
||||
refetchInterval?: number | false
|
||||
}
|
||||
|
||||
export function useExecutionsMetrics(
|
||||
filters: DashboardMetricsFilters,
|
||||
options?: UseExecutionsMetricsOptions
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: logKeys.executions(filters.workspaceId, filters),
|
||||
queryFn: () => fetchExecutionsMetrics(filters),
|
||||
enabled: Boolean(filters.workspaceId) && (options?.enabled ?? true),
|
||||
refetchInterval: options?.refetchInterval ?? false,
|
||||
staleTime: 10 * 1000, // Metrics can be slightly stale (10 seconds)
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
interface DashboardLogsFilters {
|
||||
workspaceId: string
|
||||
startDate: string
|
||||
endDate: string
|
||||
workflowIds?: string[]
|
||||
folderIds?: string[]
|
||||
triggers?: string[]
|
||||
level?: string
|
||||
searchQuery?: string
|
||||
limit: number
|
||||
}
|
||||
|
||||
interface DashboardLogsPage {
|
||||
logs: any[] // Will be mapped by the consumer
|
||||
hasMore: boolean
|
||||
nextPage: number | undefined
|
||||
}
|
||||
|
||||
async function fetchDashboardLogsPage(
|
||||
filters: DashboardLogsFilters,
|
||||
page: number,
|
||||
workflowId?: string
|
||||
): Promise<DashboardLogsPage> {
|
||||
const params = new URLSearchParams({
|
||||
limit: filters.limit.toString(),
|
||||
offset: ((page - 1) * filters.limit).toString(),
|
||||
workspaceId: filters.workspaceId,
|
||||
startDate: filters.startDate,
|
||||
endDate: filters.endDate,
|
||||
order: 'desc',
|
||||
details: 'full',
|
||||
})
|
||||
|
||||
if (workflowId) {
|
||||
params.set('workflowIds', workflowId)
|
||||
} else if (filters.workflowIds && filters.workflowIds.length > 0) {
|
||||
params.set('workflowIds', filters.workflowIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.folderIds && filters.folderIds.length > 0) {
|
||||
params.set('folderIds', filters.folderIds.join(','))
|
||||
}
|
||||
|
||||
if (filters.triggers && filters.triggers.length > 0) {
|
||||
params.set('triggers', filters.triggers.join(','))
|
||||
}
|
||||
|
||||
if (filters.level && filters.level !== 'all') {
|
||||
params.set('level', filters.level)
|
||||
}
|
||||
|
||||
if (filters.searchQuery?.trim()) {
|
||||
const parsed = parseQuery(filters.searchQuery)
|
||||
const extraParams = queryToApiParams(parsed)
|
||||
Object.entries(extraParams).forEach(([key, value]) => {
|
||||
params.set(key, value)
|
||||
})
|
||||
}
|
||||
applyFilterParams(params, filters)
|
||||
|
||||
const response = await fetch(`/api/logs?${params.toString()}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch dashboard logs')
|
||||
throw new Error('Failed to fetch logs for dashboard')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const logs = data.data || []
|
||||
const hasMore = logs.length === filters.limit
|
||||
|
||||
return {
|
||||
logs,
|
||||
hasMore,
|
||||
nextPage: hasMore ? page + 1 : undefined,
|
||||
}
|
||||
const apiData: LogsResponse = await response.json()
|
||||
return apiData.data || []
|
||||
}
|
||||
|
||||
interface UseDashboardLogsOptions {
|
||||
enabled?: boolean
|
||||
refetchInterval?: number | false
|
||||
}
|
||||
|
||||
export function useGlobalDashboardLogs(
|
||||
filters: DashboardLogsFilters,
|
||||
/**
|
||||
* Hook for fetching all logs for dashboard metrics.
|
||||
* Unlike useLogsList, this fetches all logs in a single request
|
||||
* to ensure dashboard metrics are computed from complete data.
|
||||
*/
|
||||
export function useDashboardLogs(
|
||||
workspaceId: string | undefined,
|
||||
filters: Omit<LogFilters, 'limit'>,
|
||||
options?: UseDashboardLogsOptions
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: logKeys.globalLogs(filters.workspaceId, filters),
|
||||
queryFn: ({ pageParam }) => fetchDashboardLogsPage(filters, pageParam),
|
||||
enabled: Boolean(filters.workspaceId) && (options?.enabled ?? true),
|
||||
staleTime: 10 * 1000, // Slightly stale (10 seconds)
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
})
|
||||
}
|
||||
|
||||
export function useWorkflowDashboardLogs(
|
||||
workflowId: string | undefined,
|
||||
filters: DashboardLogsFilters,
|
||||
options?: UseDashboardLogsOptions
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: logKeys.workflowLogs(filters.workspaceId, workflowId, filters),
|
||||
queryFn: ({ pageParam }) => fetchDashboardLogsPage(filters, pageParam, workflowId),
|
||||
enabled: Boolean(filters.workspaceId) && Boolean(workflowId) && (options?.enabled ?? true),
|
||||
staleTime: 10 * 1000, // Slightly stale (10 seconds)
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
return useQuery({
|
||||
queryKey: logKeys.dashboard(workspaceId, filters),
|
||||
queryFn: () => fetchAllLogs(workspaceId as string, filters),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
refetchInterval: options?.refetchInterval ?? false,
|
||||
staleTime: 0,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ProviderName } from '@/stores/providers/types'
|
||||
import type { OpenRouterModelInfo, ProviderName } from '@/stores/providers/types'
|
||||
|
||||
const logger = createLogger('ProviderModelsQuery')
|
||||
|
||||
@@ -11,7 +11,12 @@ const providerEndpoints: Record<ProviderName, string> = {
|
||||
openrouter: '/api/providers/openrouter/models',
|
||||
}
|
||||
|
||||
async function fetchProviderModels(provider: ProviderName): Promise<string[]> {
|
||||
interface ProviderModelsResponse {
|
||||
models: string[]
|
||||
modelInfo?: Record<string, OpenRouterModelInfo>
|
||||
}
|
||||
|
||||
async function fetchProviderModels(provider: ProviderName): Promise<ProviderModelsResponse> {
|
||||
const response = await fetch(providerEndpoints[provider])
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -24,8 +29,12 @@ async function fetchProviderModels(provider: ProviderName): Promise<string[]> {
|
||||
|
||||
const data = await response.json()
|
||||
const models: string[] = Array.isArray(data.models) ? data.models : []
|
||||
const uniqueModels = provider === 'openrouter' ? Array.from(new Set(models)) : models
|
||||
|
||||
return provider === 'openrouter' ? Array.from(new Set(models)) : models
|
||||
return {
|
||||
models: uniqueModels,
|
||||
modelInfo: data.modelInfo,
|
||||
}
|
||||
}
|
||||
|
||||
export function useProviderModels(provider: ProviderName) {
|
||||
|
||||
@@ -579,6 +579,21 @@ export const auth = betterAuth({
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-groups`,
|
||||
},
|
||||
|
||||
{
|
||||
providerId: 'vertex-ai',
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/vertex-ai`,
|
||||
},
|
||||
|
||||
{
|
||||
providerId: 'microsoft-teams',
|
||||
clientId: env.MICROSOFT_CLIENT_ID as string,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/**
|
||||
* Fallback free credits (in dollars) when env var is not set
|
||||
*/
|
||||
export const DEFAULT_FREE_CREDITS = 10
|
||||
export const DEFAULT_FREE_CREDITS = 20
|
||||
|
||||
/**
|
||||
* Default per-user minimum limits (in dollars) for paid plans when env vars are absent
|
||||
|
||||
@@ -32,8 +32,8 @@ export class DocsChunker {
|
||||
// Use the existing TextChunker for chunking logic
|
||||
this.textChunker = new TextChunker({
|
||||
chunkSize: options.chunkSize ?? 300, // Max 300 tokens per chunk
|
||||
minChunkSize: options.minChunkSize ?? 1,
|
||||
overlap: options.overlap ?? 50,
|
||||
minCharactersPerChunk: options.minCharactersPerChunk ?? 1,
|
||||
chunkOverlap: options.chunkOverlap ?? 50,
|
||||
})
|
||||
// Use localhost docs in development, production docs otherwise
|
||||
this.baseUrl = options.baseUrl ?? 'https://docs.sim.ai'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user