Auger/lib/services/duriin_service.dart

117 lines
3.4 KiB
Dart

import 'dart:convert';
import 'package:capstone_project/utils/agrigator.dart';
import 'package:http/http.dart' as http;
const _baseUrl = 'http://duriin.imbenji.net';
// pair of (article, semantic distance from the query / seed). distance is
// null when the underlying endpoint doesnt return one.
class SimilarHit {
final FeedItem item;
final double? distance;
SimilarHit({required this.item, this.distance});
}
class DuriinService {
Future<List<FeedItem>> search({
required String ticker,
required String companyName,
required DateTime from,
required DateTime to,
int limit = 100,
}) async {
final fromStr = from.toUtc().toIso8601String();
final toStr = to.toUtc().toIso8601String();
// build the keyword set: the ticker itself plus the company brand name
// (first token of the registered name — "NVIDIA Corporation" -> "NVIDIA",
// "Apple Inc" -> "Apple"). the full legal name rarely appears verbatim in
// articles so matching on it would miss most hits.
final keywords = <String>{};
final t = ticker.trim();
if (t.isNotEmpty) keywords.add(t);
final brand = companyName.trim().split(RegExp(r"\s+")).firstOrNull ?? "";
if (brand.isNotEmpty) keywords.add(brand);
final hits = await _fetchHits(queryParams: {
'keyword': keywords.toList(),
'keyword_mode': 'or',
'from': fromStr,
'to': toStr,
'limit': '$limit',
'order': 'newest',
});
final items = hits.map((h) => h.item).toList();
print('Duriin: ${items.length} articles for $ticker / $companyName (kw=${keywords.join(",")})');
return items;
}
// neighbours of a given article via the vector index. used by the event
// clusterer to build real clusters instead of arbitrary time slices.
Future<List<SimilarHit>> findSimilar(int articleId, {int limit = 25}) async {
return _fetchHits(queryParams: {
'similar_to_article': '$articleId',
'limit': '$limit',
});
}
Future<List<SimilarHit>> _fetchHits({required Map<String, dynamic> queryParams}) async {
final uri = Uri.parse(_baseUrl).replace(
path: '/articles',
queryParameters: queryParams,
);
try {
final response = await http.get(uri);
if (response.statusCode != 200) {
print('Duriin: HTTP ${response.statusCode} for $uri');
return [];
}
final body = response.body.trim();
if (body.isEmpty) return [];
final list = jsonDecode(body) as List<dynamic>;
return list.map((raw) {
final m = raw as Map<String, dynamic>;
final id = m['id'] is int
? m['id'] as int
: int.tryParse('${m['id']}');
final item = FeedItem(
id: id,
title: (m['title'] ?? '').toString(),
description: (m['description'] ?? '').toString(),
content: (m['content'] ?? '').toString(),
link: (m['url'] ?? '').toString(),
source: (m['source'] ?? '').toString().isNotEmpty ? m['source'].toString() : null,
pubDate: m['pub_date'] != null ? DateTime.tryParse(m['pub_date'].toString()) : null,
);
final distRaw = m['distance'];
final distance = distRaw is num ? distRaw.toDouble() : null;
return SimilarHit(item: item, distance: distance);
}).where((h) => h.item.title.isNotEmpty && h.item.link.isNotEmpty).toList();
} catch (e) {
print('Duriin fetch error: $e');
return [];
}
}
}