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
5 changes: 5 additions & 0 deletions flutter_app/lib/src/agent_llm_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'models.dart';
import 'native_bridge.dart';
import 'native_tool_router.dart';
import 'prompt_context.dart';
import 'public_study_tools.dart';
import 'studyos_tool_catalog.dart';
import 'studyos_tool_executor.dart';

Expand All @@ -31,6 +32,7 @@ class AgentLlmRequest {
this.searchTalks = _unavailableTalks,
required this.mailTools,
required this.onToolTrace,
this.publicStudyTools,
this.onDelta,
this.cancelToken,
});
Expand All @@ -47,6 +49,7 @@ class AgentLlmRequest {
final Future<String> Function() readAcademicStatus;
final Future<String> Function(String query, int limit) searchTalks;
final MailToolRunner mailTools;
final PublicStudyToolRunner? publicStudyTools;
final void Function(ToolTrace trace) onToolTrace;

/// Optional sink for streamed reply fragments. Cloud uses it for SSE; the
Expand Down Expand Up @@ -150,6 +153,7 @@ class LocalNativeLlmProvider implements AgentLlmProvider {
searchTalks: request.searchTalks,
mailTools: request.mailTools,
nativeTools: nativeTools,
publicStudyTools: request.publicStudyTools,
);

for (var round = 0; round < _maxToolRounds; round += 1) {
Expand Down Expand Up @@ -337,6 +341,7 @@ class CloudLlmProvider implements AgentLlmProvider {
readSchedule: request.readSchedule,
searchTalks: request.searchTalks,
mailTools: request.mailTools,
publicStudyTools: request.publicStudyTools,
onToolTrace: request.onToolTrace,
onDelta: request.onDelta,
cancelToken: request.cancelToken,
Expand Down
3 changes: 3 additions & 0 deletions flutter_app/lib/src/agent_message_sender.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'memory_store.dart';
import 'models.dart';
import 'native_bridge.dart';
import 'prompt_context.dart';
import 'public_study_tools.dart';

Future<String> _unavailableTalks(String query, int limit) async =>
'Tübingen Talks are not available.';
Expand All @@ -27,6 +28,7 @@ Future<String> sendAgentMessage({
Future<String> Function(String query, int limit) searchTalks =
_unavailableTalks,
required MailToolRunner mailTools,
required PublicStudyToolRunner publicStudyTools,
required void Function(ToolTrace trace) onToolTrace,
AgentStreamSink? onDelta,
AgentCancelToken? cancelToken,
Expand Down Expand Up @@ -54,6 +56,7 @@ Future<String> sendAgentMessage({
readAcademicStatus: readAcademicStatus,
searchTalks: searchTalks,
mailTools: mailTools,
publicStudyTools: publicStudyTools,
onDelta: onDelta,
cancelToken: cancelToken,
);
Expand Down
3 changes: 3 additions & 0 deletions flutter_app/lib/src/agent_request_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'models.dart';
import 'native_bridge.dart';
import 'native_tool_router.dart';
import 'prompt_context.dart';
import 'public_study_tools.dart';

Future<String> _unavailableAcademicStatus() async =>
'Academic status is not available.';
Expand Down Expand Up @@ -53,6 +54,7 @@ class AgentRequestRunner {
Future<String> Function(String query, int limit) searchTalks =
_unavailableTalks,
required MailToolRunner mailTools,
PublicStudyToolRunner? publicStudyTools,
AgentStreamSink? onDelta,
AgentCancelToken? cancelToken,
}) async {
Expand All @@ -71,6 +73,7 @@ class AgentRequestRunner {
readAcademicStatus: readAcademicStatus,
searchTalks: searchTalks,
mailTools: mailTools,
publicStudyTools: publicStudyTools,
onToolTrace: onToolTrace,
onDelta: onDelta,
cancelToken: cancelToken,
Expand Down
4 changes: 4 additions & 0 deletions flutter_app/lib/src/app_shell_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'native_bridge.dart';
import 'official_document_models.dart';
import 'official_documents_repository.dart';
import 'profile_context.dart';
import 'public_study_tools.dart';
import 'send_error_message.dart';
import 'session_store.dart';
import 'timetable_repository.dart';
Expand Down Expand Up @@ -75,6 +76,7 @@ class AppShellController extends ChangeNotifier {
final AcademicRepository _academicRepository = AcademicRepository();
final OfficialDocumentsRepository _documentsRepository =
OfficialDocumentsRepository();
final PublicStudyToolRunner _publicStudyTools = LivePublicStudyToolRunner();
final TextEditingController inputController = TextEditingController();
final ScrollController messageScrollController = ScrollController();

Expand Down Expand Up @@ -199,6 +201,7 @@ class AppShellController extends ChangeNotifier {
_streamNotifyTimer?.cancel();
_eventSubscription?.cancel();
voice.dispose();
_publicStudyTools.close();
if (_ownsTalksRepository) talksRepository.dispose();
inputController.dispose();
messageScrollController.dispose();
Expand Down Expand Up @@ -474,6 +477,7 @@ class AppShellController extends ChangeNotifier {
repository: _mailRepository,
profile: _profile,
),
publicStudyTools: _publicStudyTools,
onToolTrace: addToolTrace,
onDelta: _handleStreamDelta,
cancelToken: cancelToken,
Expand Down
14 changes: 9 additions & 5 deletions flutter_app/lib/src/campus_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import 'package:http/http.dart' as http;
import 'campus_models.dart';

class CampusClient {
CampusClient({http.Client? httpClient})
: _httpClient = httpClient ?? http.Client();
CampusClient({
http.Client? httpClient,
this.timeout = const Duration(seconds: 12),
}) : _httpClient = httpClient ?? http.Client();

static const _mealplanUrl =
'https://www.my-stuwe.de/wp-json/mealplans/v1/canteens?lang=de';
static final sourceUri = Uri.parse(
'https://www.my-stuwe.de/wp-json/mealplans/v1/canteens?lang=de',
);
static const _tuebingenCanteenIds = <String>{
'611',
'621',
Expand All @@ -19,9 +22,10 @@ class CampusClient {
};

final http.Client _httpClient;
final Duration timeout;

Future<List<CampusCanteen>> fetchTuebingenCanteens() async {
final response = await _httpClient.get(Uri.parse(_mealplanUrl));
final response = await _httpClient.get(sourceUri).timeout(timeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw CampusException('Mensa data returned HTTP ${response.statusCode}.');
}
Expand Down
100 changes: 100 additions & 0 deletions flutter_app/lib/src/capability_result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
enum CapabilityState { fresh, stale, empty, unavailable, failed }

enum CapabilityPrivacy { publicExternal, privateLocal }

enum CapabilityEffect { readOnly, userApproved }

class CapabilityPolicy {
const CapabilityPolicy({required this.privacy, required this.effect});

static const publicRead = CapabilityPolicy(
privacy: CapabilityPrivacy.publicExternal,
effect: CapabilityEffect.readOnly,
);

final CapabilityPrivacy privacy;
final CapabilityEffect effect;

Map<String, Object?> toJson() => <String, Object?>{
'privacy': _snakeCase(privacy.name),
'effect': _snakeCase(effect.name),
};
}

class CapabilitySource {
const CapabilitySource({
required this.id,
required this.label,
required this.url,
this.reliability = 'live',
});

final String id;
final String label;
final String url;
final String reliability;

Map<String, Object?> toJson() => <String, Object?>{
'id': id,
'label': label,
'url': url,
'reliability': reliability,
};
}

class CapabilityResult<T> {
const CapabilityResult({
required this.state,
required this.policy,
required this.source,
required this.fetchedAt,
this.expiresAt,
this.data,
this.message,
});

final CapabilityState state;
final CapabilityPolicy policy;
final CapabilitySource source;
final DateTime fetchedAt;
final DateTime? expiresAt;
final T? data;
final String? message;

Map<String, Object?> toJson(
Object? Function(T value) encodeData,
) => <String, Object?>{
'state': state.name,
'policy': policy.toJson(),
'source': source.toJson(),
'fetched_at': fetchedAt.toUtc().toIso8601String(),
if (expiresAt != null) 'expires_at': expiresAt!.toUtc().toIso8601String(),
if (data != null) 'data': encodeData(data as T),
if (message != null) 'message': message,
};
}

String boundedCapabilityMessage(Object error, {int maxLength = 240}) {
final text = error
.toString()
.replaceAll(RegExp(r'\s+'), ' ')
.trim()
.replaceAllMapped(
RegExp(
r'(password|authorization|cookie|token|api[_-]?key|secret)=[^\s]+',
caseSensitive: false,
),
(match) => '${match.group(1)}=[redacted]',
)
.replaceAllMapped(
RegExp(r'Bearer\s+[^\s]+', caseSensitive: false),
(_) => 'Bearer [redacted]',
);
if (text.isEmpty) return 'The live data source failed.';
return text.length <= maxLength ? text : '${text.substring(0, maxLength)}…';
}

String _snakeCase(String value) => value.replaceAllMapped(
RegExp(r'([a-z0-9])([A-Z])'),
(match) => '${match.group(1)}_${match.group(2)!.toLowerCase()}',
);
3 changes: 3 additions & 0 deletions flutter_app/lib/src/cloud_agent_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'mail_tools.dart';
import 'models.dart';
import 'native_tool_router.dart';
import 'prompt_context.dart';
import 'public_study_tools.dart';
import 'studyos_tool_catalog.dart';
import 'studyos_tool_executor.dart';

Expand Down Expand Up @@ -46,6 +47,7 @@ class CloudAgentClient {
Future<String> Function(String query, int limit) searchTalks =
_unavailableTalks,
required MailToolRunner mailTools,
PublicStudyToolRunner? publicStudyTools,
void Function(ToolTrace trace)? onToolTrace,
AgentStreamSink? onDelta,
AgentCancelToken? cancelToken,
Expand Down Expand Up @@ -83,6 +85,7 @@ class CloudAgentClient {
searchTalks: searchTalks,
mailTools: mailTools,
nativeTools: _nativeTools,
publicStudyTools: publicStudyTools,
);
for (var round = 0; ; round += 1) {
final message = await _fetchTurn(
Expand Down
26 changes: 17 additions & 9 deletions flutter_app/lib/src/map_search_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import 'package:http/http.dart' as http;
import 'map_location_models.dart';

class MapSearchClient {
MapSearchClient({http.Client? client})
: _client = client ?? http.Client(),
_ownsClient = client == null;
MapSearchClient({
http.Client? client,
this.timeout = const Duration(seconds: 12),
}) : _client = client ?? http.Client(),
_ownsClient = client == null;

final http.Client _client;
final bool _ownsClient;
final Duration timeout;
static final sourceUri = Uri.parse(
'https://nominatim.openstreetmap.org/search',
);

Future<List<MapLocation>> search(String query) async {
final trimmed = query.trim();
Expand All @@ -21,12 +27,14 @@ class MapSearchClient {
'viewbox': '8.93,48.57,9.16,48.47',
'q': '$trimmed Tuebingen',
});
final response = await _client.get(
uri,
headers: const <String, String>{
'User-Agent': 'StudyOS Agent course prototype',
},
);
final response = await _client
.get(
uri,
headers: const <String, String>{
'User-Agent': 'StudyOS Agent course prototype',
},
)
.timeout(timeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw MapSearchException(
'Search failed with HTTP ${response.statusCode}.',
Expand Down
Loading