@lota-sdk/core 0.1.5
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/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- package/src/workers/worker-utils.ts +182 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { NotFoundError } from '../utils/errors'
|
|
4
|
+
import { databaseService as defaultDatabaseService } from './service'
|
|
5
|
+
import type { SurrealDBService } from './service'
|
|
6
|
+
import type { DatabaseTable } from './tables'
|
|
7
|
+
|
|
8
|
+
export abstract class BaseService<T extends z.ZodType> {
|
|
9
|
+
protected readonly db: SurrealDBService
|
|
10
|
+
constructor(
|
|
11
|
+
protected readonly table: DatabaseTable,
|
|
12
|
+
protected readonly schema: T,
|
|
13
|
+
protected readonly databaseService: SurrealDBService = defaultDatabaseService,
|
|
14
|
+
) {
|
|
15
|
+
this.db = this.databaseService
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findById(id: unknown): Promise<z.infer<T> | null> {
|
|
19
|
+
return this.databaseService.findOne(this.table, { id }, this.schema)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getById(id: unknown): Promise<z.infer<T>> {
|
|
23
|
+
const record = await this.findById(id)
|
|
24
|
+
if (!record) {
|
|
25
|
+
throw new NotFoundError(`${this.table} record not found`)
|
|
26
|
+
}
|
|
27
|
+
return record
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findAll(
|
|
31
|
+
filter: Record<string, unknown> = {},
|
|
32
|
+
options?: { limit?: number; offset?: number; orderBy?: keyof z.infer<T> & string; orderDir?: 'ASC' | 'DESC' },
|
|
33
|
+
): Promise<z.infer<T>[]> {
|
|
34
|
+
return this.databaseService.findMany(this.table, filter, this.schema, options)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async create(data: Record<string, unknown>): Promise<z.infer<T>> {
|
|
38
|
+
return this.databaseService.create(this.table, data, this.schema)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async update(id: unknown, data: Record<string, unknown>): Promise<z.infer<T>> {
|
|
42
|
+
const updated = await this.databaseService.update(this.table, id, data, this.schema)
|
|
43
|
+
if (!updated) {
|
|
44
|
+
throw new NotFoundError(`${this.table} record not found`)
|
|
45
|
+
}
|
|
46
|
+
return updated
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async delete(id: unknown): Promise<void> {
|
|
50
|
+
const deleted = await this.databaseService.deleteById(this.table, id)
|
|
51
|
+
if (!deleted) {
|
|
52
|
+
throw new NotFoundError(`${this.table} record not found`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { toTimestamp } from '@lota-sdk/shared/runtime/chat-message-metadata'
|
|
2
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
3
|
+
import type { BoundQuery, RecordId } from 'surrealdb'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import type { RecordIdRef } from './record-id'
|
|
7
|
+
import { databaseService } from './service'
|
|
8
|
+
import type { DatabaseTable } from './tables'
|
|
9
|
+
|
|
10
|
+
export const CursorRowSchema = z.object({ createdAt: z.union([z.date(), z.string(), z.number()]) })
|
|
11
|
+
|
|
12
|
+
export interface MessageHistoryPage {
|
|
13
|
+
messages: ChatMessage[]
|
|
14
|
+
hasMore: boolean
|
|
15
|
+
prevCursor: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CursorPaginationConfig {
|
|
19
|
+
table: DatabaseTable
|
|
20
|
+
parentFilterField: string
|
|
21
|
+
toRowId: (parentId: RecordIdRef, messageId: string) => RecordId
|
|
22
|
+
parseRow: (row: unknown) => { messageId: string }
|
|
23
|
+
toMessage: (row: unknown) => ChatMessage
|
|
24
|
+
queryLatest: (parentId: RecordIdRef, limit: number) => BoundQuery
|
|
25
|
+
queryBefore: (parentId: RecordIdRef, cursorCreatedAt: Date, cursorId: RecordId, limit: number) => BoundQuery
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function listMessageHistoryPage(
|
|
29
|
+
config: CursorPaginationConfig,
|
|
30
|
+
params: { parentId: RecordIdRef; take: number; beforeMessageId?: string },
|
|
31
|
+
): Promise<MessageHistoryPage> {
|
|
32
|
+
const take = Math.max(1, params.take)
|
|
33
|
+
const beforeMessageId = params.beforeMessageId?.trim()
|
|
34
|
+
const limit = take + 1
|
|
35
|
+
|
|
36
|
+
const rows = beforeMessageId
|
|
37
|
+
? await listRowsBefore(config, { parentId: params.parentId, beforeMessageId, take: limit })
|
|
38
|
+
: await databaseService.query<unknown>(config.queryLatest(params.parentId, limit))
|
|
39
|
+
|
|
40
|
+
const parsedRows = rows.map((row) => config.parseRow(row))
|
|
41
|
+
const hasMore = parsedRows.length > take
|
|
42
|
+
const pageRows = hasMore ? rows.slice(0, take) : rows
|
|
43
|
+
|
|
44
|
+
pageRows.reverse()
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
messages: pageRows.map((row) => config.toMessage(row)),
|
|
48
|
+
hasMore,
|
|
49
|
+
prevCursor: hasMore ? config.parseRow(pageRows[0]).messageId : null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function listRowsBefore(
|
|
54
|
+
config: CursorPaginationConfig,
|
|
55
|
+
params: { parentId: RecordIdRef; beforeMessageId: string; take: number },
|
|
56
|
+
): Promise<unknown[]> {
|
|
57
|
+
const cursorRow = await databaseService.findOne(
|
|
58
|
+
config.table,
|
|
59
|
+
{ [config.parentFilterField]: params.parentId, messageId: params.beforeMessageId },
|
|
60
|
+
CursorRowSchema,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (!cursorRow) {
|
|
64
|
+
throw new Error(`Cursor message not found in ${config.table}: ${params.beforeMessageId}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt))
|
|
68
|
+
const cursorId = config.toRowId(params.parentId, params.beforeMessageId)
|
|
69
|
+
|
|
70
|
+
return await databaseService.query<unknown>(
|
|
71
|
+
config.queryBefore(params.parentId, cursorCreatedAt, cursorId, params.take),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query builder for SurrealDB memory operations
|
|
3
|
+
* Builds type-safe SQL queries with proper parameterization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { validateKnnLimit } from '../config/constants'
|
|
7
|
+
import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '../config/search'
|
|
8
|
+
import { TABLES } from './tables'
|
|
9
|
+
|
|
10
|
+
const MEMORY_TABLE = TABLES.MEMORY
|
|
11
|
+
|
|
12
|
+
interface VectorSearchParams {
|
|
13
|
+
embedding: number[]
|
|
14
|
+
scopeId: string
|
|
15
|
+
limit: number
|
|
16
|
+
memoryType?: string
|
|
17
|
+
pointInTime?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface HybridSearchParams extends VectorSearchParams {
|
|
21
|
+
query: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface LinearSearchParams extends HybridSearchParams {
|
|
25
|
+
weights: [number, number]
|
|
26
|
+
normalization: 'minmax' | 'zscore' | 'none'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class MemoryQueryBuilder {
|
|
30
|
+
private buildTypeFilter(memoryType?: string): string {
|
|
31
|
+
return memoryType ? 'AND memoryType = $memoryType' : ''
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private buildTemporalPredicate(pointInTime?: string): string {
|
|
35
|
+
if (pointInTime) {
|
|
36
|
+
return 'archivedAt IS NONE AND validFrom <= $pointInTime AND IF validUntil IS NONE { true } ELSE { validUntil > $pointInTime }'
|
|
37
|
+
}
|
|
38
|
+
return 'archivedAt IS NONE AND IF validUntil IS NONE { true } ELSE { validUntil > time::now() }'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private buildTemporalFilter(pointInTime?: string): string {
|
|
42
|
+
if (pointInTime) {
|
|
43
|
+
return 'AND archivedAt IS NONE AND validFrom <= $pointInTime AND IF validUntil IS NONE { true } ELSE { validUntil > $pointInTime }'
|
|
44
|
+
}
|
|
45
|
+
return 'AND archivedAt IS NONE AND IF validUntil IS NONE { true } ELSE { validUntil > time::now() }'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build vector similarity search query
|
|
50
|
+
*/
|
|
51
|
+
buildVectorSearch(params: VectorSearchParams): { sql: string; bindVars: Record<string, unknown> } {
|
|
52
|
+
const typeFilter = this.buildTypeFilter(params.memoryType)
|
|
53
|
+
const temporalPredicate = this.buildTemporalPredicate(params.pointInTime)
|
|
54
|
+
const fetchLimit = validateKnnLimit(params.limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER)
|
|
55
|
+
|
|
56
|
+
const sql = `
|
|
57
|
+
LET $vectorCandidates = (
|
|
58
|
+
SELECT
|
|
59
|
+
id,
|
|
60
|
+
content,
|
|
61
|
+
metadata,
|
|
62
|
+
archivedAt,
|
|
63
|
+
validFrom,
|
|
64
|
+
validUntil,
|
|
65
|
+
vector::distance::knn() AS distance
|
|
66
|
+
FROM ${MEMORY_TABLE}
|
|
67
|
+
WHERE scopeId = $scopeId ${typeFilter}
|
|
68
|
+
AND embedding <|${fetchLimit}|> $embedding
|
|
69
|
+
ORDER BY distance ASC
|
|
70
|
+
LIMIT $fetchLimit
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
SELECT
|
|
74
|
+
id,
|
|
75
|
+
content,
|
|
76
|
+
metadata,
|
|
77
|
+
distance
|
|
78
|
+
FROM $vectorCandidates
|
|
79
|
+
WHERE ${temporalPredicate}
|
|
80
|
+
ORDER BY distance ASC
|
|
81
|
+
LIMIT $fetchLimit
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
sql,
|
|
86
|
+
bindVars: {
|
|
87
|
+
embedding: params.embedding,
|
|
88
|
+
scopeId: params.scopeId,
|
|
89
|
+
memoryType: params.memoryType,
|
|
90
|
+
pointInTime: params.pointInTime,
|
|
91
|
+
fetchLimit,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build hybrid search query using RRF (Reciprocal Rank Fusion)
|
|
98
|
+
* Combines vector similarity + full-text search
|
|
99
|
+
*/
|
|
100
|
+
buildHybridSearch(params: HybridSearchParams): { sql: string; bindVars: Record<string, unknown> } {
|
|
101
|
+
const typeFilter = this.buildTypeFilter(params.memoryType)
|
|
102
|
+
const temporalFilter = this.buildTemporalFilter(params.pointInTime)
|
|
103
|
+
const fetchLimit = validateKnnLimit(params.limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER)
|
|
104
|
+
|
|
105
|
+
const sql = `
|
|
106
|
+
LET $vs = (
|
|
107
|
+
SELECT
|
|
108
|
+
id,
|
|
109
|
+
content,
|
|
110
|
+
metadata,
|
|
111
|
+
vector::distance::knn() AS distance,
|
|
112
|
+
(1 / (1 + vector::distance::knn())) AS score
|
|
113
|
+
FROM ${MEMORY_TABLE}
|
|
114
|
+
WHERE scopeId = $scopeId ${typeFilter} ${temporalFilter}
|
|
115
|
+
AND embedding <|${fetchLimit}|> $embedding
|
|
116
|
+
ORDER BY distance ASC
|
|
117
|
+
LIMIT $fetchLimit
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
LET $ft_raw = (
|
|
121
|
+
SELECT id, content, metadata, search::score(1) AS score
|
|
122
|
+
FROM ${MEMORY_TABLE}
|
|
123
|
+
WHERE scopeId = $scopeId ${typeFilter} ${temporalFilter}
|
|
124
|
+
AND content @1@ $query
|
|
125
|
+
ORDER BY score DESC
|
|
126
|
+
LIMIT $fetchLimit
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
LET $ft = IF array::len($ft_raw) > 0 { $ft_raw } ELSE { (SELECT id, content, metadata, 0 AS score FROM $vs) };
|
|
130
|
+
|
|
131
|
+
SELECT id, content, metadata, rrf_score AS rrfScore
|
|
132
|
+
FROM search::rrf([$vs, $ft], ${fetchLimit}, 60)
|
|
133
|
+
ORDER BY rrfScore DESC
|
|
134
|
+
LIMIT $fetchLimit
|
|
135
|
+
`
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
sql,
|
|
139
|
+
bindVars: {
|
|
140
|
+
embedding: params.embedding,
|
|
141
|
+
query: params.query,
|
|
142
|
+
scopeId: params.scopeId,
|
|
143
|
+
memoryType: params.memoryType,
|
|
144
|
+
pointInTime: params.pointInTime,
|
|
145
|
+
fetchLimit,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build linear combination search query
|
|
152
|
+
* Allows weighted combination of vector + text scores
|
|
153
|
+
*/
|
|
154
|
+
buildLinearSearch(params: LinearSearchParams): { sql: string; bindVars: Record<string, unknown> } {
|
|
155
|
+
const typeFilter = this.buildTypeFilter(params.memoryType)
|
|
156
|
+
const temporalFilter = this.buildTemporalFilter(params.pointInTime)
|
|
157
|
+
const fetchLimit = validateKnnLimit(params.limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER)
|
|
158
|
+
|
|
159
|
+
const sql = `
|
|
160
|
+
LET $vs = (
|
|
161
|
+
SELECT
|
|
162
|
+
id,
|
|
163
|
+
content,
|
|
164
|
+
metadata,
|
|
165
|
+
vector::distance::knn() AS distance,
|
|
166
|
+
(1 / (1 + vector::distance::knn())) AS score
|
|
167
|
+
FROM ${MEMORY_TABLE}
|
|
168
|
+
WHERE scopeId = $scopeId ${typeFilter} ${temporalFilter}
|
|
169
|
+
AND embedding <|${fetchLimit}|> $embedding
|
|
170
|
+
ORDER BY distance ASC
|
|
171
|
+
LIMIT $fetchLimit
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
LET $ft_raw = (
|
|
175
|
+
SELECT id, content, metadata, search::score(1) AS score
|
|
176
|
+
FROM ${MEMORY_TABLE}
|
|
177
|
+
WHERE scopeId = $scopeId ${typeFilter} ${temporalFilter}
|
|
178
|
+
AND content @1@ $query
|
|
179
|
+
ORDER BY score DESC
|
|
180
|
+
LIMIT $fetchLimit
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
LET $ft = IF array::len($ft_raw) > 0 { $ft_raw } ELSE { (SELECT id, content, metadata, 0 AS score FROM $vs) };
|
|
184
|
+
|
|
185
|
+
SELECT id, content, metadata, linear_score AS linearScore
|
|
186
|
+
FROM search::linear([$vs, $ft], $weights, ${fetchLimit}, $normalization)
|
|
187
|
+
ORDER BY linearScore DESC
|
|
188
|
+
LIMIT $fetchLimit
|
|
189
|
+
`
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
sql,
|
|
193
|
+
bindVars: {
|
|
194
|
+
embedding: params.embedding,
|
|
195
|
+
query: params.query,
|
|
196
|
+
scopeId: params.scopeId,
|
|
197
|
+
memoryType: params.memoryType,
|
|
198
|
+
pointInTime: params.pointInTime,
|
|
199
|
+
weights: params.weights,
|
|
200
|
+
normalization: params.normalization,
|
|
201
|
+
fetchLimit,
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const memoryQueryBuilder = new MemoryQueryBuilder()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import type { BasicSearchRow, SurrealMemoryRow } from './memory-store.rows'
|
|
4
|
+
import type { MemoryRecord, MemorySearchResult } from './memory-types'
|
|
5
|
+
import { recordIdToString } from './record-id'
|
|
6
|
+
import { TABLES } from './tables'
|
|
7
|
+
|
|
8
|
+
export function isUniqueIndexConflict(error: unknown, indexName: string): boolean {
|
|
9
|
+
if (!(error instanceof Error)) return false
|
|
10
|
+
const message = error.message
|
|
11
|
+
return message.includes(indexName) && message.includes('already contains')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const coerceDate = (value: unknown): Date => {
|
|
15
|
+
if (value instanceof Date) return value
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
const millis = value < 1_000_000_000_000 ? value * 1000 : value
|
|
18
|
+
return new Date(millis)
|
|
19
|
+
}
|
|
20
|
+
return new Date(String(value))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mapRowToMemoryRecord(row: SurrealMemoryRow): MemoryRecord {
|
|
24
|
+
return {
|
|
25
|
+
id: recordIdToString(row.id, TABLES.MEMORY),
|
|
26
|
+
content: row.content,
|
|
27
|
+
embedding: row.embedding,
|
|
28
|
+
hash: row.hash,
|
|
29
|
+
scopeId: row.scopeId,
|
|
30
|
+
memoryType: row.memoryType,
|
|
31
|
+
durability: row.durability,
|
|
32
|
+
metadata: row.metadata,
|
|
33
|
+
importance: row.importance,
|
|
34
|
+
accessCount: row.accessCount,
|
|
35
|
+
needsReview: row.needsReview,
|
|
36
|
+
lastAccessedAt: row.lastAccessedAt !== undefined ? coerceDate(row.lastAccessedAt) : undefined,
|
|
37
|
+
createdAt: coerceDate(row.createdAt),
|
|
38
|
+
updatedAt: row.updatedAt !== undefined ? coerceDate(row.updatedAt) : undefined,
|
|
39
|
+
validFrom: coerceDate(row.validFrom),
|
|
40
|
+
validUntil: row.validUntil !== undefined ? coerceDate(row.validUntil) : undefined,
|
|
41
|
+
archivedAt: row.archivedAt !== undefined ? coerceDate(row.archivedAt) : undefined,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function calculateAdjustedScore(options: {
|
|
46
|
+
baseScore: number
|
|
47
|
+
supportCount: number
|
|
48
|
+
contradictCount: number
|
|
49
|
+
supportMultiplier: number
|
|
50
|
+
contradictMultiplier: number
|
|
51
|
+
}): number {
|
|
52
|
+
const supportBoost = options.supportCount * options.supportMultiplier
|
|
53
|
+
const contradictPenalty = options.contradictCount * options.contradictMultiplier
|
|
54
|
+
return Math.max(0, options.baseScore + supportBoost - contradictPenalty)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RelationCounts {
|
|
58
|
+
supersedeCount: number
|
|
59
|
+
supportCount: number
|
|
60
|
+
contradictCount: number
|
|
61
|
+
contradictions: string[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function processGraphAwareRows<T extends BasicSearchRow>(
|
|
65
|
+
rows: T[],
|
|
66
|
+
relationCounts: Map<string, RelationCounts>,
|
|
67
|
+
limit: number,
|
|
68
|
+
baseScore: (row: T) => number,
|
|
69
|
+
multipliers: { support: number; contradict: number },
|
|
70
|
+
minimumScore: number,
|
|
71
|
+
): MemorySearchResult[] {
|
|
72
|
+
return rows
|
|
73
|
+
.filter((row) => {
|
|
74
|
+
const id = recordIdToString(row.id, TABLES.MEMORY)
|
|
75
|
+
const counts = relationCounts.get(id)
|
|
76
|
+
return !counts || counts.supersedeCount === 0
|
|
77
|
+
})
|
|
78
|
+
.map((row) => {
|
|
79
|
+
const id = recordIdToString(row.id, TABLES.MEMORY)
|
|
80
|
+
const counts = relationCounts.get(id) ?? {
|
|
81
|
+
supportCount: 0,
|
|
82
|
+
contradictCount: 0,
|
|
83
|
+
supersedeCount: 0,
|
|
84
|
+
contradictions: [],
|
|
85
|
+
}
|
|
86
|
+
const score = calculateAdjustedScore({
|
|
87
|
+
baseScore: baseScore(row),
|
|
88
|
+
supportCount: counts.supportCount,
|
|
89
|
+
contradictCount: counts.contradictCount,
|
|
90
|
+
supportMultiplier: multipliers.support,
|
|
91
|
+
contradictMultiplier: multipliers.contradict,
|
|
92
|
+
})
|
|
93
|
+
const relationCount = counts.supportCount + counts.contradictCount + counts.supersedeCount
|
|
94
|
+
const result: MemorySearchResult = {
|
|
95
|
+
id,
|
|
96
|
+
content: row.content,
|
|
97
|
+
score,
|
|
98
|
+
metadata: {
|
|
99
|
+
...row.metadata,
|
|
100
|
+
supportCount: counts.supportCount,
|
|
101
|
+
contradictCount: counts.contradictCount,
|
|
102
|
+
supersedeCount: counts.supersedeCount,
|
|
103
|
+
relationCount,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
if (counts.contradictions.length > 0) {
|
|
107
|
+
result.contradictions = counts.contradictions
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
})
|
|
111
|
+
.filter((result) => result.score >= minimumScore)
|
|
112
|
+
.sort((a, b) => b.score - a.score)
|
|
113
|
+
.slice(0, limit)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function hashContent(content: string, scopeId: string, memoryType: string): string {
|
|
117
|
+
return createHash('sha256').update(`${scopeId}:${memoryType}:${content}`).digest('hex')
|
|
118
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Durability, MemoryType } from './memory-types'
|
|
2
|
+
import type { RecordIdInput } from './record-id'
|
|
3
|
+
|
|
4
|
+
export interface SurrealMemoryRow {
|
|
5
|
+
id: RecordIdInput
|
|
6
|
+
content: string
|
|
7
|
+
embedding: number[]
|
|
8
|
+
hash: string
|
|
9
|
+
scopeId: string
|
|
10
|
+
memoryType: MemoryType
|
|
11
|
+
durability: Durability
|
|
12
|
+
metadata: Record<string, unknown>
|
|
13
|
+
importance: number
|
|
14
|
+
accessCount: number
|
|
15
|
+
needsReview: boolean
|
|
16
|
+
lastAccessedAt?: Date | string
|
|
17
|
+
createdAt: Date | string
|
|
18
|
+
updatedAt?: Date | string
|
|
19
|
+
validFrom: Date | string
|
|
20
|
+
validUntil?: Date | string
|
|
21
|
+
archivedAt?: Date | string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BasicSearchRow {
|
|
25
|
+
id: RecordIdInput
|
|
26
|
+
content: string
|
|
27
|
+
metadata: Record<string, unknown>
|
|
28
|
+
createdAt?: Date | string
|
|
29
|
+
}
|