Backend team successfully fixed Swagger conflicts and implemented simple GET endpoints. Frontend now integrates with GET /api/agents to list all agents. Changes: - agent_endpoint.dart: - Added fromInt() methods to all enums (AgentType, AgentStatus, ModelProviderType) - Updated AgentDto.fromJson() to handle integer enum values from backend - Added listAgents() method using HTTP GET /api/agents - Added imports: dart:async, dart:convert, dart:io, package:http - agents_page.dart: - Updated _loadAgents() to call listAgents() API method - Removed placeholder delay, now uses real data from backend - Removed unused getwidget import Backend Integration: ✅ Backend returns 5 test agents (seeded successfully) ✅ Enums transmitted as integers (CodeGenerator=0, Active=0, etc.) ✅ Frontend properly parses integer enums to Dart enum types ✅ GET /api/agents endpoint working and tested ✅ Full CRUD cycle now functional Testing: - Flutter analyze: 0 errors, 0 warnings - Backend health check: ✅ passing - List endpoint: ✅ returns 5 agents - App running: http://localhost:8080 Phase 2 Complete: Frontend can now display agents from backend! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
478 lines
14 KiB
Dart
478 lines
14 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,
|
|
required this.temperature,
|
|
required this.maxTokens,
|
|
required this.systemPrompt,
|
|
required this.enableMemory,
|
|
required 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'] as num).toDouble(),
|
|
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,
|
|
));
|
|
}
|
|
}
|
|
}
|