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();