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
1 change: 1 addition & 0 deletions chat_core/lib/chat_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export 'src/service/chat_service.dart';
export 'src/service/portkey_chat_service.dart';
export 'src/tool/tool.dart';
export 'src/tool/tool_registry.dart';
export 'src/toolbox/weather_tool.dart';
71 changes: 71 additions & 0 deletions chat_core/lib/src/toolbox/weather_tool.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:http/http.dart' as http;

import '../tool/tool.dart';

class WeatherTool implements Tool {
WeatherTool({http.Client? httpClient})
: _httpClient = httpClient ?? http.Client();

final http.Client _httpClient;

@override
String get name => 'get_weather';

@override
String get description =>
'Search for a city and get its current weather. '
'Returns geocoding results (may include multiple matches) '
'and weather data as raw JSON.';

@override
Map<String, dynamic> get parameters => {
'type': 'object',
'properties': {
'city': {
'type': 'string',
'description': 'City name (e.g. Tokyo, San Jose, Newark)',
},
},
'required': ['city'],
};

@override
Future<String> execute(Map<String, dynamic> arguments) async {
final city = arguments['city'] as String;

final geocodingResult = await _fetch(
'https://geocoding-api.open-meteo.com/v1/search?name=$city&count=5',
);
if (geocodingResult == null) return 'Failed to geocode "$city".';

final weatherResult = await _fetch(
'https://api.open-meteo.com/v1/forecast'
'?latitude=${_firstLat(geocodingResult)}'
'&longitude=${_firstLon(geocodingResult)}'
'&current=temperature_2m,weather_code',
);

return 'Geocoding results:\n$geocodingResult\n\n'
'Weather (for first match):\n${weatherResult ?? "Failed to fetch weather."}';
}

String? _firstLat(String geocodingJson) {
final match = RegExp(r'"latitude":\s*([\d.-]+)').firstMatch(geocodingJson);
return match?.group(1);
}

String? _firstLon(String geocodingJson) {
final match = RegExp(r'"longitude":\s*([\d.-]+)').firstMatch(geocodingJson);
return match?.group(1);
}

Future<String?> _fetch(String url) async {
try {
final response = await _httpClient.get(Uri.parse(url));
if (response.statusCode != 200) return null;
return response.body;
} on Exception {
return null;
}
}
}
38 changes: 38 additions & 0 deletions chat_core/test/integration/portkey_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,44 @@ void main() {
});
},
);

group(
'WeatherTool integration',
skip: config == null ? 'no local.properties' : null,
() {
late AgentService agent;

setUp(() {
final registry = ToolRegistry();
registry.register(WeatherTool());
agent = AgentService(
baseChatService: PortkeyChatService(PortkeyClient(config: config!)),
toolRegistry: registry,
);
});

tearDown(() {
agent.close();
});

test('LLM calls weather tool and returns weather info', () async {
final events = await agent.chat([
Message.user(
'Use the get_weather tool to check the weather in San Jose. '
'I mean San Jose, California, US. '
'Reply with the city, state, temperature and condition.',
),
]).toList();

final textDeltas = events.whereType<TextDelta>().toList();
final fullText = textDeltas.map((e) => e.text).join();
print('WeatherTool: $fullText');

expect(events.whereType<ToolCallRequest>(), isNotEmpty);
expect(fullText.toLowerCase(), contains('san jose'));
});
},
);
}

class _AddTool implements Tool {
Expand Down
80 changes: 80 additions & 0 deletions chat_core/test/tool/weather_tool_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'dart:convert';

import 'package:chat_core/chat_core.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:test/test.dart';

void main() {
group('WeatherTool', () {
test('returns geocoding and weather raw data', () async {
final mockClient = MockClient((request) async {
if (request.url.host == 'geocoding-api.open-meteo.com') {
return http.Response(
jsonEncode({
'results': [
{
'name': 'San Jose',
'country': 'United States',
'admin1': 'California',
'latitude': 37.3382,
'longitude': -121.8863,
},
{
'name': 'San José',
'country': 'Costa Rica',
'latitude': 9.9281,
'longitude': -84.0907,
},
],
}),
200,
);
}
return http.Response(
jsonEncode({
'current': {'temperature_2m': 18.5, 'weather_code': 2},
}),
200,
);
});

final tool = WeatherTool(httpClient: mockClient);
final result = await tool.execute({'city': 'San Jose'});

expect(result, contains('San Jose'));
expect(result, contains('Costa Rica'));
expect(result, contains('California'));
expect(result, contains('18.5'));
});

test('returns error for unknown city', () async {
final mockClient = MockClient((request) async {
return http.Response(jsonEncode({}), 200);
});

final tool = WeatherTool(httpClient: mockClient);
final result = await tool.execute({'city': 'Nonexistent'});

expect(result, contains('Geocoding results'));
});

test('handles HTTP error', () async {
final mockClient = MockClient((request) async {
return http.Response('Server Error', 500);
});

final tool = WeatherTool(httpClient: mockClient);
final result = await tool.execute({'city': 'Tokyo'});

expect(result, contains('Failed to geocode'));
});

test('has correct tool definition', () {
final tool = WeatherTool();
expect(tool.name, 'get_weather');
expect(tool.description, isNotEmpty);
expect(tool.parameters['required'], contains('city'));
});
});
}
Loading