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:
@@ -114,6 +114,7 @@ func routes(logger *slog.Logger, cfg *config.Config, db *store.DB, provider ai.P
|
||||
mcpHandler := mcpserver.New(cfg.MCP, toolSet)
|
||||
mux.Handle(cfg.MCP.Path, auth.Middleware(cfg.Auth, keyring, logger)(mcpHandler))
|
||||
mux.HandleFunc("/favicon.ico", serveFavicon)
|
||||
mux.HandleFunc("/llm", serveLLMInstructions)
|
||||
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
22
internal/app/llm.go
Normal file
22
internal/app/llm.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
amcsllm "git.warky.dev/wdevs/amcs/llm"
|
||||
)
|
||||
|
||||
func serveLLMInstructions(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/llm" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(amcsllm.MemoryInstructions)
|
||||
}
|
||||
29
internal/app/llm_test.go
Normal file
29
internal/app/llm_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
amcsllm "git.warky.dev/wdevs/amcs/llm"
|
||||
)
|
||||
|
||||
func TestServeLLMInstructions(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/llm", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveLLMInstructions(rec, req)
|
||||
|
||||
res := rec.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK)
|
||||
}
|
||||
if got := res.Header.Get("Content-Type"); got != "text/markdown; charset=utf-8" {
|
||||
t.Fatalf("content-type = %q, want %q", got, "text/markdown; charset=utf-8")
|
||||
}
|
||||
if body := rec.Body.String(); body != string(amcsllm.MemoryInstructions) {
|
||||
t.Fatalf("body = %q, want embedded instructions", body)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user