Add a democratic idea submission and AI synthesis platform
Implement a full-stack application with a React frontend and a Python Flask backend. The backend integrates with an AI agent to filter political ideas for democratic values and synthesize accepted ideas into a collective voice. Includes API endpoints for idea submission, retrieval, and synthesis, along with database persistence. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 31c5f770-9905-46af-a938-9d40ef3d4404 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/Xzzm5QH Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"cors": "^2",
|
||||
"drizzle-orm": "catalog:",
|
||||
"express": "^5",
|
||||
"openai": "^6.33.0",
|
||||
"pino": "^9",
|
||||
"pino-http": "^10"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import OpenAI from "openai";
|
||||
import { logger } from "./logger";
|
||||
|
||||
const openai = new OpenAI({
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
export interface FilterResult {
|
||||
accepted: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export async function filterIdea(content: string): Promise<FilterResult> {
|
||||
const systemPrompt = `Tu es un agent de filtrage éthique pour une plateforme démocratique citoyenne.
|
||||
Ta mission : analyser les idées politiques soumises et décider si elles respectent les valeurs démocratiques.
|
||||
|
||||
Critères d'ACCEPTATION :
|
||||
- L'idée promeut les droits fondamentaux, la liberté, l'égalité, la justice sociale
|
||||
- L'idée propose des améliorations concrètes pour la société
|
||||
- L'idée est constructive, même si critique du gouvernement ou des institutions
|
||||
- L'idée débat de politiques publiques de manière civile
|
||||
|
||||
Critères de REJET :
|
||||
- Contenu fasciste, totalitaire ou autoritaire
|
||||
- Appels à la haine, discrimination ou violence
|
||||
- Négation de droits fondamentaux pour des groupes de personnes
|
||||
- Propagande pour des idéologies qui détruisent la démocratie
|
||||
- Contenu raciste, sexiste, homophobe ou xénophobe
|
||||
- Appels au renversement violent de la démocratie
|
||||
|
||||
Réponds UNIQUEMENT avec un JSON valide, sans markdown, dans ce format exact :
|
||||
{"accepted": true} ou {"accepted": false, "reason": "explication courte en français"}`;
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
max_completion_tokens: 200,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: `Idée à analyser : "${content}"` },
|
||||
],
|
||||
});
|
||||
|
||||
const raw = response.choices[0]?.message?.content ?? '{"accepted": false, "reason": "Erreur d\'analyse"}';
|
||||
const result = JSON.parse(raw) as FilterResult;
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Error filtering idea");
|
||||
return { accepted: false, reason: "Erreur interne de filtrage" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function synthesizeIdeas(ideas: string[]): Promise<string> {
|
||||
if (ideas.length === 0) {
|
||||
return "Aucune idée n'a encore été soumise. Soyez le premier à partager votre vision pour une société meilleure.";
|
||||
}
|
||||
|
||||
const systemPrompt = `Tu es un synthétiseur démocratique. Tu reçois une liste d'idées politiques citoyennes filtrées et validées.
|
||||
Ta mission : créer UN texte synthétique, éloquent et inspirant qui capture l'essence collective de ces idées.
|
||||
|
||||
Ce texte est "La Voix du Peuple" — il doit :
|
||||
- Être écrit à la première personne du pluriel (nous, notre, nos)
|
||||
- Capturer les thèmes communs et les aspirations partagées
|
||||
- Être poétique mais concret, inspirant mais ancré dans la réalité
|
||||
- Faire environ 3-5 paragraphes
|
||||
- Commencer par "Nous, le peuple, ..."
|
||||
- Respecter la diversité des idées sans en ignorer aucune
|
||||
- Être rédigé en français
|
||||
|
||||
NE PAS mentionner les idées individuellement, mais les fondre dans une vision collective cohérente.`;
|
||||
|
||||
const ideasText = ideas.map((idea, i) => `${i + 1}. ${idea}`).join("\n");
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-5.2",
|
||||
max_completion_tokens: 1000,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: `Voici les idées citoyennes à synthétiser :\n\n${ideasText}\n\nRédige "La Voix du Peuple".`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content ?? "Synthèse en cours...";
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Error synthesizing ideas");
|
||||
return "La synthèse est temporairement indisponible. Vos idées ont été enregistrées.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, count, and } from "drizzle-orm";
|
||||
import { db, ideasTable, synthesisTable } from "@workspace/db";
|
||||
import { SubmitIdeaBody } from "@workspace/api-zod";
|
||||
import { filterIdea, synthesizeIdeas } from "../lib/ai-agent";
|
||||
import { logger } from "../lib/logger";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/ideas", async (_req, res): Promise<void> => {
|
||||
const ideas = await db
|
||||
.select()
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, true))
|
||||
.orderBy(ideasTable.createdAt);
|
||||
res.json(ideas);
|
||||
});
|
||||
|
||||
router.get("/ideas/stats", async (_req, res): Promise<void> => {
|
||||
const [totalRow] = await db.select({ value: count() }).from(ideasTable);
|
||||
const [acceptedRow] = await db
|
||||
.select({ value: count() })
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, true));
|
||||
const [rejectedRow] = await db
|
||||
.select({ value: count() })
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, false));
|
||||
|
||||
res.json({
|
||||
total: Number(totalRow?.value ?? 0),
|
||||
accepted: Number(acceptedRow?.value ?? 0),
|
||||
rejected: Number(rejectedRow?.value ?? 0),
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/ideas", async (req, res): Promise<void> => {
|
||||
const parsed = SubmitIdeaBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: "validation_error", message: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { content, author } = parsed.data;
|
||||
|
||||
req.log.info({ contentLength: content.length }, "Filtering new idea");
|
||||
|
||||
const filterResult = await filterIdea(content);
|
||||
|
||||
const [idea] = await db
|
||||
.insert(ideasTable)
|
||||
.values({
|
||||
content,
|
||||
author: author ?? null,
|
||||
accepted: filterResult.accepted,
|
||||
rejectionReason: filterResult.reason ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (filterResult.accepted) {
|
||||
triggerSynthesisUpdate().catch((err) => {
|
||||
logger.error({ err }, "Background synthesis update failed");
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
id: idea!.id,
|
||||
accepted: filterResult.accepted,
|
||||
reason: filterResult.reason,
|
||||
idea: idea,
|
||||
});
|
||||
});
|
||||
|
||||
async function triggerSynthesisUpdate(): Promise<void> {
|
||||
const ideas = await db
|
||||
.select({ content: ideasTable.content })
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, true))
|
||||
.orderBy(ideasTable.createdAt);
|
||||
|
||||
const ideaTexts = ideas.map((i) => i.content);
|
||||
const synthesizedText = await synthesizeIdeas(ideaTexts);
|
||||
|
||||
const existing = await db.select().from(synthesisTable).limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(synthesisTable)
|
||||
.set({ text: synthesizedText, ideaCount: ideaTexts.length });
|
||||
} else {
|
||||
await db.insert(synthesisTable).values({
|
||||
text: synthesizedText,
|
||||
ideaCount: ideaTexts.length,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({ ideaCount: ideaTexts.length }, "Synthesis updated");
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import healthRouter from "./health";
|
||||
import ideasRouter from "./ideas";
|
||||
import synthesisRouter from "./synthesis";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.use(healthRouter);
|
||||
router.use(ideasRouter);
|
||||
router.use(synthesisRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { db, synthesisTable } from "@workspace/db";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/synthesis", async (_req, res): Promise<void> => {
|
||||
const [synthesis] = await db.select().from(synthesisTable).limit(1);
|
||||
|
||||
if (!synthesis) {
|
||||
res.json({
|
||||
text: "Aucune idée n'a encore été soumise. Soyez le premier à partager votre vision pour une société meilleure.",
|
||||
ideaCount: 0,
|
||||
updatedAt: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
text: synthesis.text,
|
||||
ideaCount: synthesis.ideaCount,
|
||||
updatedAt: synthesis.updatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user