feat(llm): add LLM integration instructions and handler

* Serve LLM instructions at `/llm`
* Include markdown content for memory instructions
* Update README with LLM integration details
* Add tests for LLM instructions handler
* Modify database migrations to use GUIDs for thoughts and projects
This commit is contained in:
Hein
2026-03-25 18:02:42 +02:00
parent cebef3a07c
commit 8d0a91a961
16 changed files with 600 additions and 41 deletions

View File

@@ -13,7 +13,9 @@ import (
func (db *DB) InsertLink(ctx context.Context, link thoughttypes.ThoughtLink) error {
_, err := db.pool.Exec(ctx, `
insert into thought_links (from_id, to_id, relation)
values ($1, $2, $3)
select f.id, t.id, $3
from thoughts f, thoughts t
where f.guid = $1 and t.guid = $2
`, link.FromID, link.ToID, link.Relation)
if err != nil {
return fmt.Errorf("insert link: %w", err)
@@ -23,15 +25,15 @@ func (db *DB) InsertLink(ctx context.Context, link thoughttypes.ThoughtLink) err
func (db *DB) LinkedThoughts(ctx context.Context, thoughtID uuid.UUID) ([]thoughttypes.LinkedThought, error) {
rows, err := db.pool.Query(ctx, `
select t.id, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'outgoing' as direction, l.created_at
select t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'outgoing' as direction, l.created_at
from thought_links l
join thoughts t on t.id = l.to_id
where l.from_id = $1
where l.from_id = (select id from thoughts where guid = $1)
union all
select t.id, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'incoming' as direction, l.created_at
select t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'incoming' as direction, l.created_at
from thought_links l
join thoughts t on t.id = l.from_id
where l.to_id = $1
where l.to_id = (select id from thoughts where guid = $1)
order by created_at desc
`, thoughtID)
if err != nil {

View File

@@ -15,7 +15,7 @@ func (db *DB) CreateProject(ctx context.Context, name, description string) (thou
row := db.pool.QueryRow(ctx, `
insert into projects (name, description)
values ($1, $2)
returning id, name, description, created_at, last_active_at
returning guid, name, description, created_at, last_active_at
`, name, description)
var project thoughttypes.Project
@@ -29,13 +29,13 @@ func (db *DB) GetProject(ctx context.Context, nameOrID string) (thoughttypes.Pro
var row pgx.Row
if parsedID, err := uuid.Parse(strings.TrimSpace(nameOrID)); err == nil {
row = db.pool.QueryRow(ctx, `
select id, name, description, created_at, last_active_at
select guid, name, description, created_at, last_active_at
from projects
where id = $1
where guid = $1
`, parsedID)
} else {
row = db.pool.QueryRow(ctx, `
select id, name, description, created_at, last_active_at
select guid, name, description, created_at, last_active_at
from projects
where name = $1
`, strings.TrimSpace(nameOrID))
@@ -53,10 +53,10 @@ func (db *DB) GetProject(ctx context.Context, nameOrID string) (thoughttypes.Pro
func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary, error) {
rows, err := db.pool.Query(ctx, `
select p.id, p.name, p.description, p.created_at, p.last_active_at, count(t.id) as thought_count
select p.guid, p.name, p.description, p.created_at, p.last_active_at, count(t.id) as thought_count
from projects p
left join thoughts t on t.project_id = p.id and t.archived_at is null
group by p.id
left join thoughts t on t.project_id = p.guid and t.archived_at is null
group by p.guid, p.name, p.description, p.created_at, p.last_active_at
order by p.last_active_at desc, p.created_at desc
`)
if err != nil {
@@ -79,7 +79,7 @@ func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary,
}
func (db *DB) TouchProject(ctx context.Context, id uuid.UUID) error {
tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`, id)
tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where guid = $1`, id)
if err != nil {
return fmt.Errorf("touch project: %w", err)
}

View File

@@ -30,7 +30,7 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e
row := tx.QueryRow(ctx, `
insert into thoughts (content, metadata, project_id)
values ($1, $2::jsonb, $3)
returning id, created_at, updated_at
returning guid, created_at, updated_at
`, thought.Content, metadata, thought.ProjectID)
created := thought
@@ -123,7 +123,7 @@ func (db *DB) ListThoughts(ctx context.Context, filter thoughttypes.ListFilter)
}
query := `
select id, content, metadata, project_id, archived_at, created_at, updated_at
select guid, content, metadata, project_id, archived_at, created_at, updated_at
from thoughts
`
if len(conditions) > 0 {
@@ -209,9 +209,9 @@ func (db *DB) Stats(ctx context.Context) (thoughttypes.ThoughtStats, error) {
func (db *DB) GetThought(ctx context.Context, id uuid.UUID) (thoughttypes.Thought, error) {
row := db.pool.QueryRow(ctx, `
select id, content, metadata, project_id, archived_at, created_at, updated_at
select guid, content, metadata, project_id, archived_at, created_at, updated_at
from thoughts
where id = $1
where guid = $1
`, id)
var thought thoughttypes.Thought
@@ -248,7 +248,7 @@ func (db *DB) UpdateThought(ctx context.Context, id uuid.UUID, content string, e
metadata = $3::jsonb,
project_id = $4,
updated_at = now()
where id = $1
where guid = $1
`, id, content, metadataBytes, projectID)
if err != nil {
return thoughttypes.Thought{}, fmt.Errorf("update thought: %w", err)
@@ -278,7 +278,7 @@ func (db *DB) UpdateThought(ctx context.Context, id uuid.UUID, content string, e
}
func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
tag, err := db.pool.Exec(ctx, `delete from thoughts where id = $1`, id)
tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`, id)
if err != nil {
return fmt.Errorf("delete thought: %w", err)
}
@@ -289,7 +289,7 @@ func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
}
func (db *DB) ArchiveThought(ctx context.Context, id uuid.UUID) error {
tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where id = $1`, id)
tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`, id)
if err != nil {
return fmt.Errorf("archive thought: %w", err)
}
@@ -322,14 +322,14 @@ func (db *DB) SearchSimilarThoughts(ctx context.Context, embedding []float32, em
}
if excludeID != nil {
args = append(args, *excludeID)
conditions = append(conditions, fmt.Sprintf("t.id <> $%d", len(args)))
conditions = append(conditions, fmt.Sprintf("t.guid <> $%d", len(args)))
}
args = append(args, limit)
query := `
select t.id, t.content, t.metadata, 1 - (e.embedding <=> $1) as similarity, t.created_at
select t.guid, t.content, t.metadata, 1 - (e.embedding <=> $1) as similarity, t.created_at
from thoughts t
join embeddings e on e.thought_id = t.id
join embeddings e on e.thought_id = t.guid
where ` + strings.Join(conditions, " and ") + fmt.Sprintf(`
order by e.embedding <=> $1
limit $%d`, len(args))