88 lines
2.8 KiB
Dart
88 lines
2.8 KiB
Dart
import '../models/article.dart';
|
|||
|
|
import '../models/briefing.dart';
|
||
|
|
import '../models/news_source.dart';
|
||
|
|
|
||
|
|
class SummarizerException implements Exception {
|
||
|
|
SummarizerException(this.message);
|
||
|
|
|
||
|
|
final String message;
|
||
|
|
|
||
|
|
@override
|
||
|
|
String toString() => message;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Something that can turn articles into summaries. Implemented by the
|
||
|
|
/// direct Claude API client and by the n8n webhook bridge.
|
||
|
|
abstract class Summarizer {
|
||
|
|
String get name;
|
||
|
|
|
||
|
|
Future<String> summarizeArticle(Article article, String text, SourceLanguage language);
|
||
|
|
|
||
|
|
Future<DailyBriefing> createBriefing(List<Article> articles, SourceLanguage language);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Shared prompt material and response mapping for all summarizers.
|
||
|
|
class BriefingInput {
|
||
|
|
BriefingInput(List<Article> articles)
|
||
|
|
: byId = {for (var i = 0; i < articles.length; i++) 'a${i + 1}': articles[i]};
|
||
|
|
|
||
|
|
/// Short ids keep the prompt compact and are easy for the model to cite.
|
||
|
|
final Map<String, Article> byId;
|
||
|
|
|
||
|
|
static const excerptChars = 220;
|
||
|
|
|
||
|
|
String toPromptList() {
|
||
|
|
final buffer = StringBuffer();
|
||
|
|
byId.forEach((id, a) {
|
||
|
|
final excerpt = a.excerpt.length > excerptChars
|
||
|
|
? '${a.excerpt.substring(0, excerptChars)}…'
|
||
|
|
: a.excerpt;
|
||
|
|
buffer.writeln('[$id] (${a.source.name}) ${a.title}'
|
||
|
|
'${excerpt.isEmpty ? '' : ' — $excerpt'}');
|
||
|
|
});
|
||
|
|
return buffer.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
List<Map<String, dynamic>> toJsonList() => [
|
||
|
|
for (final e in byId.entries)
|
||
|
|
{
|
||
|
|
'id': e.key,
|
||
|
|
'title': e.value.title,
|
||
|
|
'url': e.value.link,
|
||
|
|
'source': e.value.source.name,
|
||
|
|
'language': e.value.source.language.name,
|
||
|
|
'published': e.value.published?.toIso8601String(),
|
||
|
|
'excerpt': e.value.excerpt,
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
/// Maps `{intro, topics: [{title, summary, article_ids}]}` onto a briefing.
|
||
|
|
DailyBriefing toBriefing(Map<String, dynamic> json, String generatedBy) {
|
||
|
|
final topics = (json['topics'] as List? ?? []).map((t) {
|
||
|
|
final topic = t as Map<String, dynamic>;
|
||
|
|
final ids = (topic['article_ids'] as List? ?? []).cast<String>();
|
||
|
|
return BriefingTopic(
|
||
|
|
title: topic['title'] as String? ?? '',
|
||
|
|
summary: topic['summary'] as String? ?? '',
|
||
|
|
sources: [
|
||
|
|
for (final id in ids)
|
||
|
|
if (byId[id] != null)
|
||
|
|
ArticleRef(
|
||
|
|
title: byId[id]!.title,
|
||
|
|
url: byId[id]!.link,
|
||
|
|
sourceName: byId[id]!.source.name,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}).toList();
|
||
|
|
return DailyBriefing(
|
||
|
|
createdAt: DateTime.now(),
|
||
|
|
intro: json['intro'] as String? ?? '',
|
||
|
|
topics: topics,
|
||
|
|
generatedBy: generatedBy,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
String languageName(SourceLanguage language) =>
|
||
|
|
language == SourceLanguage.de ? 'German' : 'English';
|