diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml deleted file mode 100644 index b672dea..0000000 --- a/.idea/data_source_mapping.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/main.go b/main.go index 0672e88..bf22092 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,26 @@ func main() { }) r.GET("/db.yaml", func(c *gin.Context) { c.File("db.yaml") }) + // Add knowledgeModel save endpoint (replaces snapshot) + r.POST("/admin/chats/snapshot", func(c *gin.Context) { + if repo == nil { + c.JSON(500, gin.H{"error": "repository not configured"}) + return + } + var req struct { + Text string `json:"text"` + } + if err := c.BindJSON(&req); err != nil || req.Text == "" { + c.JSON(400, gin.H{"error": "invalid request"}) + return + } + if err := repo.SaveKnowledgeModel(c.Request.Context(), req.Text); err != nil { + c.JSON(500, gin.H{"error": "failed to save snapshot"}) + return + } + c.JSON(200, gin.H{"status": "ok"}) + }) + // JSON: list chat interactions r.GET("/admin/chats", func(c *gin.Context) { if repo == nil { @@ -143,5 +163,39 @@ func main() { } }) + // List knowledgeModel entries (replaces snapshots) + r.GET("/admin/chats/snapshots", func(c *gin.Context) { + if repo == nil { + c.JSON(200, gin.H{"items": []interface{}{}}) + return + } + models, err := repo.ListKnowledgeModels(c.Request.Context(), 100, 0) + if err != nil { + c.JSON(500, gin.H{"error": "failed to list snapshots"}) + return + } + c.JSON(200, gin.H{"items": models}) + }) + + // Get a specific knowledgeModel entry (replaces snapshot) + r.GET("/admin/chats/snapshot/:id", func(c *gin.Context) { + if repo == nil { + c.JSON(404, gin.H{"error": "repository not configured"}) + return + } + idStr := c.Param("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + c.JSON(400, gin.H{"error": "invalid id"}) + return + } + text, err := repo.GetKnowledgeModelText(c.Request.Context(), id) + if err != nil { + c.JSON(404, gin.H{"error": "snapshot not found"}) + return + } + c.Data(200, "text/yaml; charset=utf-8", []byte(text)) + }) + r.Run(":8080") } diff --git a/repository.go b/repository.go index 959ced1..070cf1d 100644 --- a/repository.go +++ b/repository.go @@ -31,6 +31,9 @@ type ChatRepositoryAPI interface { ListChatInteractions(ctx context.Context, limit, offset int) ([]ChatInteraction, error) SaveLLMRawEvent(ctx context.Context, correlationID, phase, raw string) error ListLLMRawEvents(ctx context.Context, correlationID string, limit, offset int) ([]RawLLMEvent, error) + SaveKnowledgeModel(ctx context.Context, text string) error + ListKnowledgeModels(ctx context.Context, limit, offset int) ([]knowledgeModelMeta, error) + GetKnowledgeModelText(ctx context.Context, id int64) (string, error) } // RawLLMEvent represents a stored raw LLM exchange phase @@ -41,6 +44,13 @@ type RawLLMEvent struct { CreatedAt time.Time `json:"created_at"` } +// knowledgeModelMeta is used for listing knowledgeModel metadata +// (exported for use in interface, but can be unexported if not needed outside package) +type knowledgeModelMeta struct { + ID int64 `json:"id"` + CreatedAt time.Time `json:"created_at"` +} + // PGChatRepository is a PostgreSQL implementation using pgxpool type PGChatRepository struct { pool *pgxpool.Pool @@ -94,6 +104,15 @@ func (r *PGChatRepository) ensureSchema(ctx context.Context) error { if _, err := r.pool.Exec(ctx, ddlRaw); err != nil { return err } + // Add knowledgeModel table + ddlKnowledgeModel := `CREATE TABLE IF NOT EXISTS knowledgeModel ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + knowledge_text TEXT NOT NULL + );` + if _, err := r.pool.Exec(ctx, ddlKnowledgeModel); err != nil { + return err + } // Legacy column cleanup (ignore errors) for _, drop := range []string{ "ALTER TABLE chat_interactions DROP COLUMN IF EXISTS raw_keywords_json", @@ -205,6 +224,58 @@ func (r *PGChatRepository) ListLLMRawEvents(ctx context.Context, correlationID s return out, rows.Err() } +// SaveKnowledgeModel inserts a new knowledgeModel entry +func (r *PGChatRepository) SaveKnowledgeModel(ctx context.Context, text string) error { + if r == nil || r.pool == nil { + return nil + } + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + _, err := r.pool.Exec(ctx, `INSERT INTO knowledgeModel (knowledge_text) VALUES ($1)`, text) + return err +} + +// ListKnowledgeModels returns a list of knowledgeModel metadata (id, created_at) +func (r *PGChatRepository) ListKnowledgeModels(ctx context.Context, limit, offset int) ([]knowledgeModelMeta, error) { + if r == nil || r.pool == nil { + return nil, nil + } + if limit <= 0 || limit > 1000 { + limit = 100 + } + if offset < 0 { + offset = 0 + } + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + rows, err := r.pool.Query(ctx, `SELECT id, created_at FROM knowledgeModel ORDER BY created_at DESC LIMIT $1 OFFSET $2`, limit, offset) + if err != nil { + return nil, err + } + defer rows.Close() + var out []knowledgeModelMeta + for rows.Next() { + var s knowledgeModelMeta + if err := rows.Scan(&s.ID, &s.CreatedAt); err != nil { + return nil, err + } + out = append(out, s) + } + return out, rows.Err() +} + +// GetKnowledgeModelText returns the knowledge_text for a given id +func (r *PGChatRepository) GetKnowledgeModelText(ctx context.Context, id int64) (string, error) { + if r == nil || r.pool == nil { + return "", nil + } + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + var text string + err := r.pool.QueryRow(ctx, `SELECT knowledge_text FROM knowledgeModel WHERE id=$1`, id).Scan(&text) + return text, err +} + // Close releases pool resources func (r *PGChatRepository) Close() { if r != nil && r.pool != nil { diff --git a/ui_dbedit.html b/ui_dbedit.html index f30cd06..376233e 100644 --- a/ui_dbedit.html +++ b/ui_dbedit.html @@ -14,6 +14,10 @@ + +
@@ -24,6 +28,8 @@ const loadBtn = document.getElementById('load'); const addBtn = document.getElementById('add'); const downloadBtn = document.getElementById('download'); + const saveSnapshotBtn = document.getElementById('saveSnapshot'); + const snapshotSelect = document.getElementById('snapshotSelect'); let data = []; loadBtn.onclick = async function() { @@ -58,6 +64,52 @@ URL.revokeObjectURL(url); }; + saveSnapshotBtn.onclick = async function() { + const text = yamlArea.value; + status.textContent = 'Saving snapshot...'; + const resp = await fetch('/admin/chats/snapshot', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({text}) + }); + const result = await resp.json(); + if (result.status === 'ok') { + status.textContent = 'Snapshot saved!'; + loadSnapshots(); // Refresh snapshot list + } else { + status.textContent = 'Error: ' + (result.error || 'Unknown error'); + } + }; + + async function loadSnapshots() { + const resp = await fetch('/admin/chats/snapshots'); + if (!resp.ok) return; + const result = await resp.json(); + snapshotSelect.innerHTML = ''; + (result.items || []).forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; + opt.textContent = `#${s.id} - ${new Date(s.created_at).toLocaleString()}`; + snapshotSelect.appendChild(opt); + }); + } + snapshotSelect.onchange = async function() { + const id = snapshotSelect.value; + if (!id) return; + status.textContent = 'Loading snapshot...'; + const resp = await fetch(`/admin/chats/snapshot/${id}`); + if (!resp.ok) { status.textContent = 'Failed to load snapshot'; return; } + const text = await resp.text(); + yamlArea.value = text; + try { + data = jsyaml.load(text); + status.textContent = 'Snapshot loaded!'; + renderForm(); + } catch (e) { + status.textContent = 'YAML parse error!'; + } + }; + function renderForm() { const formArea = document.getElementById('formArea'); formArea.innerHTML = ''; @@ -183,6 +235,9 @@ procDiv.appendChild(pdiv); }); } + + // Load snapshot list on page load + loadSnapshots(); diff --git a/visits.bleve/store/root.bolt b/visits.bleve/store/root.bolt index 28eeff7..a896a59 100644 Binary files a/visits.bleve/store/root.bolt and b/visits.bleve/store/root.bolt differ