fix: tool calling

This commit is contained in:
cogwheel0
2025-09-01 16:28:49 +05:30
parent 7daf331daf
commit d801fe9371
4 changed files with 369 additions and 67 deletions

View File

@@ -69,10 +69,35 @@ class SocketService {
});
}
// Subscribe to general channel events (server-broadcasted channel updates)
void onChannelEvents(void Function(Map<String, dynamic> event) handler) {
_socket?.on('channel-events', (data) {
try {
if (data is Map<String, dynamic>) {
handler(data);
} else if (data is Map) {
handler(Map<String, dynamic>.from(data));
}
} catch (_) {}
});
}
void offChatEvents() {
_socket?.off('chat-events');
}
void offChannelEvents() {
_socket?.off('channel-events');
}
// Subscribe to an arbitrary socket.io event (used for dynamic tool channels)
void onEvent(String eventName, void Function(dynamic data) handler) {
_socket?.on(eventName, handler);
}
void offEvent(String eventName) {
_socket?.off(eventName);
}
void dispose() {
try {
_socket?.dispose();

View File

@@ -34,6 +34,19 @@ class ToolCallsContent {
/// Utility to parse <details type="tool_calls"> blocks from content
class ToolCallsParser {
static String _unescapeHtml(String s) {
return s
.replaceAll('&quot;', '"')
.replaceAll('&#34;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&#39;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&#60;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&#62;', '>')
.replaceAll('&amp;', '&')
.replaceAll('&#38;', '&');
}
/// Represents a mixed stream of text and tool-call entries in original order
/// as they appeared in the content.
static List<ToolCallsSegment>? segments(String content) {
@@ -59,12 +72,19 @@ class ToolCallsParser {
// Find end of opening tag
final openEnd = content.indexOf('>', start);
if (openEnd == -1) {
// Malformed; append rest as text
// Malformed opening tag; append the rest as text and stop
segs.add(ToolCallsSegment.text(content.substring(start)));
break;
}
final openTag = content.substring(start, openEnd + 1);
// Parse attributes from opening tag immediately (to support streaming)
final attrs = <String, String>{};
final attrRegex = RegExp(r'(\w+)="(.*?)"');
for (final m in attrRegex.allMatches(openTag)) {
attrs[m.group(1)!] = m.group(2) ?? '';
}
// Find matching closing tag with nesting support
int depth = 1;
int i = openEnd + 1;
@@ -81,28 +101,22 @@ class ToolCallsParser {
}
}
if (depth != 0) {
// Unclosed details; append the rest as text
segs.add(ToolCallsSegment.text(content.substring(start)));
break;
}
final isToolCalls = (attrs['type'] ?? '') == 'tool_calls';
final fullMatch = content.substring(start, i);
// Parse attributes from opening tag
final attrs = <String, String>{};
final attrRegex = RegExp(r'(\w+)="(.*?)"');
for (final m in attrRegex.allMatches(openTag)) {
attrs[m.group(1)!] = m.group(2) ?? '';
}
if ((attrs['type'] ?? '') == 'tool_calls') {
if (isToolCalls) {
// Decode attributes for tool call tile
dynamic _decode(String? s) {
if (s == null || s.isEmpty) return null;
try {
return json.decode(s);
final unescaped = _unescapeHtml(s);
return json.decode(unescaped);
} catch (_) {
return s;
// If JSON decode fails, return unescaped string for display
try {
return _unescapeHtml(s);
} catch (_) {
return s;
}
}
}
@@ -125,11 +139,26 @@ class ToolCallsParser {
),
),
);
} else {
segs.add(ToolCallsSegment.text(fullMatch));
// If details not closed yet, stop scanning (wait for more stream)
if (depth != 0) {
break;
}
// If closed, advance index to the end of the block
index = i;
continue;
}
index = i;
// Non-tool_calls: keep as text (full block) when closed; if not closed, append remainder and stop
if (depth != 0) {
segs.add(ToolCallsSegment.text(content.substring(start)));
break;
} else {
final fullMatch = content.substring(start, i);
segs.add(ToolCallsSegment.text(fullMatch));
index = i;
}
}
return segs.isEmpty ? null : segs;
@@ -139,6 +168,7 @@ class ToolCallsParser {
static ToolCallsContent? parse(String content) {
if (content.isEmpty || !content.contains('<details')) return null;
// We need mainContent that excludes tool_calls blocks even if unclosed (streaming)
final segs = segments(content);
if (segs == null) return null;
@@ -148,7 +178,18 @@ class ToolCallsParser {
if (seg.isToolCall && seg.entry != null) {
calls.add(seg.entry!);
} else if (seg.text != null && seg.text!.isNotEmpty) {
buf.write(seg.text);
// Remove any embedded tool_calls blocks that may have slipped into text
final cleaned = seg.text!
.replaceAll(
RegExp(
r'<details\s+type=\"tool_calls\"[^>]*>[\s\S]*?<\/details>',
multiLine: true,
dotAll: true,
),
'',
)
.trim();
if (cleaned.isNotEmpty) buf.write(cleaned);
}
}
@@ -209,4 +250,3 @@ class ToolCallsSegment {
bool get isToolCall => entry != null;
}