CODEX_ADK/FRONTEND/lib/api/endpoints/agent_endpoint.dart
Svrnty 797ee55caf fix: Make AgentDto configuration fields nullable for list endpoint compatibility
The backend /api/agents list endpoint returns a lightweight DTO without
configuration fields (temperature, maxTokens, systemPrompt, enableMemory,
conversationWindowSize). This caused a TypeError when parsing the response
as these fields were required in AgentDto.

Changes:
- Made 5 configuration fields nullable in AgentDto
- Updated constructor to accept optional values
- Fixed fromJson() to safely handle null values with explicit checks
- Maintains backward compatibility with full agent detail responses

This fix resolves the "Error Loading Agents" issue and allows the agents
page to display correctly. List endpoint now parses successfully while
detail endpoints still provide full configuration.

Fixes: TypeError: null: type 'Null' is not a subtype of type 'num'

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 00:01:21 -04:00

596 lines
18 KiB
Dart

/// Agent management endpoints for CQRS API
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../client.dart';
import '../types.dart';
// =============================================================================
// Enums
// =============================================================================
/// Specifies the type/purpose of the agent
enum AgentType {
codeGenerator('CodeGenerator'),
codeReviewer('CodeReviewer'),
debugger('Debugger'),
documenter('Documenter'),
custom('Custom');
const AgentType(this.value);
final String value;
static AgentType fromString(String value) {
return AgentType.values.firstWhere(
(type) => type.value == value,
orElse: () => AgentType.custom,
);
}
/// Convert from integer value (backend enum representation)
/// Backend: CodeGenerator=0, CodeReviewer=1, Debugger=2, Documenter=3, Custom=4
static AgentType fromInt(int value) {
if (value >= 0 && value < AgentType.values.length) {
return AgentType.values[value];
}
return AgentType.custom;
}
}
/// Represents the current status of an agent
enum AgentStatus {
active('Active'),
inactive('Inactive'),
error('Error');
const AgentStatus(this.value);
final String value;
static AgentStatus fromString(String value) {
return AgentStatus.values.firstWhere(
(status) => status.value == value,
orElse: () => AgentStatus.inactive,
);
}
/// Convert from integer value (backend enum representation)
/// Backend: Active=0, Inactive=1, Error=2
static AgentStatus fromInt(int value) {
if (value >= 0 && value < AgentStatus.values.length) {
return AgentStatus.values[value];
}
return AgentStatus.inactive;
}
}
/// Specifies the type of model provider (cloud API or local endpoint)
enum ModelProviderType {
cloudApi('CloudApi'),
localEndpoint('LocalEndpoint'),
custom('Custom');
const ModelProviderType(this.value);
final String value;
static ModelProviderType fromString(String value) {
return ModelProviderType.values.firstWhere(
(type) => type.value == value,
orElse: () => ModelProviderType.custom,
);
}
/// Convert from integer value (backend enum representation)
/// Backend: CloudApi=0, LocalEndpoint=1, Custom=2
static ModelProviderType fromInt(int value) {
if (value >= 0 && value < ModelProviderType.values.length) {
return ModelProviderType.values[value];
}
return ModelProviderType.custom;
}
}
// =============================================================================
// Commands
// =============================================================================
/// Command to create a new AI agent with configuration
class CreateAgentCommand implements Serializable {
final String name;
final String description;
final AgentType type;
final String modelProvider;
final String modelName;
final ModelProviderType providerType;
final String? modelEndpoint;
final String? apiKey;
final double temperature;
final int maxTokens;
final String systemPrompt;
final bool enableMemory;
final int conversationWindowSize;
const CreateAgentCommand({
required this.name,
required this.description,
required this.type,
required this.modelProvider,
required this.modelName,
required this.providerType,
this.modelEndpoint,
this.apiKey,
this.temperature = 0.7,
this.maxTokens = 4000,
required this.systemPrompt,
this.enableMemory = true,
this.conversationWindowSize = 10,
});
@override
Map<String, Object?> toJson() => {
'name': name,
'description': description,
'type': type.value,
'modelProvider': modelProvider,
'modelName': modelName,
'providerType': providerType.value,
'modelEndpoint': modelEndpoint,
'apiKey': apiKey,
'temperature': temperature,
'maxTokens': maxTokens,
'systemPrompt': systemPrompt,
'enableMemory': enableMemory,
'conversationWindowSize': conversationWindowSize,
};
}
/// Command to update an existing agent's configuration
class UpdateAgentCommand implements Serializable {
final String id;
final String? name;
final String? description;
final AgentType? type;
final String? modelProvider;
final String? modelName;
final ModelProviderType? providerType;
final String? modelEndpoint;
final String? apiKey;
final double? temperature;
final int? maxTokens;
final String? systemPrompt;
final bool? enableMemory;
final int? conversationWindowSize;
final AgentStatus? status;
const UpdateAgentCommand({
required this.id,
this.name,
this.description,
this.type,
this.modelProvider,
this.modelName,
this.providerType,
this.modelEndpoint,
this.apiKey,
this.temperature,
this.maxTokens,
this.systemPrompt,
this.enableMemory,
this.conversationWindowSize,
this.status,
});
@override
Map<String, Object?> toJson() => {
'id': id,
if (name != null) 'name': name,
if (description != null) 'description': description,
if (type != null) 'type': type!.value,
if (modelProvider != null) 'modelProvider': modelProvider,
if (modelName != null) 'modelName': modelName,
if (providerType != null) 'providerType': providerType!.value,
if (modelEndpoint != null) 'modelEndpoint': modelEndpoint,
if (apiKey != null) 'apiKey': apiKey,
if (temperature != null) 'temperature': temperature,
if (maxTokens != null) 'maxTokens': maxTokens,
if (systemPrompt != null) 'systemPrompt': systemPrompt,
if (enableMemory != null) 'enableMemory': enableMemory,
if (conversationWindowSize != null)
'conversationWindowSize': conversationWindowSize,
if (status != null) 'status': status!.value,
};
}
/// Command to soft-delete an agent
class DeleteAgentCommand implements Serializable {
final String id;
const DeleteAgentCommand({required this.id});
@override
Map<String, Object?> toJson() => {'id': id};
}
// =============================================================================
// Queries
// =============================================================================
/// Query to get a single agent by ID
class GetAgentQuery implements Serializable {
final String id;
const GetAgentQuery({required this.id});
@override
Map<String, Object?> toJson() => {'id': id};
}
// =============================================================================
// DTOs
// =============================================================================
/// Response containing agent details
class AgentDto {
final String id;
final String name;
final String description;
final AgentType type;
final String modelProvider;
final String modelName;
final ModelProviderType providerType;
final String? modelEndpoint;
final double? temperature;
final int? maxTokens;
final String? systemPrompt;
final bool? enableMemory;
final int? conversationWindowSize;
final AgentStatus status;
final DateTime createdAt;
final DateTime updatedAt;
const AgentDto({
required this.id,
required this.name,
required this.description,
required this.type,
required this.modelProvider,
required this.modelName,
required this.providerType,
this.modelEndpoint,
this.temperature,
this.maxTokens,
this.systemPrompt,
this.enableMemory,
this.conversationWindowSize,
required this.status,
required this.createdAt,
required this.updatedAt,
});
factory AgentDto.fromJson(Map<String, Object?> json) {
// Helper to parse enum from either int or string
AgentType parseAgentType(Object? value) {
if (value is int) return AgentType.fromInt(value);
if (value is String) return AgentType.fromString(value);
return AgentType.custom;
}
AgentStatus parseAgentStatus(Object? value) {
if (value is int) return AgentStatus.fromInt(value);
if (value is String) return AgentStatus.fromString(value);
return AgentStatus.inactive;
}
ModelProviderType parseProviderType(Object? value) {
if (value is int) return ModelProviderType.fromInt(value);
if (value is String) return ModelProviderType.fromString(value);
return ModelProviderType.custom;
}
return AgentDto(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
type: parseAgentType(json['type']),
modelProvider: json['modelProvider'] as String,
modelName: json['modelName'] as String,
providerType: parseProviderType(json['providerType']),
modelEndpoint: json['modelEndpoint'] as String?,
temperature: json['temperature'] != null ? (json['temperature'] as num).toDouble() : null,
maxTokens: json['maxTokens'] as int?,
systemPrompt: json['systemPrompt'] as String?,
enableMemory: json['enableMemory'] as bool?,
conversationWindowSize: json['conversationWindowSize'] as int?,
status: parseAgentStatus(json['status']),
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
}
Map<String, Object?> toJson() => {
'id': id,
'name': name,
'description': description,
'type': type.value,
'modelProvider': modelProvider,
'modelName': modelName,
'providerType': providerType.value,
'modelEndpoint': modelEndpoint,
'temperature': temperature,
'maxTokens': maxTokens,
'systemPrompt': systemPrompt,
'enableMemory': enableMemory,
'conversationWindowSize': conversationWindowSize,
'status': status.value,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
// =============================================================================
// Extension Methods
// =============================================================================
/// Agent management endpoints
extension AgentEndpoint on CqrsApiClient {
/// Create a new AI agent
///
/// Example:
/// ```dart
/// final result = await client.createAgent(
/// CreateAgentCommand(
/// name: 'Code Generator',
/// description: 'AI agent for code generation',
/// type: AgentType.codeGenerator,
/// modelProvider: 'ollama',
/// modelName: 'phi',
/// providerType: ModelProviderType.localEndpoint,
/// modelEndpoint: 'http://localhost:11434',
/// systemPrompt: 'You are a code generation assistant',
/// ),
/// );
/// ```
Future<Result<void>> createAgent(CreateAgentCommand command) async {
return executeCommand(
endpoint: 'createAgent',
command: command,
);
}
/// Update an existing agent's configuration
///
/// Example:
/// ```dart
/// final result = await client.updateAgent(
/// UpdateAgentCommand(
/// id: 'agent-uuid',
/// name: 'Updated Name',
/// status: AgentStatus.active,
/// ),
/// );
/// ```
Future<Result<void>> updateAgent(UpdateAgentCommand command) async {
return executeCommand(
endpoint: 'updateAgent',
command: command,
);
}
/// Soft-delete an agent
///
/// Example:
/// ```dart
/// final result = await client.deleteAgent(
/// DeleteAgentCommand(id: 'agent-uuid'),
/// );
/// ```
Future<Result<void>> deleteAgent(DeleteAgentCommand command) async {
return executeCommand(
endpoint: 'deleteAgent',
command: command,
);
}
/// Get a single agent by ID
///
/// Example:
/// ```dart
/// final result = await client.getAgent('agent-uuid');
///
/// result.when(
/// success: (agent) => print('Agent: ${agent.name}'),
/// error: (error) => print('Error: ${error.message}'),
/// );
/// ```
Future<Result<AgentDto>> getAgent(String id) async {
return executeQuery<AgentDto>(
endpoint: 'getAgent',
query: GetAgentQuery(id: id),
fromJson: (json) => AgentDto.fromJson(json as Map<String, Object?>),
);
}
/// List all agents
///
/// Returns a list of all active agents from the backend.
/// Backend endpoint: GET /api/agents
///
/// Example:
/// ```dart
/// final result = await client.listAgents();
///
/// result.when(
/// success: (agents) => print('Found ${agents.length} agents'),
/// error: (error) => print('Error: ${error.message}'),
/// );
/// ```
Future<Result<List<AgentDto>>> listAgents() async {
try {
final Uri url = Uri.parse('${config.baseUrl}/api/agents');
final http.Response response = await http
.get(url, headers: config.defaultHeaders)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
final Object? jsonData = jsonDecode(response.body);
if (jsonData is! List) {
return ApiError<List<AgentDto>>(ApiErrorInfo(
message: 'Expected array response, got ${jsonData.runtimeType}',
type: ApiErrorType.serialization,
));
}
final List<AgentDto> agents = jsonData
.map((item) => AgentDto.fromJson(item as Map<String, Object?>))
.toList();
return ApiSuccess<List<AgentDto>>(agents);
}
// Handle error responses
return ApiError<List<AgentDto>>(ApiErrorInfo(
message: 'Failed to load agents',
type: ApiErrorType.http,
statusCode: response.statusCode,
));
} on TimeoutException {
return ApiError<List<AgentDto>>(ApiErrorInfo(
message: 'Request timed out',
type: ApiErrorType.timeout,
));
} on SocketException {
return ApiError<List<AgentDto>>(ApiErrorInfo(
message: 'No internet connection',
type: ApiErrorType.network,
));
} catch (e) {
return ApiError<List<AgentDto>>(ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
));
}
}
/// Get conversations for a specific agent
///
/// Returns all conversations associated with the specified agent.
/// Backend endpoint: GET /api/agents/{id}/conversations
///
/// Example:
/// ```dart
/// final result = await client.getAgentConversations('agent-uuid');
///
/// result.when(
/// success: (conversations) {
/// print('Found ${conversations.length} conversations for agent');
/// },
/// error: (error) => print('Error: ${error.message}'),
/// );
/// ```
Future<Result<List<dynamic>>> getAgentConversations(String agentId) async {
try {
final Uri url =
Uri.parse('${config.baseUrl}/api/agents/$agentId/conversations');
final http.Response response = await http
.get(url, headers: config.defaultHeaders)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
final Object? jsonData = jsonDecode(response.body);
if (jsonData is! List) {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Expected array response, got ${jsonData.runtimeType}',
type: ApiErrorType.serialization,
));
}
return ApiSuccess<List<dynamic>>(jsonData);
}
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Failed to load agent conversations',
type: ApiErrorType.http,
statusCode: response.statusCode,
));
} on TimeoutException {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Request timed out',
type: ApiErrorType.timeout,
));
} on SocketException {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'No internet connection',
type: ApiErrorType.network,
));
} catch (e) {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
));
}
}
/// Get execution history for a specific agent
///
/// Returns the 100 most recent executions for the specified agent.
/// Backend endpoint: GET /api/agents/{id}/executions
///
/// Example:
/// ```dart
/// final result = await client.getAgentExecutions('agent-uuid');
///
/// result.when(
/// success: (executions) {
/// print('Found ${executions.length} executions for agent');
/// },
/// error: (error) => print('Error: ${error.message}'),
/// );
/// ```
Future<Result<List<dynamic>>> getAgentExecutions(String agentId) async {
try {
final Uri url =
Uri.parse('${config.baseUrl}/api/agents/$agentId/executions');
final http.Response response = await http
.get(url, headers: config.defaultHeaders)
.timeout(config.timeout);
if (response.statusCode >= 200 && response.statusCode < 300) {
final Object? jsonData = jsonDecode(response.body);
if (jsonData is! List) {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Expected array response, got ${jsonData.runtimeType}',
type: ApiErrorType.serialization,
));
}
return ApiSuccess<List<dynamic>>(jsonData);
}
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Failed to load agent executions',
type: ApiErrorType.http,
statusCode: response.statusCode,
));
} on TimeoutException {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Request timed out',
type: ApiErrorType.timeout,
));
} on SocketException {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'No internet connection',
type: ApiErrorType.network,
));
} catch (e) {
return ApiError<List<dynamic>>(ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
));
}
}
}