Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions apps/api/agents/langgraph/PRAgent/generators/argumentsGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Arguments Generator
* Searches Green Party knowledge bases for relevant arguments using Qdrant
*/

import { getQdrantInstance } from '../../../../database/services/QdrantService/index.js';
import { mistralEmbeddingService } from '../../../../services/mistral/MistralEmbeddingService/index.js';
import { SYSTEM_COLLECTIONS } from '../../../../config/systemCollectionsConfig.js';

export interface ArgumentResult {
source: string;
text: string;
relevance: number;
metadata: {
collection: string;
category?: string;
contentType?: string;
url?: string;
};
}

/**
* Search for relevant arguments from Green Party knowledge bases
* Uses Qdrant multi-collection hybrid search
*/
export async function searchArgumentsFromNotebooks(
topic: string,
options: {
collections?: string[];
limit?: number;
threshold?: number;
} = {}
): Promise<ArgumentResult[]> {
const {
collections = [
'grundsatz_documents', // Grundsatzprogramme
'bundestag_content', // Bundestagsfraktion content
'kommunalwiki_documents', // KommunalWiki
'gruene_de_documents', // gruene.de
'gruene_at_documents' // gruene.at
],
limit = 10,
threshold = 0.35
} = options;

const qdrant = getQdrantInstance();

// Initialize services
await qdrant.init();
await mistralEmbeddingService.init();

if (!qdrant.isAvailable() || !mistralEmbeddingService.isReady()) {
console.warn('[ArgumentsGenerator] Qdrant or Mistral not available');
return [];
}

// Generate embedding for the topic
let topicEmbedding: number[];
try {
topicEmbedding = await mistralEmbeddingService.generateEmbedding(topic);
} catch (error) {
console.error('[ArgumentsGenerator] Embedding generation failed:', error);
return [];
}

// Search across all collections in parallel
const searchPromises = collections.map(async (collection) => {
try {
// Use hybrid search (vector + text) for better precision
const result = await qdrant.hybridSearchDocuments(
topicEmbedding,
topic,
{
collection,
limit: Math.ceil(limit / collections.length) + 5, // Over-fetch per collection
threshold
}
);

return result.results.map(r => ({
source: (r.title as string) || (r.document_id as string) || collection,
text: (r.chunk_text as string) || '',
relevance: r.score || 0,
metadata: {
collection,
category: (r.section as string) || '',
contentType: (r.metadata?.content_type as string) || '',
url: (r.url as string) || ''
}
}));
} catch (error) {
console.error(`[ArgumentsGenerator] Search failed for ${collection}:`, error);
return [];
}
});

const results = await Promise.all(searchPromises);

// Merge, deduplicate, and sort by relevance
const allArguments = results.flat();

// Deduplicate by text content (keep highest relevance)
const uniqueArguments = Array.from(
new Map(allArguments.map(a => [a.text, a])).values()
);

return uniqueArguments
.sort((a, b) => b.relevance - a.relevance)
.slice(0, limit);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import mistralClient from '../../../../workers/mistralClient.js';
import type { ArgumentResult } from './argumentsGenerator.js';

/**
* Generate a concise summary of research arguments using Mistral Small
* Summarizes the key findings from Green Party knowledge bases
*/
export async function summarizeArguments(
topic: string,
argumentsList: ArgumentResult[]
): Promise<string> {
if (!argumentsList || argumentsList.length === 0) {
return 'Keine relevanten Argumente gefunden.';
}

const argumentsText = argumentsList.map((arg, idx) =>
`${idx + 1}. **${arg.source}** (Relevanz: ${Math.round(arg.relevance * 100)}%)\n ${arg.text}`
).join('\n\n');

const prompt = `Du bist ein grüner Kommunikationsberater.

**Aufgabe**: Fasse die folgenden recherchierten Argumente aus grünen Wissensdatenbanken zu einem prägnanten, übersichtlichen Summary zusammen.

**Thema**: ${topic}

**Recherchierte Argumente**:

${argumentsText}

**Deine Antwort**:
Erstelle eine strukturierte Zusammenfassung mit:
- **Kernaussagen**: Die 2-3 wichtigsten Argumente (als Stichpunkte)
- **Quellenbasis**: Welche Dokumente/Programme liefern die stärksten Belege?
- **Nutzungshinweis**: Wie können diese Argumente in der Kommunikation verwendet werden?

Halte die Zusammenfassung kurz (max. 200 Wörter), präzise und sofort nutzbar.`;

try {
const response = await mistralClient.chat.complete({
model: 'mistral-small-latest',
messages: [
{
role: 'user',
content: prompt
}
],
max_tokens: 500,
temperature: 0.3
});

const summary = response.choices?.[0]?.message?.content || '';
return summary || 'Zusammenfassung konnte nicht erstellt werden.';
} catch (error) {
console.error('[ArgumentsSummarizer] Failed to generate summary:', error);
return `**Recherchierte Argumente (${argumentsList.length})**\n\nDie Recherche hat ${argumentsList.length} relevante Argumente aus grünen Wissensdatenbanken gefunden. Bitte siehe Details unten.`;
}
}
60 changes: 60 additions & 0 deletions apps/api/agents/langgraph/PRAgent/generators/framingGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { assemblePromptGraphAsync } from '../../promptAssemblyGraph.js';
import { MARKDOWN_FORMATTING_INSTRUCTIONS } from '../../../../utils/prompt/index.js';
import type { EnrichedState } from '../../../../utils/types/requestEnrichment.js';
import type { PRAgentRequest } from '../types.js';

/**
* Generates strategic framing: narrative, values, audiences, wording
*/
export async function generateStrategicFraming(
enrichedState: EnrichedState,
req: any
): Promise<string> {
console.log('[PR Agent] Generating strategic framing');

const request = enrichedState.request as PRAgentRequest;

const systemRole = `Du bist ein erfahrener strategischer Kommunikationsberater für Bündnis 90/Die Grünen.

Deine Aufgabe ist es, das strategische Framing für politische Kommunikation zu entwickeln - bevor die eigentlichen Texte geschrieben werden.

Analysiere das Thema und liefere eine kompakte strategische Einschätzung (1-2 Absätze) mit folgenden Elementen:

1. **Grüner Kern**: Wie verknüpfen wir das Thema mit unseren Grundwerten (Klimaschutz, soziale Gerechtigkeit, Freiheit, wirtschaftliche Modernisierung)?

2. **Zielgruppen-Ansprache**:
- Wen wollen wir erreichen? (eigene Basis vs. bürgerliche Mitte)
- Emotionalisierung (Herz) oder Faktenfokus (Kopf)?

3. **Wording**: Welche Begriffe nutzen wir, um das Thema positiv zu besetzen? (z.B. "Schutz" statt "Verbot", "Innovation" statt "Regulierung")

4. **Narrativ**: In einen Satz: Was ist die Geschichte, die wir erzählen wollen?

Sei präzise und strategisch. Dies ist die Grundlage für alle nachfolgenden Kommunikationsmaßnahmen.`;

const userMessage = `Thema: ${request.inhalt}

Entwickle das strategische Framing für dieses Thema.`;

const promptResult = await assemblePromptGraphAsync({
...enrichedState,
systemRole,
request: userMessage,
constraints: 'Antwort: 1-2 kompakte Absätze, maximal 800 Zeichen.',
formatting: MARKDOWN_FORMATTING_INSTRUCTIONS
});

const aiResult = await req.app.locals.aiWorkerPool.processRequest({
type: 'social',
usePrivacyMode: request.usePrivacyMode || false,
systemPrompt: promptResult.system,
messages: promptResult.messages,
options: {
max_tokens: 600,
temperature: 0.7,
top_p: 0.9
}
}, req);

return aiResult.content || aiResult.data?.content || '';
}
72 changes: 72 additions & 0 deletions apps/api/agents/langgraph/PRAgent/generators/platformGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { loadPromptConfig, buildConstraints, buildRequestContent, SimpleTemplateEngine } from '../../PromptProcessor.js';
import { assemblePromptGraphAsync } from '../../promptAssemblyGraph.js';
import type { EnrichedState } from '../../../../utils/types/requestEnrichment.js';
import type { PRAgentRequest, SocialPlatformConfig } from '../types.js';

/**
* Generates content for a specific platform using existing config
* @param platform - Platform ID (instagram, facebook, pressemitteilung)
* @param enrichedState - Pre-enriched request with documents, knowledge, etc.
* @param req - Express request object
*/
export async function generatePlatformContent(
platform: string,
enrichedState: EnrichedState,
req: any
): Promise<string> {
console.log(`[PR Agent] Generating ${platform} content`);

try {
const request = enrichedState.request as PRAgentRequest;
const socialConfig = loadPromptConfig('social');
const platformConfig = socialConfig.platforms?.[platform] as SocialPlatformConfig | undefined;

if (!platformConfig) {
throw new Error(`Platform config not found: ${platform}`);
}

let systemRole = socialConfig.systemRole;
if (socialConfig.systemRoleExtensions?.[platform]) {
systemRole += '\n\n' + socialConfig.systemRoleExtensions[platform];
}

const constraints = buildConstraints(socialConfig, { platforms: [platform] });

const requestContent = buildRequestContent(socialConfig, {
inhalt: request.inhalt,
platforms: [platform],
zitatgeber: request.zitatgeber,
was: request.was,
wie: request.wie
});

const promptResult = await assemblePromptGraphAsync({
...enrichedState,
systemRole,
constraints,
request: requestContent,
taskInstructions: SimpleTemplateEngine.render(
socialConfig.taskInstructions,
{ platforms: platform }
)
});

const aiResult = await req.app.locals.aiWorkerPool.processRequest({
type: 'social',
usePrivacyMode: request.usePrivacyMode || false,
systemPrompt: promptResult.system,
messages: promptResult.messages,
options: {
max_tokens: platformConfig.maxLength * 2,
temperature: socialConfig.options?.temperature || 0.6,
top_p: platformConfig.top_p || 0.9
}
}, req);

return aiResult.content || aiResult.data?.content || '';

} catch (error) {
console.error(`[PR Agent] Error generating ${platform} content:`, error);
return `[Fehler bei der Generierung des ${platform} Inhalts]`;
}
}
65 changes: 65 additions & 0 deletions apps/api/agents/langgraph/PRAgent/generators/riskGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { assemblePromptGraphAsync } from '../../promptAssemblyGraph.js';
import { MARKDOWN_FORMATTING_INSTRUCTIONS } from '../../../../utils/prompt/index.js';
import type { EnrichedState } from '../../../../utils/types/requestEnrichment.js';

/**
* Generates risk analysis: counter-arguments from political opponents
* Runs AFTER content generation to analyze specific claims
*/
export async function generateRiskAnalysis(
enrichedState: EnrichedState,
framing: string,
socialContent: Record<string, string>,
pressRelease: string,
req: any
): Promise<string> {
console.log('[PR Agent] Generating risk analysis');

const request = enrichedState.request as { usePrivacyMode?: boolean };

const systemRole = `Du bist ein kritischer Analyst für politische Kommunikation der Grünen.

Deine Aufgabe: Identifiziere potenzielle Angriffspunkte in der Kommunikation und bereite Counter-Speech vor.

Analysiere die generierten Inhalte und liefere eine kompakte Risiko-Einschätzung (1-2 Absätze):

1. **Counter-Speech**: Was werden politische Gegner (Union, FDP, AfD) voraussichtlich antworten?
2. **Schwachstellen**: Welche Aussagen könnten angegriffen werden?
3. **Sprachregelung**: Wie reagieren wir auf die offensichtlichste Kritik? (2-3 Sätze für Kommentarmoderation)

Sei kritisch und realistisch. Besser vorbereitet als überrascht.`;

const userMessage = `Strategisches Framing:
${framing}

Generierte Pressemitteilung:
${pressRelease}

Social Media Inhalte:
- Instagram: ${socialContent.instagram}
- Facebook: ${socialContent.facebook}

Analysiere diese Kommunikation auf Risiken und bereite Counter-Speech vor.`;

const promptResult = await assemblePromptGraphAsync({
...enrichedState,
systemRole,
request: userMessage,
constraints: 'Antwort: 1-2 kompakte Absätze, maximal 1000 Zeichen.',
formatting: MARKDOWN_FORMATTING_INSTRUCTIONS
});

const aiResult = await req.app.locals.aiWorkerPool.processRequest({
type: 'social',
usePrivacyMode: request.usePrivacyMode || false,
systemPrompt: promptResult.system,
messages: promptResult.messages,
options: {
max_tokens: 800,
temperature: 0.6,
top_p: 0.85
}
}, req);

return aiResult.content || aiResult.data?.content || '';
}
Loading