add ability to save

This commit is contained in:
lehel 2025-10-07 10:04:03 +02:00
parent 509de38353
commit 3f2789b4da
No known key found for this signature in database
GPG Key ID: 9C4F9D6111EE5CFA
5 changed files with 180 additions and 7 deletions

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4464bfa3-4e16-44aa-b61e-e142cf841d2f/console.sql" value="4464bfa3-4e16-44aa-b61e-e142cf841d2f" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/85be2c57-4234-463a-a995-092322f406a0/console.sql" value="85be2c57-4234-463a-a995-092322f406a0" />
</component>
</project>

54
main.go
View File

@ -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")
}

View File

@ -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 {

View File

@ -14,6 +14,10 @@
<button id="load" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">Load db.yaml</button>
<button id="add" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">Add Entry</button>
<button id="download" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition">Download YAML</button>
<button id="saveSnapshot" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition">Save Snapshot</button>
<select id="snapshotSelect" class="px-4 py-2 bg-yellow-100 border border-yellow-400 rounded-lg text-yellow-900">
<option value="">Load from snapshot...</option>
</select>
</div>
<textarea id="yamlArea" placeholder="YAML will appear here..." class="w-full h-40 mb-4 p-2 border border-gray-300 rounded-lg bg-gray-100 text-sm"></textarea>
<div id="formArea"></div>
@ -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 = '<option value="">Load from snapshot...</option>';
(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();
</script>
</body>
</html>

Binary file not shown.