Skip to content

Commit 862a175

Browse files
authored
Merge pull request #400 from netzbegruenung/feature/expo-mobile-app
feat: Plan Mode, PR Agent, and Major Platform Improvements
2 parents 67bcad1 + 82674ae commit 862a175

212 files changed

Lines changed: 15334 additions & 3835 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Arguments Generator
3+
* Searches Green Party knowledge bases for relevant arguments using Qdrant
4+
*/
5+
6+
import { getQdrantInstance } from '../../../../database/services/QdrantService/index.js';
7+
import { mistralEmbeddingService } from '../../../../services/mistral/MistralEmbeddingService/index.js';
8+
import { SYSTEM_COLLECTIONS } from '../../../../config/systemCollectionsConfig.js';
9+
10+
export interface ArgumentResult {
11+
source: string;
12+
text: string;
13+
relevance: number;
14+
metadata: {
15+
collection: string;
16+
category?: string;
17+
contentType?: string;
18+
url?: string;
19+
};
20+
}
21+
22+
/**
23+
* Search for relevant arguments from Green Party knowledge bases
24+
* Uses Qdrant multi-collection hybrid search
25+
*/
26+
export async function searchArgumentsFromNotebooks(
27+
topic: string,
28+
options: {
29+
collections?: string[];
30+
limit?: number;
31+
threshold?: number;
32+
} = {}
33+
): Promise<ArgumentResult[]> {
34+
const {
35+
collections = [
36+
'grundsatz_documents', // Grundsatzprogramme
37+
'bundestag_content', // Bundestagsfraktion content
38+
'kommunalwiki_documents', // KommunalWiki
39+
'gruene_de_documents', // gruene.de
40+
'gruene_at_documents' // gruene.at
41+
],
42+
limit = 10,
43+
threshold = 0.35
44+
} = options;
45+
46+
const qdrant = getQdrantInstance();
47+
48+
// Initialize services
49+
await qdrant.init();
50+
await mistralEmbeddingService.init();
51+
52+
if (!qdrant.isAvailable() || !mistralEmbeddingService.isReady()) {
53+
console.warn('[ArgumentsGenerator] Qdrant or Mistral not available');
54+
return [];
55+
}
56+
57+
// Generate embedding for the topic
58+
let topicEmbedding: number[];
59+
try {
60+
topicEmbedding = await mistralEmbeddingService.generateEmbedding(topic);
61+
} catch (error) {
62+
console.error('[ArgumentsGenerator] Embedding generation failed:', error);
63+
return [];
64+
}
65+
66+
// Search across all collections in parallel
67+
const searchPromises = collections.map(async (collection) => {
68+
try {
69+
// Use hybrid search (vector + text) for better precision
70+
const result = await qdrant.hybridSearchDocuments(
71+
topicEmbedding,
72+
topic,
73+
{
74+
collection,
75+
limit: Math.ceil(limit / collections.length) + 5, // Over-fetch per collection
76+
threshold
77+
}
78+
);
79+
80+
return result.results.map(r => ({
81+
source: (r.title as string) || (r.document_id as string) || collection,
82+
text: (r.chunk_text as string) || '',
83+
relevance: r.score || 0,
84+
metadata: {
85+
collection,
86+
category: (r.section as string) || '',
87+
contentType: (r.metadata?.content_type as string) || '',
88+
url: (r.url as string) || ''
89+
}
90+
}));
91+
} catch (error) {
92+
console.error(`[ArgumentsGenerator] Search failed for ${collection}:`, error);
93+
return [];
94+
}
95+
});
96+
97+
const results = await Promise.all(searchPromises);
98+
99+
// Merge, deduplicate, and sort by relevance
100+
const allArguments = results.flat();
101+
102+
// Deduplicate by text content (keep highest relevance)
103+
const uniqueArguments = Array.from(
104+
new Map(allArguments.map(a => [a.text, a])).values()
105+
);
106+
107+
return uniqueArguments
108+
.sort((a, b) => b.relevance - a.relevance)
109+
.slice(0, limit);
110+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import mistralClient from '../../../../workers/mistralClient.js';
2+
import type { ArgumentResult } from './argumentsGenerator.js';
3+
4+
/**
5+
* Generate a concise summary of research arguments using Mistral Small
6+
* Summarizes the key findings from Green Party knowledge bases
7+
*/
8+
export async function summarizeArguments(
9+
topic: string,
10+
argumentsList: ArgumentResult[]
11+
): Promise<string> {
12+
if (!argumentsList || argumentsList.length === 0) {
13+
return 'Keine relevanten Argumente gefunden.';
14+
}
15+
16+
const argumentsText = argumentsList.map((arg, idx) =>
17+
`${idx + 1}. **${arg.source}** (Relevanz: ${Math.round(arg.relevance * 100)}%)\n ${arg.text}`
18+
).join('\n\n');
19+
20+
const prompt = `Du bist ein grüner Kommunikationsberater.
21+
22+
**Aufgabe**: Fasse die folgenden recherchierten Argumente aus grünen Wissensdatenbanken zu einem prägnanten, übersichtlichen Summary zusammen.
23+
24+
**Thema**: ${topic}
25+
26+
**Recherchierte Argumente**:
27+
28+
${argumentsText}
29+
30+
**Deine Antwort**:
31+
Erstelle eine strukturierte Zusammenfassung mit:
32+
- **Kernaussagen**: Die 2-3 wichtigsten Argumente (als Stichpunkte)
33+
- **Quellenbasis**: Welche Dokumente/Programme liefern die stärksten Belege?
34+
- **Nutzungshinweis**: Wie können diese Argumente in der Kommunikation verwendet werden?
35+
36+
Halte die Zusammenfassung kurz (max. 200 Wörter), präzise und sofort nutzbar.`;
37+
38+
try {
39+
const response = await mistralClient.chat.complete({
40+
model: 'mistral-small-latest',
41+
messages: [
42+
{
43+
role: 'user',
44+
content: prompt
45+
}
46+
],
47+
max_tokens: 500,
48+
temperature: 0.3
49+
});
50+
51+
const summary = response.choices?.[0]?.message?.content || '';
52+
return summary || 'Zusammenfassung konnte nicht erstellt werden.';
53+
} catch (error) {
54+
console.error('[ArgumentsSummarizer] Failed to generate summary:', error);
55+
return `**Recherchierte Argumente (${argumentsList.length})**\n\nDie Recherche hat ${argumentsList.length} relevante Argumente aus grünen Wissensdatenbanken gefunden. Bitte siehe Details unten.`;
56+
}
57+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { assemblePromptGraphAsync } from '../../promptAssemblyGraph.js';
2+
import { MARKDOWN_FORMATTING_INSTRUCTIONS } from '../../../../utils/prompt/index.js';
3+
import type { EnrichedState } from '../../../../utils/types/requestEnrichment.js';
4+
import type { PRAgentRequest } from '../types.js';
5+
6+
/**
7+
* Generates strategic framing: narrative, values, audiences, wording
8+
*/
9+
export async function generateStrategicFraming(
10+
enrichedState: EnrichedState,
11+
req: any
12+
): Promise<string> {
13+
console.log('[PR Agent] Generating strategic framing');
14+
15+
const request = enrichedState.request as PRAgentRequest;
16+
17+
const systemRole = `Du bist ein erfahrener strategischer Kommunikationsberater für Bündnis 90/Die Grünen.
18+
19+
Deine Aufgabe ist es, das strategische Framing für politische Kommunikation zu entwickeln - bevor die eigentlichen Texte geschrieben werden.
20+
21+
Analysiere das Thema und liefere eine kompakte strategische Einschätzung (1-2 Absätze) mit folgenden Elementen:
22+
23+
1. **Grüner Kern**: Wie verknüpfen wir das Thema mit unseren Grundwerten (Klimaschutz, soziale Gerechtigkeit, Freiheit, wirtschaftliche Modernisierung)?
24+
25+
2. **Zielgruppen-Ansprache**:
26+
- Wen wollen wir erreichen? (eigene Basis vs. bürgerliche Mitte)
27+
- Emotionalisierung (Herz) oder Faktenfokus (Kopf)?
28+
29+
3. **Wording**: Welche Begriffe nutzen wir, um das Thema positiv zu besetzen? (z.B. "Schutz" statt "Verbot", "Innovation" statt "Regulierung")
30+
31+
4. **Narrativ**: In einen Satz: Was ist die Geschichte, die wir erzählen wollen?
32+
33+
Sei präzise und strategisch. Dies ist die Grundlage für alle nachfolgenden Kommunikationsmaßnahmen.`;
34+
35+
const userMessage = `Thema: ${request.inhalt}
36+
37+
Entwickle das strategische Framing für dieses Thema.`;
38+
39+
const promptResult = await assemblePromptGraphAsync({
40+
...enrichedState,
41+
systemRole,
42+
request: userMessage,
43+
constraints: 'Antwort: 1-2 kompakte Absätze, maximal 800 Zeichen.',
44+
formatting: MARKDOWN_FORMATTING_INSTRUCTIONS
45+
});
46+
47+
const aiResult = await req.app.locals.aiWorkerPool.processRequest({
48+
type: 'social',
49+
usePrivacyMode: request.usePrivacyMode || false,
50+
systemPrompt: promptResult.system,
51+
messages: promptResult.messages,
52+
options: {
53+
max_tokens: 600,
54+
temperature: 0.7,
55+
top_p: 0.9
56+
}
57+
}, req);
58+
59+
return aiResult.content || aiResult.data?.content || '';
60+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { loadPromptConfig, buildConstraints, buildRequestContent, SimpleTemplateEngine } from '../../PromptProcessor.js';
2+
import { assemblePromptGraphAsync } from '../../promptAssemblyGraph.js';
3+
import type { EnrichedState } from '../../../../utils/types/requestEnrichment.js';
4+
import type { PRAgentRequest, SocialPlatformConfig } from '../types.js';
5+
6+
/**
7+
* Generates content for a specific platform using existing config
8+
* @param platform - Platform ID (instagram, facebook, pressemitteilung)
9+
* @param enrichedState - Pre-enriched request with documents, knowledge, etc.
10+
* @param req - Express request object
11+
*/
12+
export async function generatePlatformContent(
13+
platform: string,
14+
enrichedState: EnrichedState,
15+
req: any
16+
): Promise<string> {
17+
console.log(`[PR Agent] Generating ${platform} content`);
18+
19+
try {
20+
const request = enrichedState.request as PRAgentRequest;
21+
const socialConfig = loadPromptConfig('social');
22+
const platformConfig = socialConfig.platforms?.[platform] as SocialPlatformConfig | undefined;
23+
24+
if (!platformConfig) {
25+
throw new Error(`Platform config not found: ${platform}`);
26+
}
27+
28+
let systemRole = socialConfig.systemRole;
29+
if (socialConfig.systemRoleExtensions?.[platform]) {
30+
systemRole += '\n\n' + socialConfig.systemRoleExtensions[platform];
31+
}
32+
33+
const constraints = buildConstraints(socialConfig, { platforms: [platform] });
34+
35+
const requestContent = buildRequestContent(socialConfig, {
36+
inhalt: request.inhalt,
37+
platforms: [platform],
38+
zitatgeber: request.zitatgeber,
39+
was: request.was,
40+
wie: request.wie
41+
});
42+
43+
const promptResult = await assemblePromptGraphAsync({
44+
...enrichedState,
45+
systemRole,
46+
constraints,
47+
request: requestContent,
48+
taskInstructions: SimpleTemplateEngine.render(
49+
socialConfig.taskInstructions,
50+
{ platforms: platform }
51+
)
52+
});
53+
54+
const aiResult = await req.app.locals.aiWorkerPool.processRequest({
55+
type: 'social',
56+
usePrivacyMode: request.usePrivacyMode || false,
57+
systemPrompt: promptResult.system,
58+
messages: promptResult.messages,
59+
options: {
60+
max_tokens: platformConfig.maxLength * 2,
61+
temperature: socialConfig.options?.temperature || 0.6,
62+
top_p: platformConfig.top_p || 0.9
63+
}
64+
}, req);
65+
66+
return aiResult.content || aiResult.data?.content || '';
67+
68+
} catch (error) {
69+
console.error(`[PR Agent] Error generating ${platform} content:`, error);
70+
return `[Fehler bei der Generierung des ${platform} Inhalts]`;
71+
}
72+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { assemblePromptGraphAsync } from '../../promptAssemblyGraph.js';
2+
import { MARKDOWN_FORMATTING_INSTRUCTIONS } from '../../../../utils/prompt/index.js';
3+
import type { EnrichedState } from '../../../../utils/types/requestEnrichment.js';
4+
5+
/**
6+
* Generates risk analysis: counter-arguments from political opponents
7+
* Runs AFTER content generation to analyze specific claims
8+
*/
9+
export async function generateRiskAnalysis(
10+
enrichedState: EnrichedState,
11+
framing: string,
12+
socialContent: Record<string, string>,
13+
pressRelease: string,
14+
req: any
15+
): Promise<string> {
16+
console.log('[PR Agent] Generating risk analysis');
17+
18+
const request = enrichedState.request as { usePrivacyMode?: boolean };
19+
20+
const systemRole = `Du bist ein kritischer Analyst für politische Kommunikation der Grünen.
21+
22+
Deine Aufgabe: Identifiziere potenzielle Angriffspunkte in der Kommunikation und bereite Counter-Speech vor.
23+
24+
Analysiere die generierten Inhalte und liefere eine kompakte Risiko-Einschätzung (1-2 Absätze):
25+
26+
1. **Counter-Speech**: Was werden politische Gegner (Union, FDP, AfD) voraussichtlich antworten?
27+
2. **Schwachstellen**: Welche Aussagen könnten angegriffen werden?
28+
3. **Sprachregelung**: Wie reagieren wir auf die offensichtlichste Kritik? (2-3 Sätze für Kommentarmoderation)
29+
30+
Sei kritisch und realistisch. Besser vorbereitet als überrascht.`;
31+
32+
const userMessage = `Strategisches Framing:
33+
${framing}
34+
35+
Generierte Pressemitteilung:
36+
${pressRelease}
37+
38+
Social Media Inhalte:
39+
- Instagram: ${socialContent.instagram}
40+
- Facebook: ${socialContent.facebook}
41+
42+
Analysiere diese Kommunikation auf Risiken und bereite Counter-Speech vor.`;
43+
44+
const promptResult = await assemblePromptGraphAsync({
45+
...enrichedState,
46+
systemRole,
47+
request: userMessage,
48+
constraints: 'Antwort: 1-2 kompakte Absätze, maximal 1000 Zeichen.',
49+
formatting: MARKDOWN_FORMATTING_INSTRUCTIONS
50+
});
51+
52+
const aiResult = await req.app.locals.aiWorkerPool.processRequest({
53+
type: 'social',
54+
usePrivacyMode: request.usePrivacyMode || false,
55+
systemPrompt: promptResult.system,
56+
messages: promptResult.messages,
57+
options: {
58+
max_tokens: 800,
59+
temperature: 0.6,
60+
top_p: 0.85
61+
}
62+
}, req);
63+
64+
return aiResult.content || aiResult.data?.content || '';
65+
}

0 commit comments

Comments
 (0)