mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: add skills to each history
This commit is contained in:
@@ -134,6 +134,7 @@ CREATE INDEX IF NOT EXISTS idx_lifecycle_events_event_type ON lifecycle_events(e
|
||||
CREATE TABLE IF NOT EXISTS history (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
messages JSONB NOT NULL,
|
||||
skills TEXT[] NOT NULL DEFAULT '{}'::text[],
|
||||
timestamp TIMESTAMPTZ NOT NULL,
|
||||
"user" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
-- name: CreateHistory :one
|
||||
INSERT INTO history (messages, timestamp, "user")
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, messages, timestamp, "user";
|
||||
INSERT INTO history (messages, skills, timestamp, "user")
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, messages, skills, timestamp, "user";
|
||||
|
||||
-- name: ListHistoryByUserSince :many
|
||||
SELECT id, messages, timestamp, "user"
|
||||
SELECT id, messages, skills, timestamp, "user"
|
||||
FROM history
|
||||
WHERE "user" = $1 AND timestamp >= $2
|
||||
ORDER BY timestamp ASC;
|
||||
|
||||
-- name: GetHistoryByID :one
|
||||
SELECT id, messages, timestamp, "user"
|
||||
SELECT id, messages, skills, timestamp, "user"
|
||||
FROM history
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListHistoryByUser :many
|
||||
SELECT id, messages, timestamp, "user"
|
||||
SELECT id, messages, skills, timestamp, "user"
|
||||
FROM history
|
||||
WHERE "user" = $1
|
||||
ORDER BY timestamp DESC
|
||||
|
||||
@@ -2366,6 +2366,20 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"chat.AgentSkill": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat.ChatRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2404,6 +2418,18 @@ const docTemplate = `{
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/chat.AgentSkill"
|
||||
}
|
||||
},
|
||||
"use_skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2421,6 +2447,12 @@ const docTemplate = `{
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2615,6 +2647,12 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2642,6 +2680,12 @@ const docTemplate = `{
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2357,6 +2357,20 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"chat.AgentSkill": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat.ChatRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2395,6 +2409,18 @@
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/chat.AgentSkill"
|
||||
}
|
||||
},
|
||||
"use_skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2412,6 +2438,12 @@
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2606,6 +2638,12 @@
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2633,6 +2671,12 @@
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
definitions:
|
||||
chat.AgentSkill:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
chat.ChatRequest:
|
||||
properties:
|
||||
current_platform:
|
||||
@@ -25,6 +34,14 @@ definitions:
|
||||
type: string
|
||||
query:
|
||||
type: string
|
||||
skills:
|
||||
items:
|
||||
$ref: '#/definitions/chat.AgentSkill'
|
||||
type: array
|
||||
use_skills:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
chat.ChatResponse:
|
||||
properties:
|
||||
@@ -36,6 +53,10 @@ definitions:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
skills:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
chat.GatewayMessage:
|
||||
additionalProperties: true
|
||||
@@ -162,6 +183,10 @@ definitions:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type: array
|
||||
skills:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
history.ListResponse:
|
||||
properties:
|
||||
@@ -179,6 +204,10 @@ definitions:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type: array
|
||||
skills:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
timestamp:
|
||||
type: string
|
||||
user_id:
|
||||
|
||||
@@ -85,16 +85,22 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
|
||||
}
|
||||
|
||||
var messages []GatewayMessage
|
||||
var historySkills []string
|
||||
if !skipHistory {
|
||||
messages, err = r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
|
||||
if err != nil {
|
||||
return ChatResponse{}, err
|
||||
}
|
||||
historySkills, err = r.loadHistorySkills(ctx, req.UserID, maxContextLoadTime)
|
||||
if err != nil {
|
||||
return ChatResponse{}, err
|
||||
}
|
||||
}
|
||||
if len(req.Messages) > 0 {
|
||||
messages = append(messages, req.Messages...)
|
||||
}
|
||||
messages = sanitizeGatewayMessages(messages)
|
||||
useSkills := normalizeSkills(append(historySkills, req.UseSkills...))
|
||||
|
||||
payload := agentGatewayRequest{
|
||||
APIKey: provider.ApiKey,
|
||||
@@ -109,6 +115,8 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
|
||||
CurrentPlatform: req.CurrentPlatform,
|
||||
Messages: messages,
|
||||
Query: req.Query,
|
||||
Skills: req.Skills,
|
||||
UseSkills: useSkills,
|
||||
}
|
||||
payload.Language = language
|
||||
|
||||
@@ -117,7 +125,7 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
|
||||
return ChatResponse{}, err
|
||||
}
|
||||
|
||||
if err := r.storeHistory(ctx, req.UserID, req.Query, resp.Messages); err != nil {
|
||||
if err := r.storeHistory(ctx, req.UserID, req.Query, resp.Messages, resp.Skills); err != nil {
|
||||
return ChatResponse{}, err
|
||||
}
|
||||
if err := r.storeMemory(ctx, req.UserID, req.Query, resp.Messages); err != nil {
|
||||
@@ -126,6 +134,7 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
|
||||
|
||||
return ChatResponse{
|
||||
Messages: resp.Messages,
|
||||
Skills: resp.Skills,
|
||||
Model: chatModel.ModelID,
|
||||
Provider: provider.ClientType,
|
||||
}, nil
|
||||
@@ -163,6 +172,11 @@ func (r *Resolver) TriggerSchedule(ctx context.Context, userID string, schedule
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
historySkills, err := r.loadHistorySkills(ctx, userID, maxContextLoadTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
useSkills := normalizeSkills(historySkills)
|
||||
|
||||
payload := agentGatewayScheduleRequest{
|
||||
APIKey: provider.ApiKey,
|
||||
@@ -178,13 +192,14 @@ func (r *Resolver) TriggerSchedule(ctx context.Context, userID string, schedule
|
||||
Messages: messages,
|
||||
Query: schedule.Command,
|
||||
Schedule: schedule,
|
||||
UseSkills: useSkills,
|
||||
}
|
||||
|
||||
resp, err := r.postSchedule(ctx, payload, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.storeHistory(ctx, userID, schedule.Command, resp.Messages); err != nil {
|
||||
if err := r.storeHistory(ctx, userID, schedule.Command, resp.Messages, resp.Skills); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.storeMemory(ctx, userID, schedule.Command, resp.Messages); err != nil {
|
||||
@@ -235,17 +250,24 @@ func (r *Resolver) StreamChat(ctx context.Context, req ChatRequest) (<-chan Stre
|
||||
}
|
||||
|
||||
var messages []GatewayMessage
|
||||
var historySkills []string
|
||||
if !skipHistory {
|
||||
messages, err = r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
historySkills, err = r.loadHistorySkills(ctx, req.UserID, maxContextLoadTime)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(req.Messages) > 0 {
|
||||
messages = append(messages, req.Messages...)
|
||||
}
|
||||
messages = sanitizeGatewayMessages(messages)
|
||||
useSkills := normalizeSkills(append(historySkills, req.UseSkills...))
|
||||
|
||||
payload := agentGatewayRequest{
|
||||
APIKey: provider.ApiKey,
|
||||
@@ -260,6 +282,8 @@ func (r *Resolver) StreamChat(ctx context.Context, req ChatRequest) (<-chan Stre
|
||||
CurrentPlatform: req.CurrentPlatform,
|
||||
Messages: messages,
|
||||
Query: req.Query,
|
||||
Skills: req.Skills,
|
||||
UseSkills: useSkills,
|
||||
}
|
||||
payload.Language = language
|
||||
|
||||
@@ -285,6 +309,8 @@ type agentGatewayRequest struct {
|
||||
CurrentPlatform string `json:"currentPlatform,omitempty"`
|
||||
Messages []GatewayMessage `json:"messages"`
|
||||
Query string `json:"query"`
|
||||
Skills []AgentSkill `json:"skills,omitempty"`
|
||||
UseSkills []string `json:"useSkills,omitempty"`
|
||||
}
|
||||
|
||||
type agentGatewayScheduleRequest struct {
|
||||
@@ -301,10 +327,13 @@ type agentGatewayScheduleRequest struct {
|
||||
Messages []GatewayMessage `json:"messages"`
|
||||
Query string `json:"query"`
|
||||
Schedule SchedulePayload `json:"schedule"`
|
||||
Skills []AgentSkill `json:"skills,omitempty"`
|
||||
UseSkills []string `json:"useSkills,omitempty"`
|
||||
}
|
||||
|
||||
type agentGatewayResponse struct {
|
||||
Messages []GatewayMessage `json:"messages"`
|
||||
Skills []string `json:"skills"`
|
||||
}
|
||||
|
||||
func (r *Resolver) postChat(ctx context.Context, payload agentGatewayRequest, token string) (agentGatewayResponse, error) {
|
||||
@@ -472,7 +501,36 @@ func (r *Resolver) loadHistoryMessages(ctx context.Context, userID string, maxCo
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) storeHistory(ctx context.Context, userID, query string, responseMessages []GatewayMessage) error {
|
||||
func (r *Resolver) loadHistorySkills(ctx context.Context, userID string, maxContextLoadTime int) ([]string, error) {
|
||||
if r.queries == nil {
|
||||
return nil, fmt.Errorf("history queries not configured")
|
||||
}
|
||||
pgUserID, err := parseUUID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from := time.Now().UTC().Add(-time.Duration(normalizeMaxContextLoad(maxContextLoadTime)) * time.Minute)
|
||||
rows, err := r.queries.ListHistoryByUserSince(ctx, sqlc.ListHistoryByUserSinceParams{
|
||||
User: pgUserID,
|
||||
Timestamp: pgtype.Timestamptz{
|
||||
Time: from,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
combined := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if len(row.Skills) == 0 {
|
||||
continue
|
||||
}
|
||||
combined = append(combined, row.Skills...)
|
||||
}
|
||||
return normalizeSkills(combined), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) storeHistory(ctx context.Context, userID, query string, responseMessages []GatewayMessage, skills []string) error {
|
||||
if r.queries == nil {
|
||||
return fmt.Errorf("history queries not configured")
|
||||
}
|
||||
@@ -493,8 +551,10 @@ func (r *Resolver) storeHistory(ctx context.Context, userID, query string, respo
|
||||
if err := r.ensureUserExists(ctx, pgUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
normalizedSkills := normalizeSkills(skills)
|
||||
_, err = r.queries.CreateHistory(ctx, sqlc.CreateHistoryParams{
|
||||
Messages: payload,
|
||||
Skills: normalizedSkills,
|
||||
Timestamp: pgtype.Timestamptz{
|
||||
Time: time.Now().UTC(),
|
||||
Valid: true,
|
||||
@@ -579,7 +639,7 @@ func (r *Resolver) tryStoreFromStreamPayload(ctx context.Context, userID, query,
|
||||
// Case 1: event: done + data: {messages: [...]}
|
||||
if eventType == "done" {
|
||||
if parsed, ok := parseGatewayResponse([]byte(data)); ok {
|
||||
return r.storeRound(ctx, userID, query, parsed.Messages)
|
||||
return r.storeRound(ctx, userID, query, parsed.Messages, parsed.Skills)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,14 +651,14 @@ func (r *Resolver) tryStoreFromStreamPayload(ctx context.Context, userID, query,
|
||||
if err := json.Unmarshal([]byte(data), &envelope); err == nil {
|
||||
if envelope.Type == "done" && len(envelope.Data) > 0 {
|
||||
if parsed, ok := parseGatewayResponse(envelope.Data); ok {
|
||||
return r.storeRound(ctx, userID, query, parsed.Messages)
|
||||
return r.storeRound(ctx, userID, query, parsed.Messages, parsed.Skills)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: data: {messages:[...]} without event
|
||||
if parsed, ok := parseGatewayResponse([]byte(data)); ok {
|
||||
return r.storeRound(ctx, userID, query, parsed.Messages)
|
||||
return r.storeRound(ctx, userID, query, parsed.Messages, parsed.Skills)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -614,8 +674,8 @@ func parseGatewayResponse(payload []byte) (agentGatewayResponse, bool) {
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
func (r *Resolver) storeRound(ctx context.Context, userID, query string, messages []GatewayMessage) (bool, error) {
|
||||
if err := r.storeHistory(ctx, userID, query, messages); err != nil {
|
||||
func (r *Resolver) storeRound(ctx context.Context, userID, query string, messages []GatewayMessage, skills []string) (bool, error) {
|
||||
if err := r.storeHistory(ctx, userID, query, messages, skills); err != nil {
|
||||
return true, err
|
||||
}
|
||||
if err := r.storeMemory(ctx, userID, query, messages); err != nil {
|
||||
@@ -624,6 +684,23 @@ func (r *Resolver) storeRound(ctx context.Context, userID, query string, message
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func normalizeSkills(skills []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
normalized := make([]string, 0, len(skills))
|
||||
for _, skill := range skills {
|
||||
trimmed := strings.TrimSpace(skill)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
normalized = append(normalized, trimmed)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func gatewayMessageToMemory(msg GatewayMessage) (string, string) {
|
||||
role := "assistant"
|
||||
if raw, ok := msg["role"].(string); ok && strings.TrimSpace(raw) != "" {
|
||||
|
||||
@@ -9,6 +9,12 @@ type Message struct {
|
||||
|
||||
type GatewayMessage map[string]interface{}
|
||||
|
||||
type AgentSkill struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ChatRequest struct {
|
||||
UserID string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
@@ -22,10 +28,13 @@ type ChatRequest struct {
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
CurrentPlatform string `json:"current_platform,omitempty"`
|
||||
Messages []GatewayMessage `json:"messages,omitempty"`
|
||||
Skills []AgentSkill `json:"skills,omitempty"`
|
||||
UseSkills []string `json:"use_skills,omitempty"`
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Messages []GatewayMessage `json:"messages"`
|
||||
Skills []string `json:"skills,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
}
|
||||
|
||||
@@ -12,23 +12,30 @@ import (
|
||||
)
|
||||
|
||||
const createHistory = `-- name: CreateHistory :one
|
||||
INSERT INTO history (messages, timestamp, "user")
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, messages, timestamp, "user"
|
||||
INSERT INTO history (messages, skills, timestamp, "user")
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, messages, skills, timestamp, "user"
|
||||
`
|
||||
|
||||
type CreateHistoryParams struct {
|
||||
Messages []byte `json:"messages"`
|
||||
Skills []string `json:"skills"`
|
||||
Timestamp pgtype.Timestamptz `json:"timestamp"`
|
||||
User pgtype.UUID `json:"user"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateHistory(ctx context.Context, arg CreateHistoryParams) (History, error) {
|
||||
row := q.db.QueryRow(ctx, createHistory, arg.Messages, arg.Timestamp, arg.User)
|
||||
row := q.db.QueryRow(ctx, createHistory,
|
||||
arg.Messages,
|
||||
arg.Skills,
|
||||
arg.Timestamp,
|
||||
arg.User,
|
||||
)
|
||||
var i History
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Messages,
|
||||
&i.Skills,
|
||||
&i.Timestamp,
|
||||
&i.User,
|
||||
)
|
||||
@@ -56,7 +63,7 @@ func (q *Queries) DeleteHistoryByUser(ctx context.Context, user pgtype.UUID) err
|
||||
}
|
||||
|
||||
const getHistoryByID = `-- name: GetHistoryByID :one
|
||||
SELECT id, messages, timestamp, "user"
|
||||
SELECT id, messages, skills, timestamp, "user"
|
||||
FROM history
|
||||
WHERE id = $1
|
||||
`
|
||||
@@ -67,6 +74,7 @@ func (q *Queries) GetHistoryByID(ctx context.Context, id pgtype.UUID) (History,
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Messages,
|
||||
&i.Skills,
|
||||
&i.Timestamp,
|
||||
&i.User,
|
||||
)
|
||||
@@ -74,7 +82,7 @@ func (q *Queries) GetHistoryByID(ctx context.Context, id pgtype.UUID) (History,
|
||||
}
|
||||
|
||||
const listHistoryByUser = `-- name: ListHistoryByUser :many
|
||||
SELECT id, messages, timestamp, "user"
|
||||
SELECT id, messages, skills, timestamp, "user"
|
||||
FROM history
|
||||
WHERE "user" = $1
|
||||
ORDER BY timestamp DESC
|
||||
@@ -98,6 +106,7 @@ func (q *Queries) ListHistoryByUser(ctx context.Context, arg ListHistoryByUserPa
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Messages,
|
||||
&i.Skills,
|
||||
&i.Timestamp,
|
||||
&i.User,
|
||||
); err != nil {
|
||||
@@ -112,7 +121,7 @@ func (q *Queries) ListHistoryByUser(ctx context.Context, arg ListHistoryByUserPa
|
||||
}
|
||||
|
||||
const listHistoryByUserSince = `-- name: ListHistoryByUserSince :many
|
||||
SELECT id, messages, timestamp, "user"
|
||||
SELECT id, messages, skills, timestamp, "user"
|
||||
FROM history
|
||||
WHERE "user" = $1 AND timestamp >= $2
|
||||
ORDER BY timestamp ASC
|
||||
@@ -135,6 +144,7 @@ func (q *Queries) ListHistoryByUserSince(ctx context.Context, arg ListHistoryByU
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Messages,
|
||||
&i.Skills,
|
||||
&i.Timestamp,
|
||||
&i.User,
|
||||
); err != nil {
|
||||
|
||||
@@ -36,6 +36,7 @@ type ContainerVersion struct {
|
||||
type History struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Messages []byte `json:"messages"`
|
||||
Skills []string `json:"skills"`
|
||||
Timestamp pgtype.Timestamptz `json:"timestamp"`
|
||||
User pgtype.UUID `json:"user"`
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func (s *Service) Create(ctx context.Context, userID string, req CreateRequest)
|
||||
}
|
||||
row, err := s.queries.CreateHistory(ctx, sqlc.CreateHistoryParams{
|
||||
Messages: payload,
|
||||
Skills: normalizeSkills(req.Skills),
|
||||
Timestamp: pgtype.Timestamptz{
|
||||
Time: time.Now().UTC(),
|
||||
Valid: true,
|
||||
@@ -117,6 +118,7 @@ func toRecord(row sqlc.History) (Record, error) {
|
||||
}
|
||||
record := Record{
|
||||
Messages: messages,
|
||||
Skills: normalizeSkills(row.Skills),
|
||||
}
|
||||
if row.Timestamp.Valid {
|
||||
record.Timestamp = row.Timestamp.Time
|
||||
@@ -136,6 +138,23 @@ func toRecord(row sqlc.History) (Record, error) {
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func normalizeSkills(skills []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
normalized := make([]string, 0, len(skills))
|
||||
for _, skill := range skills {
|
||||
trimmed := strings.TrimSpace(skill)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
normalized = append(normalized, trimmed)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func parseUUID(id string) (pgtype.UUID, error) {
|
||||
parsed, err := uuid.Parse(strings.TrimSpace(id))
|
||||
if err != nil {
|
||||
|
||||
@@ -5,12 +5,14 @@ import "time"
|
||||
type Record struct {
|
||||
ID string `json:"id"`
|
||||
Messages []map[string]interface{} `json:"messages"`
|
||||
Skills []string `json:"skills"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type CreateRequest struct {
|
||||
Messages []map[string]interface{} `json:"messages"`
|
||||
Skills []string `json:"skills,omitempty"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
|
||||
Reference in New Issue
Block a user