feat: add AI chat for repports
This commit is contained in:
@ -296,6 +296,60 @@ func (p *Pipeline) callProviderForReport(ctx context.Context, excerpt, question
|
||||
return provider.Summarize(ctx, prompt, GenOptions{Think: true, NumCtx: 16384})
|
||||
}
|
||||
|
||||
// GenerateReportMessageAsync génère une réponse de conversation en arrière-plan.
|
||||
// history contient tous les messages précédents (user + assistant), dans l'ordre.
|
||||
func (p *Pipeline) GenerateReportMessageAsync(messageID string, report *models.Report, history []models.ReportMessage, mgr *ReportManager) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
mgr.Register(messageID, cancel)
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer mgr.Remove(messageID)
|
||||
|
||||
answer, err := p.callProviderForConversation(ctx, report, history)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
_ = p.repo.UpdateReportMessage(messageID, "error", err.Error())
|
||||
return
|
||||
}
|
||||
_ = p.repo.UpdateReportMessage(messageID, "done", answer)
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Pipeline) callProviderForConversation(ctx context.Context, report *models.Report, history []models.ReportMessage) (string, error) {
|
||||
provider, _, err := p.buildProviderForRole("report")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Tu es un assistant financier expert engagé dans une conversation avec un trader.\n\n")
|
||||
sb.WriteString("## Contexte initial\n")
|
||||
sb.WriteString("Extraits sélectionnés :\n")
|
||||
sb.WriteString(report.ContextExcerpt)
|
||||
sb.WriteString("\n\nQuestion initiale : ")
|
||||
sb.WriteString(report.Question)
|
||||
sb.WriteString("\nRéponse initiale : ")
|
||||
sb.WriteString(report.Answer)
|
||||
sb.WriteString("\n\n## Suite de la conversation\n")
|
||||
|
||||
for _, msg := range history {
|
||||
if msg.Role == "user" {
|
||||
sb.WriteString("Trader : ")
|
||||
} else {
|
||||
sb.WriteString("Assistant : ")
|
||||
}
|
||||
sb.WriteString(msg.Content)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\nRéponds en français, de façon précise et orientée trading.")
|
||||
|
||||
return provider.Summarize(ctx, sb.String(), GenOptions{Think: true, NumCtx: 16384})
|
||||
}
|
||||
|
||||
func buildPrompt(systemPrompt string, symbols []string, articles []models.Article, tz string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(systemPrompt)
|
||||
|
||||
@ -69,6 +69,79 @@ func (h *Handler) GetGeneratingStatus(c *gin.Context) {
|
||||
httputil.OK(c, gin.H{"generating": h.pipeline.IsGenerating()})
|
||||
}
|
||||
|
||||
func (h *Handler) ListReportMessages(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
reportID := c.Param("id")
|
||||
|
||||
report, err := h.repo.GetReport(reportID, userID)
|
||||
if err != nil {
|
||||
httputil.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
msgs, err := h.repo.ListReportMessages(reportID)
|
||||
if err != nil {
|
||||
httputil.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
httputil.OK(c, msgs)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateReportMessage(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
reportID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httputil.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.repo.GetReport(reportID, userID)
|
||||
if err != nil {
|
||||
httputil.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
if report == nil || report.Status != "done" {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer l'historique pour le contexte IA
|
||||
history, err := h.repo.ListReportMessages(reportID)
|
||||
if err != nil {
|
||||
httputil.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Persister le message utilisateur
|
||||
userMsg, err := h.repo.CreateReportMessage(reportID, "user", req.Content, "done")
|
||||
if err != nil {
|
||||
httputil.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Créer le message assistant en attente
|
||||
assistantMsg, err := h.repo.CreateReportMessage(reportID, "assistant", "", "generating")
|
||||
if err != nil {
|
||||
httputil.InternalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Ajouter le message utilisateur à l'historique avant de lancer la génération
|
||||
history = append(history, *userMsg)
|
||||
|
||||
h.pipeline.GenerateReportMessageAsync(assistantMsg.ID, report, history, h.reportManager)
|
||||
|
||||
c.JSON(http.StatusCreated, assistantMsg)
|
||||
}
|
||||
|
||||
func buildExcerptContext(excerpts []string) string {
|
||||
if len(excerpts) == 1 {
|
||||
return excerpts[0]
|
||||
|
||||
@ -49,6 +49,8 @@ func SetupRouter(h *handlers.Handler, jwtSecret string) *gin.Engine {
|
||||
authed.GET("/reports", h.ListReports)
|
||||
authed.POST("/reports", h.CreateReport)
|
||||
authed.DELETE("/reports/:id", h.DeleteReport)
|
||||
authed.GET("/reports/:id/messages", h.ListReportMessages)
|
||||
authed.POST("/reports/:id/messages", h.CreateReportMessage)
|
||||
|
||||
// Admin
|
||||
admin := authed.Group("/admin")
|
||||
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS report_messages;
|
||||
@ -0,0 +1,8 @@
|
||||
CREATE TABLE report_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
report_id UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
|
||||
role VARCHAR(10) NOT NULL CHECK (role IN ('user', 'assistant')),
|
||||
content TEXT NOT NULL,
|
||||
status VARCHAR(10) NOT NULL DEFAULT 'done' CHECK (status IN ('done', 'generating', 'error')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@ -116,3 +116,12 @@ type Report struct {
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ReportMessage struct {
|
||||
ID string `json:"id"`
|
||||
ReportID string `json:"report_id"`
|
||||
Role string `json:"role"` // user | assistant
|
||||
Content string `json:"content"`
|
||||
Status string `json:"status"` // done | generating | error
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
@ -666,3 +666,55 @@ func (r *Repository) DeleteReport(id, userID string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM reports WHERE id=$1 AND user_id=$2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) GetReport(id, userID string) (*Report, error) {
|
||||
rep := &Report{}
|
||||
err := r.db.QueryRow(`
|
||||
SELECT id, user_id, summary_id, context_excerpt, question, answer, status, error_msg, created_at
|
||||
FROM reports WHERE id=$1 AND user_id=$2`, id, userID,
|
||||
).Scan(&rep.ID, &rep.UserID, &rep.SummaryID, &rep.ContextExcerpt, &rep.Question, &rep.Answer, &rep.Status, &rep.ErrorMsg, &rep.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return rep, err
|
||||
}
|
||||
|
||||
// ── Report messages ────────────────────────────────────────────────────────
|
||||
|
||||
func (r *Repository) CreateReportMessage(reportID, role, content, status string) (*ReportMessage, error) {
|
||||
msg := &ReportMessage{}
|
||||
err := r.db.QueryRow(`
|
||||
INSERT INTO report_messages (report_id, role, content, status)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, report_id, role, content, status, created_at`,
|
||||
reportID, role, content, status,
|
||||
).Scan(&msg.ID, &msg.ReportID, &msg.Role, &msg.Content, &msg.Status, &msg.CreatedAt)
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateReportMessage(id, status, content string) error {
|
||||
_, err := r.db.Exec(`
|
||||
UPDATE report_messages SET status=$1, content=$2 WHERE id=$3`,
|
||||
status, content, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) ListReportMessages(reportID string) ([]ReportMessage, error) {
|
||||
rows, err := r.db.Query(`
|
||||
SELECT id, report_id, role, content, status, created_at
|
||||
FROM report_messages WHERE report_id=$1
|
||||
ORDER BY created_at ASC`, reportID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var msgs []ReportMessage
|
||||
for rows.Next() {
|
||||
var m ReportMessage
|
||||
if err := rows.Scan(&m.ID, &m.ReportID, &m.Role, &m.Content, &m.Status, &m.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user