add ability to save
This commit is contained in:
parent
509de38353
commit
3f2789b4da
|
|
@ -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
54
main.go
|
|
@ -73,6 +73,26 @@ func main() {
|
||||||
})
|
})
|
||||||
r.GET("/db.yaml", func(c *gin.Context) { c.File("db.yaml") })
|
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
|
// JSON: list chat interactions
|
||||||
r.GET("/admin/chats", func(c *gin.Context) {
|
r.GET("/admin/chats", func(c *gin.Context) {
|
||||||
if repo == nil {
|
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")
|
r.Run(":8080")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ type ChatRepositoryAPI interface {
|
||||||
ListChatInteractions(ctx context.Context, limit, offset int) ([]ChatInteraction, error)
|
ListChatInteractions(ctx context.Context, limit, offset int) ([]ChatInteraction, error)
|
||||||
SaveLLMRawEvent(ctx context.Context, correlationID, phase, raw string) error
|
SaveLLMRawEvent(ctx context.Context, correlationID, phase, raw string) error
|
||||||
ListLLMRawEvents(ctx context.Context, correlationID string, limit, offset int) ([]RawLLMEvent, 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
|
// RawLLMEvent represents a stored raw LLM exchange phase
|
||||||
|
|
@ -41,6 +44,13 @@ type RawLLMEvent struct {
|
||||||
CreatedAt time.Time `json:"created_at"`
|
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
|
// PGChatRepository is a PostgreSQL implementation using pgxpool
|
||||||
type PGChatRepository struct {
|
type PGChatRepository struct {
|
||||||
pool *pgxpool.Pool
|
pool *pgxpool.Pool
|
||||||
|
|
@ -94,6 +104,15 @@ func (r *PGChatRepository) ensureSchema(ctx context.Context) error {
|
||||||
if _, err := r.pool.Exec(ctx, ddlRaw); err != nil {
|
if _, err := r.pool.Exec(ctx, ddlRaw); err != nil {
|
||||||
return err
|
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)
|
// Legacy column cleanup (ignore errors)
|
||||||
for _, drop := range []string{
|
for _, drop := range []string{
|
||||||
"ALTER TABLE chat_interactions DROP COLUMN IF EXISTS raw_keywords_json",
|
"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()
|
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
|
// Close releases pool resources
|
||||||
func (r *PGChatRepository) Close() {
|
func (r *PGChatRepository) Close() {
|
||||||
if r != nil && r.pool != nil {
|
if r != nil && r.pool != nil {
|
||||||
|
|
|
||||||
|
|
@ -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="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="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="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>
|
</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>
|
<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>
|
<div id="formArea"></div>
|
||||||
|
|
@ -24,6 +28,8 @@
|
||||||
const loadBtn = document.getElementById('load');
|
const loadBtn = document.getElementById('load');
|
||||||
const addBtn = document.getElementById('add');
|
const addBtn = document.getElementById('add');
|
||||||
const downloadBtn = document.getElementById('download');
|
const downloadBtn = document.getElementById('download');
|
||||||
|
const saveSnapshotBtn = document.getElementById('saveSnapshot');
|
||||||
|
const snapshotSelect = document.getElementById('snapshotSelect');
|
||||||
let data = [];
|
let data = [];
|
||||||
|
|
||||||
loadBtn.onclick = async function() {
|
loadBtn.onclick = async function() {
|
||||||
|
|
@ -58,6 +64,52 @@
|
||||||
URL.revokeObjectURL(url);
|
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() {
|
function renderForm() {
|
||||||
const formArea = document.getElementById('formArea');
|
const formArea = document.getElementById('formArea');
|
||||||
formArea.innerHTML = '';
|
formArea.innerHTML = '';
|
||||||
|
|
@ -183,6 +235,9 @@
|
||||||
procDiv.appendChild(pdiv);
|
procDiv.appendChild(pdiv);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load snapshot list on page load
|
||||||
|
loadSnapshots();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue