@nextsparkjs/plugin-langchain 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +41 -0
- package/api/observability/metrics/route.ts +110 -0
- package/api/observability/traces/[traceId]/route.ts +398 -0
- package/api/observability/traces/route.ts +205 -0
- package/api/sessions/route.ts +332 -0
- package/components/observability/CollapsibleJson.tsx +71 -0
- package/components/observability/CompactTimeline.tsx +75 -0
- package/components/observability/ConversationFlow.tsx +271 -0
- package/components/observability/DisabledMessage.tsx +21 -0
- package/components/observability/FiltersPanel.tsx +82 -0
- package/components/observability/ObservabilityDashboard.tsx +230 -0
- package/components/observability/SpansList.tsx +210 -0
- package/components/observability/TraceDetail.tsx +335 -0
- package/components/observability/TraceStatusBadge.tsx +39 -0
- package/components/observability/TracesTable.tsx +97 -0
- package/components/observability/index.ts +7 -0
- package/docs/01-getting-started/01-overview.md +196 -0
- package/docs/01-getting-started/02-installation.md +368 -0
- package/docs/01-getting-started/03-configuration.md +794 -0
- package/docs/02-core-concepts/01-architecture.md +566 -0
- package/docs/02-core-concepts/02-agents.md +597 -0
- package/docs/02-core-concepts/03-tools.md +689 -0
- package/docs/03-orchestration/01-graph-orchestrator.md +809 -0
- package/docs/03-orchestration/02-legacy-react.md +650 -0
- package/docs/04-advanced/01-observability.md +645 -0
- package/docs/04-advanced/02-token-tracking.md +469 -0
- package/docs/04-advanced/03-streaming.md +476 -0
- package/docs/04-advanced/04-guardrails.md +597 -0
- package/docs/05-reference/01-api-reference.md +1403 -0
- package/docs/05-reference/02-customization.md +646 -0
- package/docs/05-reference/03-examples.md +881 -0
- package/docs/index.md +85 -0
- package/hooks/observability/useMetrics.ts +31 -0
- package/hooks/observability/useTraceDetail.ts +48 -0
- package/hooks/observability/useTraces.ts +59 -0
- package/lib/agent-factory.ts +354 -0
- package/lib/agent-helpers.ts +201 -0
- package/lib/db-memory-store.ts +417 -0
- package/lib/graph/index.ts +58 -0
- package/lib/graph/nodes/combiner.ts +399 -0
- package/lib/graph/nodes/router.ts +440 -0
- package/lib/graph/orchestrator-graph.ts +386 -0
- package/lib/graph/prompts/combiner.md +131 -0
- package/lib/graph/prompts/router.md +193 -0
- package/lib/graph/types.ts +365 -0
- package/lib/guardrails.ts +230 -0
- package/lib/index.ts +44 -0
- package/lib/logger.ts +70 -0
- package/lib/memory-store.ts +168 -0
- package/lib/message-serializer.ts +110 -0
- package/lib/prompt-renderer.ts +94 -0
- package/lib/providers.ts +226 -0
- package/lib/streaming.ts +232 -0
- package/lib/token-tracker.ts +298 -0
- package/lib/tools-builder.ts +192 -0
- package/lib/tracer-callbacks.ts +342 -0
- package/lib/tracer.ts +350 -0
- package/migrations/001_langchain_memory.sql +83 -0
- package/migrations/002_token_usage.sql +127 -0
- package/migrations/003_observability.sql +257 -0
- package/package.json +28 -0
- package/plugin.config.ts +170 -0
- package/presets/lib/langchain.config.ts.preset +142 -0
- package/presets/templates/sector7/ai-observability/[traceId]/page.tsx +91 -0
- package/presets/templates/sector7/ai-observability/page.tsx +54 -0
- package/types/langchain.types.ts +274 -0
- package/types/observability.types.ts +270 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* AGENT HELPERS FACTORY
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Provides utility functions for working with agent configurations.
|
|
7
|
+
* Themes define their agents in langchain.config.ts, then use these helpers.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // In theme's langchain.config.ts
|
|
12
|
+
* import { createAgentHelpers } from '@/plugins/langchain/lib/agent-helpers'
|
|
13
|
+
*
|
|
14
|
+
* export const AGENTS = { ... }
|
|
15
|
+
* export const helpers = createAgentHelpers(AGENTS)
|
|
16
|
+
* // or destructure: export const { getAgentConfig, getAgentTools } = createAgentHelpers(AGENTS)
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* ============================================================================
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
AgentDefinition,
|
|
24
|
+
AgentContext,
|
|
25
|
+
SessionConfig,
|
|
26
|
+
ToolDefinition,
|
|
27
|
+
ThemeLangChainConfig,
|
|
28
|
+
} from '../types/langchain.types'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Agent helpers interface - all utilities for working with agent configs
|
|
32
|
+
*/
|
|
33
|
+
export interface AgentHelpers {
|
|
34
|
+
/** Get complete configuration for an agent */
|
|
35
|
+
getAgentConfig: (agentName: string) => AgentDefinition | undefined
|
|
36
|
+
|
|
37
|
+
/** Get model configuration (provider, model, temperature) */
|
|
38
|
+
getAgentModelConfig: (agentName: string) => Partial<{
|
|
39
|
+
provider: 'openai' | 'anthropic' | 'ollama'
|
|
40
|
+
model: string
|
|
41
|
+
temperature: number
|
|
42
|
+
}> | undefined
|
|
43
|
+
|
|
44
|
+
/** Get tools for an agent with runtime context */
|
|
45
|
+
getAgentTools: (agentName: string, context: AgentContext) => ToolDefinition<any>[]
|
|
46
|
+
|
|
47
|
+
/** Get system prompt name (to be loaded from .md file) */
|
|
48
|
+
getAgentPromptName: (agentName: string) => string | undefined
|
|
49
|
+
|
|
50
|
+
/** Get session configuration for an agent (TTL, maxMessages) */
|
|
51
|
+
getAgentSessionConfig: (agentName: string) => SessionConfig | undefined
|
|
52
|
+
|
|
53
|
+
/** Get enrichContext function for an agent */
|
|
54
|
+
getAgentEnrichContext: (agentName: string) => ((context: AgentContext) => Promise<AgentContext>) | undefined
|
|
55
|
+
|
|
56
|
+
/** Check if an agent is configured */
|
|
57
|
+
hasAgent: (agentName: string) => boolean
|
|
58
|
+
|
|
59
|
+
/** Get all configured agent names */
|
|
60
|
+
getAgentNames: () => string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create agent helper functions bound to a specific agents configuration.
|
|
65
|
+
*
|
|
66
|
+
* @param agents - Record of agent name to AgentDefinition
|
|
67
|
+
* @param defaults - Optional default values for provider/model/temperature
|
|
68
|
+
* @returns Object with all helper functions
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const AGENTS = {
|
|
73
|
+
* 'single-agent': {
|
|
74
|
+
* provider: 'ollama',
|
|
75
|
+
* model: 'llama3.2:3b',
|
|
76
|
+
* temperature: 0.3,
|
|
77
|
+
* systemPrompt: 'single-agent',
|
|
78
|
+
* createTools: (ctx) => [...tools],
|
|
79
|
+
* },
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* const helpers = createAgentHelpers(AGENTS)
|
|
83
|
+
* const config = helpers.getAgentConfig('single-agent')
|
|
84
|
+
* const tools = helpers.getAgentTools('single-agent', { userId, teamId })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function createAgentHelpers(
|
|
88
|
+
agents: Record<string, AgentDefinition>,
|
|
89
|
+
defaults?: {
|
|
90
|
+
provider?: 'openai' | 'anthropic' | 'ollama'
|
|
91
|
+
model?: string
|
|
92
|
+
temperature?: number
|
|
93
|
+
}
|
|
94
|
+
): AgentHelpers {
|
|
95
|
+
return {
|
|
96
|
+
/**
|
|
97
|
+
* Get complete configuration for an agent
|
|
98
|
+
*/
|
|
99
|
+
getAgentConfig(agentName: string): AgentDefinition | undefined {
|
|
100
|
+
return agents[agentName]
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get model configuration for an agent (provider, model, temperature)
|
|
105
|
+
*/
|
|
106
|
+
getAgentModelConfig(agentName: string): Partial<{
|
|
107
|
+
provider: 'openai' | 'anthropic' | 'ollama'
|
|
108
|
+
model: string
|
|
109
|
+
temperature: number
|
|
110
|
+
}> | undefined {
|
|
111
|
+
const agent = agents[agentName]
|
|
112
|
+
if (!agent) {
|
|
113
|
+
// Fall back to defaults if provided
|
|
114
|
+
if (defaults?.provider) {
|
|
115
|
+
return {
|
|
116
|
+
provider: defaults.provider,
|
|
117
|
+
model: defaults.model,
|
|
118
|
+
temperature: defaults.temperature,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
provider: agent.provider,
|
|
126
|
+
model: agent.model,
|
|
127
|
+
temperature: agent.temperature,
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get tools for an agent with runtime context
|
|
133
|
+
*/
|
|
134
|
+
getAgentTools(
|
|
135
|
+
agentName: string,
|
|
136
|
+
context: AgentContext
|
|
137
|
+
): ToolDefinition<any>[] {
|
|
138
|
+
const agent = agents[agentName]
|
|
139
|
+
if (!agent?.createTools) {
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
return agent.createTools(context) as ToolDefinition<any>[]
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get system prompt name for an agent
|
|
147
|
+
* Returns the prompt name to be loaded from .md file
|
|
148
|
+
*/
|
|
149
|
+
getAgentPromptName(agentName: string): string | undefined {
|
|
150
|
+
const agent = agents[agentName]
|
|
151
|
+
return agent?.systemPrompt
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get session configuration for an agent (TTL, maxMessages)
|
|
156
|
+
*/
|
|
157
|
+
getAgentSessionConfig(agentName: string): SessionConfig | undefined {
|
|
158
|
+
const agent = agents[agentName]
|
|
159
|
+
return agent?.sessionConfig
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get enrichContext function for an agent
|
|
164
|
+
*/
|
|
165
|
+
getAgentEnrichContext(
|
|
166
|
+
agentName: string
|
|
167
|
+
): ((context: AgentContext) => Promise<AgentContext>) | undefined {
|
|
168
|
+
const agent = agents[agentName]
|
|
169
|
+
return agent?.enrichContext
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if an agent is configured
|
|
174
|
+
*/
|
|
175
|
+
hasAgent(agentName: string): boolean {
|
|
176
|
+
return agentName in agents
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all configured agent names
|
|
181
|
+
*/
|
|
182
|
+
getAgentNames(): string[] {
|
|
183
|
+
return Object.keys(agents)
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create helpers from a full ThemeLangChainConfig
|
|
190
|
+
* Convenience function that extracts agents and defaults
|
|
191
|
+
*/
|
|
192
|
+
export function createHelpersFromConfig(config: ThemeLangChainConfig): AgentHelpers {
|
|
193
|
+
return createAgentHelpers(
|
|
194
|
+
config.agents || {},
|
|
195
|
+
{
|
|
196
|
+
provider: config.defaultProvider,
|
|
197
|
+
model: config.defaultModel,
|
|
198
|
+
temperature: config.defaultTemperature,
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Memory Store for LangChain
|
|
3
|
+
*
|
|
4
|
+
* Persistent storage for conversation history using PostgreSQL.
|
|
5
|
+
* Supports multi-tenancy with userId + teamId + sessionId isolation.
|
|
6
|
+
* Supports multiple conversations per user with no expiration by default.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { BaseMessage, HumanMessage } from '@langchain/core/messages'
|
|
10
|
+
import { queryWithRLS, mutateWithRLS, query } from '@nextsparkjs/core/lib/db'
|
|
11
|
+
import {
|
|
12
|
+
serializeMessages,
|
|
13
|
+
deserializeMessages,
|
|
14
|
+
type SerializedMessage,
|
|
15
|
+
} from './message-serializer'
|
|
16
|
+
import type { AgentContext, SessionConfig } from '../types/langchain.types'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Conversation/Session information returned by list and get operations
|
|
20
|
+
*/
|
|
21
|
+
export interface ConversationInfo {
|
|
22
|
+
sessionId: string
|
|
23
|
+
name: string | null
|
|
24
|
+
messageCount: number
|
|
25
|
+
firstMessage: string | null
|
|
26
|
+
isPinned: boolean
|
|
27
|
+
createdAt: Date
|
|
28
|
+
updatedAt: Date
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Conversation limits
|
|
33
|
+
*/
|
|
34
|
+
export const CONVERSATION_LIMITS = {
|
|
35
|
+
MAX_CONVERSATIONS: 50,
|
|
36
|
+
MAX_MESSAGES_PER_CONVERSATION: 50,
|
|
37
|
+
} as const
|
|
38
|
+
|
|
39
|
+
const DEFAULT_MAX_MESSAGES = CONVERSATION_LIMITS.MAX_MESSAGES_PER_CONVERSATION
|
|
40
|
+
const DEFAULT_TTL_HOURS: number | null = null // No expiration by default
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Row type from database
|
|
44
|
+
*/
|
|
45
|
+
interface SessionRow {
|
|
46
|
+
id: string
|
|
47
|
+
userId: string
|
|
48
|
+
teamId: string
|
|
49
|
+
sessionId: string
|
|
50
|
+
name: string | null
|
|
51
|
+
isPinned: boolean
|
|
52
|
+
messages: SerializedMessage[]
|
|
53
|
+
maxMessages: number
|
|
54
|
+
expiresAt: Date | null
|
|
55
|
+
createdAt: Date
|
|
56
|
+
updatedAt: Date
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a new session ID
|
|
61
|
+
*/
|
|
62
|
+
export function generateSessionId(userId: string): string {
|
|
63
|
+
return `${userId}-${Date.now()}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract first human message content from serialized messages
|
|
68
|
+
*/
|
|
69
|
+
function getFirstHumanMessage(messages: SerializedMessage[]): string | null {
|
|
70
|
+
const humanMessage = messages.find((m) => m.type === 'human')
|
|
71
|
+
if (!humanMessage) return null
|
|
72
|
+
|
|
73
|
+
const content = humanMessage.content
|
|
74
|
+
if (typeof content === 'string') {
|
|
75
|
+
return content.slice(0, 100) // Truncate to 100 chars
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Database-backed memory store with full multi-tenancy support
|
|
82
|
+
*/
|
|
83
|
+
export const dbMemoryStore = {
|
|
84
|
+
/**
|
|
85
|
+
* Get messages for a session
|
|
86
|
+
*/
|
|
87
|
+
getMessages: async (
|
|
88
|
+
sessionId: string,
|
|
89
|
+
context: AgentContext
|
|
90
|
+
): Promise<BaseMessage[]> => {
|
|
91
|
+
const { userId, teamId } = context
|
|
92
|
+
|
|
93
|
+
const result = await queryWithRLS<SessionRow>(
|
|
94
|
+
`SELECT messages
|
|
95
|
+
FROM public."langchain_sessions"
|
|
96
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3
|
|
97
|
+
AND ("expiresAt" IS NULL OR "expiresAt" > now())`,
|
|
98
|
+
[userId, teamId, sessionId],
|
|
99
|
+
userId
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (!result.length) return []
|
|
103
|
+
|
|
104
|
+
return deserializeMessages(result[0].messages)
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get full session info (not just messages)
|
|
109
|
+
*/
|
|
110
|
+
getSession: async (
|
|
111
|
+
sessionId: string,
|
|
112
|
+
context: AgentContext
|
|
113
|
+
): Promise<ConversationInfo | null> => {
|
|
114
|
+
const { userId, teamId } = context
|
|
115
|
+
|
|
116
|
+
const result = await queryWithRLS<SessionRow>(
|
|
117
|
+
`SELECT "sessionId", name, "isPinned", messages, "createdAt", "updatedAt"
|
|
118
|
+
FROM public."langchain_sessions"
|
|
119
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3
|
|
120
|
+
AND ("expiresAt" IS NULL OR "expiresAt" > now())`,
|
|
121
|
+
[userId, teamId, sessionId],
|
|
122
|
+
userId
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if (!result.length) return null
|
|
126
|
+
|
|
127
|
+
const row = result[0]
|
|
128
|
+
return {
|
|
129
|
+
sessionId: row.sessionId,
|
|
130
|
+
name: row.name,
|
|
131
|
+
messageCount: row.messages.length,
|
|
132
|
+
firstMessage: getFirstHumanMessage(row.messages),
|
|
133
|
+
isPinned: row.isPinned,
|
|
134
|
+
createdAt: row.createdAt,
|
|
135
|
+
updatedAt: row.updatedAt,
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Add messages to a session (upsert with sliding window)
|
|
141
|
+
* Auto-generates name from first message if not set
|
|
142
|
+
*
|
|
143
|
+
* OPTIMIZED: Uses a single query with CTE instead of 3 separate queries
|
|
144
|
+
* - Reads existing messages, combines with new, applies sliding window
|
|
145
|
+
* - All done atomically in PostgreSQL
|
|
146
|
+
*/
|
|
147
|
+
addMessages: async (
|
|
148
|
+
sessionId: string,
|
|
149
|
+
messages: BaseMessage[],
|
|
150
|
+
context: AgentContext,
|
|
151
|
+
config: SessionConfig = {}
|
|
152
|
+
): Promise<void> => {
|
|
153
|
+
const { userId, teamId } = context
|
|
154
|
+
const maxMessages = config.maxMessages || DEFAULT_MAX_MESSAGES
|
|
155
|
+
const ttlHours = config.ttlHours === undefined ? DEFAULT_TTL_HOURS : config.ttlHours
|
|
156
|
+
|
|
157
|
+
// Serialize new messages
|
|
158
|
+
const newMessagesSerialized = serializeMessages(messages)
|
|
159
|
+
|
|
160
|
+
// Auto-generate name from first human message
|
|
161
|
+
let autoName: string | null = null
|
|
162
|
+
if (messages.length > 0) {
|
|
163
|
+
const firstHuman = messages.find((m) => m instanceof HumanMessage)
|
|
164
|
+
if (firstHuman) {
|
|
165
|
+
const content = firstHuman.content
|
|
166
|
+
if (typeof content === 'string') {
|
|
167
|
+
autoName = content.slice(0, 50) // First 50 chars
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Calculate expiresAt
|
|
173
|
+
const expiresAt = ttlHours !== null
|
|
174
|
+
? new Date(Date.now() + ttlHours * 60 * 60 * 1000)
|
|
175
|
+
: null
|
|
176
|
+
|
|
177
|
+
// Single atomic query: read existing + combine + sliding window + upsert
|
|
178
|
+
// Uses CTE to:
|
|
179
|
+
// 1. Get existing messages (or empty array)
|
|
180
|
+
// 2. Combine with new messages
|
|
181
|
+
// 3. Apply sliding window (keep last N messages)
|
|
182
|
+
// 4. Upsert the result
|
|
183
|
+
await mutateWithRLS(
|
|
184
|
+
`WITH existing_session AS (
|
|
185
|
+
SELECT messages, name
|
|
186
|
+
FROM public."langchain_sessions"
|
|
187
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3
|
|
188
|
+
),
|
|
189
|
+
combined AS (
|
|
190
|
+
SELECT COALESCE((SELECT messages FROM existing_session), '[]'::jsonb) || $4::jsonb as all_msgs
|
|
191
|
+
),
|
|
192
|
+
windowed AS (
|
|
193
|
+
SELECT COALESCE(
|
|
194
|
+
(
|
|
195
|
+
SELECT jsonb_agg(value ORDER BY ordinality)
|
|
196
|
+
FROM (
|
|
197
|
+
SELECT value, ordinality
|
|
198
|
+
FROM combined, jsonb_array_elements(all_msgs) WITH ORDINALITY
|
|
199
|
+
) sub
|
|
200
|
+
WHERE ordinality > jsonb_array_length((SELECT all_msgs FROM combined)) - $5
|
|
201
|
+
),
|
|
202
|
+
$4::jsonb
|
|
203
|
+
) as final_msgs
|
|
204
|
+
)
|
|
205
|
+
INSERT INTO public."langchain_sessions"
|
|
206
|
+
(id, "userId", "teamId", "sessionId", messages, "maxMessages", "expiresAt", name)
|
|
207
|
+
SELECT
|
|
208
|
+
gen_random_uuid()::text, $1, $2, $3,
|
|
209
|
+
(SELECT final_msgs FROM windowed),
|
|
210
|
+
$5, $6,
|
|
211
|
+
COALESCE((SELECT name FROM existing_session), $7)
|
|
212
|
+
ON CONFLICT ("userId", "teamId", "sessionId")
|
|
213
|
+
DO UPDATE SET
|
|
214
|
+
messages = EXCLUDED.messages,
|
|
215
|
+
"expiresAt" = COALESCE(public."langchain_sessions"."expiresAt", EXCLUDED."expiresAt"),
|
|
216
|
+
name = COALESCE(public."langchain_sessions".name, EXCLUDED.name),
|
|
217
|
+
"updatedAt" = now()`,
|
|
218
|
+
[userId, teamId, sessionId, JSON.stringify(newMessagesSerialized), maxMessages, expiresAt, autoName],
|
|
219
|
+
userId
|
|
220
|
+
)
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a new empty session/conversation
|
|
225
|
+
*/
|
|
226
|
+
createSession: async (
|
|
227
|
+
context: AgentContext,
|
|
228
|
+
name?: string
|
|
229
|
+
): Promise<{ sessionId: string; createdAt: Date }> => {
|
|
230
|
+
const { userId, teamId } = context
|
|
231
|
+
const sessionId = generateSessionId(userId)
|
|
232
|
+
|
|
233
|
+
const result = await queryWithRLS<{ sessionId: string; createdAt: Date }>(
|
|
234
|
+
`INSERT INTO public."langchain_sessions"
|
|
235
|
+
(id, "userId", "teamId", "sessionId", messages, name)
|
|
236
|
+
VALUES (gen_random_uuid()::text, $1, $2, $3, '[]'::jsonb, $4)
|
|
237
|
+
RETURNING "sessionId", "createdAt"`,
|
|
238
|
+
[userId, teamId, sessionId, name || null],
|
|
239
|
+
userId
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return result[0]
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Rename a session
|
|
247
|
+
*/
|
|
248
|
+
renameSession: async (
|
|
249
|
+
sessionId: string,
|
|
250
|
+
name: string,
|
|
251
|
+
context: AgentContext
|
|
252
|
+
): Promise<void> => {
|
|
253
|
+
const { userId, teamId } = context
|
|
254
|
+
|
|
255
|
+
await mutateWithRLS(
|
|
256
|
+
`UPDATE public."langchain_sessions"
|
|
257
|
+
SET name = $4, "updatedAt" = now()
|
|
258
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
|
|
259
|
+
[userId, teamId, sessionId, name],
|
|
260
|
+
userId
|
|
261
|
+
)
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Toggle pin status of a session
|
|
266
|
+
*/
|
|
267
|
+
togglePinSession: async (
|
|
268
|
+
sessionId: string,
|
|
269
|
+
isPinned: boolean,
|
|
270
|
+
context: AgentContext
|
|
271
|
+
): Promise<void> => {
|
|
272
|
+
const { userId, teamId } = context
|
|
273
|
+
|
|
274
|
+
await mutateWithRLS(
|
|
275
|
+
`UPDATE public."langchain_sessions"
|
|
276
|
+
SET "isPinned" = $4, "updatedAt" = now()
|
|
277
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
|
|
278
|
+
[userId, teamId, sessionId, isPinned],
|
|
279
|
+
userId
|
|
280
|
+
)
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Count sessions for a user (for limit enforcement)
|
|
285
|
+
*/
|
|
286
|
+
countSessions: async (context: AgentContext): Promise<number> => {
|
|
287
|
+
const { userId, teamId } = context
|
|
288
|
+
|
|
289
|
+
const result = await queryWithRLS<{ count: string }>(
|
|
290
|
+
`SELECT COUNT(*)::text as count
|
|
291
|
+
FROM public."langchain_sessions"
|
|
292
|
+
WHERE "userId" = $1 AND "teamId" = $2
|
|
293
|
+
AND ("expiresAt" IS NULL OR "expiresAt" > now())`,
|
|
294
|
+
[userId, teamId],
|
|
295
|
+
userId
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return parseInt(result[0]?.count || '0', 10)
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get oldest session (for suggesting deletion when limit reached)
|
|
303
|
+
*/
|
|
304
|
+
getOldestSession: async (
|
|
305
|
+
context: AgentContext
|
|
306
|
+
): Promise<ConversationInfo | null> => {
|
|
307
|
+
const { userId, teamId } = context
|
|
308
|
+
|
|
309
|
+
const result = await queryWithRLS<SessionRow>(
|
|
310
|
+
`SELECT "sessionId", name, "isPinned", messages, "createdAt", "updatedAt"
|
|
311
|
+
FROM public."langchain_sessions"
|
|
312
|
+
WHERE "userId" = $1 AND "teamId" = $2
|
|
313
|
+
AND ("expiresAt" IS NULL OR "expiresAt" > now())
|
|
314
|
+
AND "isPinned" = false
|
|
315
|
+
ORDER BY "updatedAt" ASC
|
|
316
|
+
LIMIT 1`,
|
|
317
|
+
[userId, teamId],
|
|
318
|
+
userId
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if (!result.length) return null
|
|
322
|
+
|
|
323
|
+
const row = result[0]
|
|
324
|
+
return {
|
|
325
|
+
sessionId: row.sessionId,
|
|
326
|
+
name: row.name,
|
|
327
|
+
messageCount: row.messages.length,
|
|
328
|
+
firstMessage: getFirstHumanMessage(row.messages),
|
|
329
|
+
isPinned: row.isPinned,
|
|
330
|
+
createdAt: row.createdAt,
|
|
331
|
+
updatedAt: row.updatedAt,
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Clear/delete a session
|
|
337
|
+
*/
|
|
338
|
+
clearSession: async (
|
|
339
|
+
sessionId: string,
|
|
340
|
+
context: AgentContext
|
|
341
|
+
): Promise<void> => {
|
|
342
|
+
const { userId, teamId } = context
|
|
343
|
+
|
|
344
|
+
await mutateWithRLS(
|
|
345
|
+
`DELETE FROM public."langchain_sessions"
|
|
346
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
|
|
347
|
+
[userId, teamId, sessionId],
|
|
348
|
+
userId
|
|
349
|
+
)
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Clean up expired sessions (run periodically)
|
|
354
|
+
* Only cleans sessions with expiresAt set and expired.
|
|
355
|
+
* Sessions with expiresAt = NULL are never cleaned up.
|
|
356
|
+
*/
|
|
357
|
+
cleanup: async (): Promise<number> => {
|
|
358
|
+
const result = await query<{ id: string }>(
|
|
359
|
+
`DELETE FROM public."langchain_sessions"
|
|
360
|
+
WHERE "expiresAt" IS NOT NULL AND "expiresAt" < now()
|
|
361
|
+
RETURNING id`
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return result.rowCount
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get all sessions for a user in a team
|
|
369
|
+
* Returns sessions sorted by: pinned first, then by updatedAt desc
|
|
370
|
+
*/
|
|
371
|
+
listSessions: async (
|
|
372
|
+
context: AgentContext
|
|
373
|
+
): Promise<ConversationInfo[]> => {
|
|
374
|
+
const { userId, teamId } = context
|
|
375
|
+
|
|
376
|
+
const result = await queryWithRLS<SessionRow>(
|
|
377
|
+
`SELECT "sessionId", name, "isPinned", messages, "createdAt", "updatedAt"
|
|
378
|
+
FROM public."langchain_sessions"
|
|
379
|
+
WHERE "userId" = $1 AND "teamId" = $2
|
|
380
|
+
AND ("expiresAt" IS NULL OR "expiresAt" > now())
|
|
381
|
+
ORDER BY "isPinned" DESC, "updatedAt" DESC`,
|
|
382
|
+
[userId, teamId],
|
|
383
|
+
userId
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return result.map((row) => ({
|
|
387
|
+
sessionId: row.sessionId,
|
|
388
|
+
name: row.name,
|
|
389
|
+
messageCount: row.messages.length,
|
|
390
|
+
firstMessage: getFirstHumanMessage(row.messages),
|
|
391
|
+
isPinned: row.isPinned,
|
|
392
|
+
createdAt: row.createdAt,
|
|
393
|
+
updatedAt: row.updatedAt,
|
|
394
|
+
}))
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Extend session TTL (deprecated - sessions now have no expiration by default)
|
|
399
|
+
* @deprecated Use for legacy support only
|
|
400
|
+
*/
|
|
401
|
+
extendSession: async (
|
|
402
|
+
sessionId: string,
|
|
403
|
+
context: AgentContext,
|
|
404
|
+
ttlHours: number = 24
|
|
405
|
+
): Promise<void> => {
|
|
406
|
+
const { userId, teamId } = context
|
|
407
|
+
const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000)
|
|
408
|
+
|
|
409
|
+
await mutateWithRLS(
|
|
410
|
+
`UPDATE public."langchain_sessions"
|
|
411
|
+
SET "expiresAt" = $4, "updatedAt" = now()
|
|
412
|
+
WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
|
|
413
|
+
[userId, teamId, sessionId, expiresAt],
|
|
414
|
+
userId
|
|
415
|
+
)
|
|
416
|
+
},
|
|
417
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangGraph Orchestrator - Main Export (GENERIC)
|
|
3
|
+
*
|
|
4
|
+
* Explicit state machine for multi-agent orchestration.
|
|
5
|
+
* Replaces inefficient ReAct loops with deterministic graph flow.
|
|
6
|
+
*
|
|
7
|
+
* GENERIC ARCHITECTURE:
|
|
8
|
+
* - Plugin exports generic interfaces (AgentTool, OrchestratorConfig)
|
|
9
|
+
* - Theme provides tool implementations and configuration
|
|
10
|
+
* - No hardcoded entity knowledge in plugin
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Generic types (NEW)
|
|
14
|
+
export type {
|
|
15
|
+
AgentTool,
|
|
16
|
+
OrchestratorConfig,
|
|
17
|
+
GenericHandlerResult,
|
|
18
|
+
} from './types'
|
|
19
|
+
|
|
20
|
+
// Core types
|
|
21
|
+
export type {
|
|
22
|
+
OrchestratorState,
|
|
23
|
+
Intent,
|
|
24
|
+
IntentType,
|
|
25
|
+
IntentAction,
|
|
26
|
+
RouterOutput,
|
|
27
|
+
HandlerResults,
|
|
28
|
+
GraphConfig,
|
|
29
|
+
RouterRoute,
|
|
30
|
+
PostHandlerRoute,
|
|
31
|
+
HandlerNodeFn,
|
|
32
|
+
ModelConfig,
|
|
33
|
+
SystemIntentType,
|
|
34
|
+
} from './types'
|
|
35
|
+
|
|
36
|
+
// Deprecated types (backward compatibility)
|
|
37
|
+
export type {
|
|
38
|
+
TaskHandlerResult,
|
|
39
|
+
CustomerHandlerResult,
|
|
40
|
+
PageHandlerResult,
|
|
41
|
+
TaskData,
|
|
42
|
+
CustomerData,
|
|
43
|
+
PageData,
|
|
44
|
+
HandlerFactories,
|
|
45
|
+
} from './types'
|
|
46
|
+
|
|
47
|
+
export { createInitialState, DEFAULT_GRAPH_CONFIG } from './types'
|
|
48
|
+
|
|
49
|
+
// Graph
|
|
50
|
+
export { createOrchestratorGraph, invokeOrchestrator } from './orchestrator-graph'
|
|
51
|
+
|
|
52
|
+
// Router node factory (NEW - replaces direct routerNode export)
|
|
53
|
+
export { createRouterNode } from './nodes/router'
|
|
54
|
+
|
|
55
|
+
// Core nodes
|
|
56
|
+
export { combinerNode } from './nodes/combiner'
|
|
57
|
+
|
|
58
|
+
// Handler nodes are theme-specific - themes must provide implementations via OrchestratorConfig
|