Compare commits
No commits in common. "master" and "main" have entirely different histories.
8
.idea/.gitignore
vendored
8
.idea/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux/example/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/macos/Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/macos/Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/macos/Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/macos/Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/macos/Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/macos/Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/example/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/FRONTEND/windows/flutter/ephemeral/.plugin_symlinks/url_launcher_windows/example/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/CODEX_ADK.iml" filepath="$PROJECT_DIR$/.idea/CODEX_ADK.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/BACKEND" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/FRONTEND" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -268,14 +268,14 @@ class ApiClient extends http.BaseClient {
|
||||
request.headers['Content-Type'] = 'application/json';
|
||||
request.headers['Accept'] = 'application/json';
|
||||
|
||||
print('🌐 ${request.method} ${request.url}');
|
||||
print('REQUEST ${request.method} ${request.url}');
|
||||
|
||||
try {
|
||||
final response = await _client.send(request);
|
||||
print('✅ ${response.statusCode} ${request.url}');
|
||||
print('RESPONSE ${response.statusCode} ${request.url}');
|
||||
return response;
|
||||
} catch (e) {
|
||||
print('❌ Request failed: $e');
|
||||
print('ERROR Request failed: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -461,7 +461,7 @@ class HomeScreen extends ConsumerWidget {
|
||||
children: [
|
||||
healthCheck.when(
|
||||
data: (isHealthy) => Text(
|
||||
isHealthy ? 'API Connected ✅' : 'API Disconnected ❌',
|
||||
isHealthy ? 'API Connected' : 'API Disconnected',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
@ -764,7 +764,7 @@ class ErrorSnackbar {
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔄 Updating API client from backend..."
|
||||
echo "Updating API client from backend..."
|
||||
|
||||
# Pull latest backend changes
|
||||
cd ../backend
|
||||
@ -778,9 +778,9 @@ openapi-generator-cli generate \
|
||||
-o lib/api/generated \
|
||||
--additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client
|
||||
|
||||
echo "✅ API client updated!"
|
||||
echo "API client updated!"
|
||||
echo ""
|
||||
echo "📝 Next steps:"
|
||||
echo "Next steps:"
|
||||
echo "1. Check backend/docs/CHANGELOG.md for breaking changes"
|
||||
echo "2. Run: flutter pub get"
|
||||
echo "3. Run: flutter test"
|
||||
@ -834,7 +834,7 @@ jobs:
|
||||
- name: Check for API changes
|
||||
run: |
|
||||
if ! diff -q lib/api/openapi.json backend/docs/openapi.json; then
|
||||
echo "⚠️ API changes detected!"
|
||||
echo "API changes detected!"
|
||||
./scripts/update_api_client.sh
|
||||
fi
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@ final api = DefaultApi(apiClient, 'http://localhost:5246');
|
||||
|
||||
// Call health check
|
||||
final isHealthy = await api.apiQueryHealthPost(healthQuery: HealthQuery());
|
||||
print('API Healthy: $isHealthy'); // true
|
||||
print('API Healthy: $isHealthy');
|
||||
```
|
||||
|
||||
---
|
||||
@ -81,8 +81,8 @@ print('API Healthy: $isHealthy'); // true
|
||||
### All Endpoints Use JSON Body
|
||||
```dart
|
||||
// Even empty requests need a body
|
||||
await api.apiQueryHealthPost(healthQuery: HealthQuery()); // ✅
|
||||
await api.apiQueryHealthPost(); // ❌ Wrong
|
||||
await api.apiQueryHealthPost(healthQuery: HealthQuery()); // Correct
|
||||
await api.apiQueryHealthPost(); // Wrong
|
||||
```
|
||||
|
||||
### Endpoint Patterns
|
||||
|
||||
@ -6,7 +6,6 @@ This directory contains context and guidelines for Claude Code when working with
|
||||
|
||||
### Core Guidelines
|
||||
- **[strict-typing.md](strict-typing.md)** - Strict typing requirements (NO dynamic, NO var, NO object)
|
||||
- **[response-protocol.md](response-protocol.md)** - Claude Code response format standards
|
||||
- **[api-quick-reference.md](api-quick-reference.md)** - Quick API reference
|
||||
|
||||
### Frontend Integration (Flutter)
|
||||
@ -19,7 +18,6 @@ This directory contains context and guidelines for Claude Code when working with
|
||||
When working on this project, always refer to:
|
||||
1. `/CLAUDE.md` - Main project standards and CQRS patterns
|
||||
2. `.claude-docs/strict-typing.md` - Type safety requirements
|
||||
3. `.claude-docs/response-protocol.md` - Communication standards
|
||||
|
||||
### For Developers
|
||||
See project root:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Quick reference for common API integration tasks. See [api-contract.md](api-contract.md) for complete documentation.
|
||||
|
||||
**Status:** ⏸️ **No Authentication Required** (R&D Phase)
|
||||
**Status:** **No Authentication Required** (R&D Phase)
|
||||
|
||||
---
|
||||
|
||||
@ -98,20 +98,20 @@ CORS Allowed Origins:
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
❌ **Wrong**: `/api/query/userlist` for paginated queries
|
||||
✅ **Right**: `/api/dynamicquery/userlist`
|
||||
**Wrong**: `/api/query/userlist` for paginated queries
|
||||
**Right**: `/api/dynamicquery/userlist`
|
||||
|
||||
❌ **Wrong**: `{ "field": "name", "operator": "Contains" }`
|
||||
✅ **Right**: `{ "path": "name", "type": "Contains" }`
|
||||
**Wrong**: `{ "field": "name", "operator": "Contains" }`
|
||||
**Right**: `{ "path": "name", "type": "Contains" }`
|
||||
|
||||
❌ **Wrong**: `{ "field": "name", "direction": "Ascending" }`
|
||||
✅ **Right**: `{ "path": "name", "ascending": true }`
|
||||
**Wrong**: `{ "field": "name", "direction": "Ascending" }`
|
||||
**Right**: `{ "path": "name", "ascending": true }`
|
||||
|
||||
❌ **Wrong**: Reading `response.totalItems`
|
||||
✅ **Right**: Reading `response.totalRecords`
|
||||
**Wrong**: Reading `response.totalItems`
|
||||
**Right**: Reading `response.totalRecords`
|
||||
|
||||
❌ **Wrong**: Frontend on port 3000 or 5173
|
||||
✅ **Right**: Frontend on port 54952 (for CORS)
|
||||
**Wrong**: Frontend on port 3000 or 5173
|
||||
**Right**: Frontend on port 54952 (for CORS)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
# MANDATORY RESPONSE PROTOCOL
|
||||
|
||||
**Claude must strictly follow this protocol for ALL responses in this project.**
|
||||
|
||||
---
|
||||
|
||||
## 🗣️ Response Protocol — Defined Answer Types
|
||||
|
||||
Claude must **always** end responses with exactly one of these two structured formats:
|
||||
|
||||
---
|
||||
|
||||
### **Answer Type 1: Binary Choice**
|
||||
Used for: simple confirmations, proceed/cancel actions, file operations.
|
||||
|
||||
**Format:**
|
||||
|
||||
(Y) Yes — [brief action summary]
|
||||
|
||||
(N) No — [brief alternative/reason]
|
||||
|
||||
(+) I don't understand — ask for clarification
|
||||
|
||||
|
||||
**When user selects `(+)`:**
|
||||
Claude responds:
|
||||
> "What part would you like me to explain?"
|
||||
Then teaches the concept step‑by‑step in plain language.
|
||||
|
||||
---
|
||||
|
||||
### **Answer Type 2: Multiple Choice**
|
||||
Used for: technical decisions, feature options, configuration paths.
|
||||
|
||||
**Format:**
|
||||
|
||||
(A) Option A — [minimalist description]
|
||||
|
||||
(B) Option B — [minimalist description]
|
||||
|
||||
(C) Option C — [minimalist description]
|
||||
|
||||
(D) Option D — [minimalist description]
|
||||
|
||||
(+) I don't understand — ask for clarification
|
||||
|
||||
|
||||
**When user selects `(+)`:**
|
||||
Claude responds:
|
||||
> "Which option would you like explained, or should I clarify what we're deciding here?"
|
||||
Then provides context on the decision + explains each option's purpose.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Mandatory Rules
|
||||
1. **No text after the last option** — choices must be the final content.
|
||||
2. Every option description ≤8 words.
|
||||
3. The `(+)` option is **always present** in both formats.
|
||||
4. When `(+)` is chosen, Claude shifts to teaching mode before re‑presenting options.
|
||||
5. Claude must include `(always read claude.md to keep context between interactions)` before every option set.
|
||||
|
||||
---
|
||||
|
||||
### Example 1 (Binary)
|
||||
|
||||
We need to initialize npm in your project folder.
|
||||
|
||||
(always read claude.md to keep context between interactions)
|
||||
|
||||
(Y) Yes — run npm init -y now
|
||||
|
||||
(N) No — show me what this does first
|
||||
|
||||
(+) I don't understand — explain npm initialization
|
||||
|
||||
|
||||
### Example 2 (Multiple Choice)
|
||||
|
||||
Choose your testing framework:
|
||||
|
||||
(always read claude.md to keep context between interactions)
|
||||
|
||||
(A) Jest — popular, feature-rich
|
||||
|
||||
(B) Vitest — faster, Vite-native
|
||||
|
||||
(C) Node test runner — built-in, minimal
|
||||
|
||||
(D) Skip tests — add later
|
||||
|
||||
(+) I don't understand — explain testing frameworks
|
||||
|
||||
|
||||
---
|
||||
|
||||
**This protocol ensures:**
|
||||
- You always have an escape hatch to learn.
|
||||
- Claude never assumes your technical knowledge.
|
||||
- Every interaction has clear, actionable paths.
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
## Examples:
|
||||
|
||||
❌ **FORBIDDEN:**
|
||||
**FORBIDDEN:**
|
||||
```typescript
|
||||
const data: any = fetchData();
|
||||
function process(input: any): any { ... }
|
||||
@ -22,7 +22,7 @@ dynamic value = getValue();
|
||||
void handleData(var data) { ... }
|
||||
```
|
||||
|
||||
✅ **REQUIRED:**
|
||||
**REQUIRED:**
|
||||
```typescript
|
||||
const data: UserData = fetchData();
|
||||
function process(input: UserInput): ProcessedOutput { ... }
|
||||
|
||||
@ -30,7 +30,6 @@ Before creating any plan, Claude MUST:
|
||||
1. **Read Project Documentation**:
|
||||
- `CLAUDE.md` - Project standards and CQRS patterns
|
||||
- `.claude-docs/strict-typing.md` - Typing requirements
|
||||
- `.claude-docs/response-protocol.md` - Communication standards
|
||||
- `docs/ARCHITECTURE.md` - System architecture
|
||||
- `docs/CHANGELOG.md` - Breaking changes history
|
||||
- `README.md` - Project overview
|
||||
@ -379,7 +378,7 @@ This skill includes comprehensive reference materials in the `references/` direc
|
||||
3. **Explicit Commands**: Use exact commands with project-specific libraries
|
||||
4. **Clear Success Criteria**: Define measurable success for each step
|
||||
5. **Reference Project Docs**: Link to CLAUDE.md, docs/, .claude-docs/
|
||||
6. **Follow Project Standards**: Respect .NET version policy, typing rules, response protocol
|
||||
6. **Follow Project Standards**: Respect .NET version policy, typing rules
|
||||
7. **No Placeholders**: Provide complete, working code
|
||||
8. **Security by Default**: Include security considerations in every plan
|
||||
9. **Idempotent Operations**: Ensure operations can be safely repeated
|
||||
@ -422,7 +421,7 @@ When this skill is activated, Claude will:
|
||||
- This skill is for **planning**, not execution (unless user approves)
|
||||
- Always use the project's actual file paths, packages, and patterns
|
||||
- Respect the project's .NET 8.0 LTS policy
|
||||
- Follow strict typing rules and response protocol
|
||||
- Follow strict typing rules
|
||||
- Reference templates but customize for specific needs
|
||||
- Keep plans detailed but concise
|
||||
- Include verification steps throughout
|
||||
|
||||
@ -1,225 +1,113 @@
|
||||
# CLAUDE.md
|
||||
You are the Devops/Backend CTO, the Frontend/UI/UX/Branding CTO reports to you. you two work in a perfectly coordinated duo.
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
# CODEX_ADK Backend - AI Context
|
||||
|
||||
## Project Overview
|
||||
## Project
|
||||
Multi-agent AI laboratory for building, testing sovereign AI agents with hierarchical workflows. CQRS-based ASP.NET Core 8.0 Web API serving Flutter app via REST API.
|
||||
|
||||
Codex is a CQRS-based ASP.NET Core 8.0 Web API using the OpenHarbor.CQRS framework with a modular architecture powered by PoweredSoft modules.
|
||||
## Stack
|
||||
- .NET 8 LTS, OpenHarbor.CQRS, PostgreSQL 15, EF Core 8
|
||||
- FluentValidation, PoweredSoft modules, AES-256 encryption
|
||||
- Docker Compose (postgres + ollama containers)
|
||||
|
||||
## .NET Version Policy
|
||||
**CRITICAL**: This project uses .NET 8.0 LTS. Do NOT upgrade to .NET 9+ without explicit approval. All projects target `net8.0`.
|
||||
|
||||
**IMPORTANT**: This project uses .NET 8.0 LTS and should NOT be upgraded to .NET 9 or later versions without explicit approval. All projects must target `net8.0`.
|
||||
## Architecture
|
||||
```
|
||||
Codex.Api/ # API endpoints, Program.cs, AppModule
|
||||
Codex.CQRS/ # Commands, Queries, Handlers
|
||||
Codex.Dal/ # DbContext, Entities, Migrations
|
||||
```
|
||||
|
||||
## Docker Setup (Recommended)
|
||||
### CQRS Pattern
|
||||
- **Commands**: Write operations (create/update/delete). Persist data, execute business logic.
|
||||
- **Queries**: Read operations. Always use `.AsNoTracking()` for read-only queries.
|
||||
|
||||
This project uses Docker containers for PostgreSQL and Ollama:
|
||||
### Module System
|
||||
PoweredSoft `IModule` system organizes features:
|
||||
1. Create feature modules (CommandsModule, QueriesModule, DalModule)
|
||||
2. Register in `AppModule`
|
||||
3. Register `AppModule` in `Program.cs`: `services.AddModule<AppModule>()`
|
||||
|
||||
**Pattern Details**: See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
||||
|
||||
## Entities
|
||||
- **Agents**: Id, Name, Provider (OpenAI/Anthropic/Ollama), Model, ApiKey(encrypted), SystemPrompt, Temperature, MaxTokens
|
||||
- **AgentTools**: Id, AgentId, Name, Description, Parameters(JSON), IsEnabled
|
||||
- **Conversations**: Id, AgentId, Title, StartedAt, EndedAt, Context(JSON)
|
||||
- **ConversationMessages**: Id, ConversationId, Role, Content, TokenCount, Timestamp
|
||||
- **AgentExecutions**: Id, AgentId, ConversationId, Status, StartedAt, CompletedAt, Result, Error, TokensUsed, Cost
|
||||
|
||||
## Commands & Queries
|
||||
|
||||
### Commands (POST /api/command/{name})
|
||||
- CreateAgent, UpdateAgent, DeleteAgent → `ICommand<Guid>`
|
||||
- CreateConversation → `ICommand<Guid>`
|
||||
- StartAgentExecution, CompleteAgentExecution → `ICommand`
|
||||
|
||||
**Structure**: 3-part pattern (Command record, Handler, Validator) in single file.
|
||||
```csharp
|
||||
public record MyCommand { /* properties */ }
|
||||
public class MyCommandHandler(DbContext db) : ICommandHandler<MyCommand> { }
|
||||
public class MyCommandValidator : AbstractValidator<MyCommand> { }
|
||||
// Registration: services.AddCommand<MyCommand, MyCommandHandler, MyCommandValidator>();
|
||||
```
|
||||
|
||||
### Queries (GET/POST /api/query/{name})
|
||||
- Health → `bool`
|
||||
- GetAgent, GetAgentExecution, GetConversation → DTOs
|
||||
- Paginated: Use `IQueryableProviderOverride<T>` for dynamic filtering/sorting
|
||||
|
||||
**Single Value**: `IQueryHandler<TQuery, TResult>`
|
||||
**Paginated**: `IQueryableProviderOverride<T>` with `.AsNoTracking()`
|
||||
|
||||
**Complete API Reference**: See [.claude-docs/api-quick-reference.md](.claude-docs/api-quick-reference.md)
|
||||
|
||||
## Docker Setup
|
||||
|
||||
```bash
|
||||
# Start all services (PostgreSQL + Ollama)
|
||||
# Start services (PostgreSQL + Ollama)
|
||||
docker-compose up -d
|
||||
|
||||
# Verify containers are running
|
||||
docker ps
|
||||
|
||||
# Apply database migrations
|
||||
# Apply migrations
|
||||
dotnet ef database update --project Codex.Dal --connection "Host=localhost;Database=codex;Username=postgres;Password=postgres"
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
|
||||
# Reset database (CAUTION: deletes all data)
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
docker-compose down -v && docker-compose up -d
|
||||
dotnet ef database update --project Codex.Dal --connection "Host=localhost;Database=codex;Username=postgres;Password=postgres"
|
||||
```
|
||||
|
||||
**Services**:
|
||||
- **PostgreSQL**: localhost:5432 (codex/postgres/postgres)
|
||||
- **Ollama**: localhost:11434
|
||||
|
||||
**Important**: If you have a local PostgreSQL running on port 5432, stop it first:
|
||||
```bash
|
||||
brew services stop postgresql@14 # or your PostgreSQL version
|
||||
```
|
||||
|
||||
### Ollama Model Management
|
||||
|
||||
```bash
|
||||
# Pull a lightweight model for testing (1.6GB)
|
||||
# Ollama model management
|
||||
docker exec codex-ollama ollama pull phi
|
||||
|
||||
# List downloaded models
|
||||
docker exec codex-ollama ollama list
|
||||
|
||||
# Test Ollama is working
|
||||
curl http://localhost:11434/api/tags
|
||||
```
|
||||
|
||||
## Building and Running
|
||||
**Services**: PostgreSQL (localhost:5432), Ollama (localhost:11434)
|
||||
|
||||
**Conflict**: Stop local PostgreSQL first: `brew services stop postgresql@14`
|
||||
|
||||
## Building & Running
|
||||
|
||||
```bash
|
||||
# Build the solution
|
||||
# Build
|
||||
dotnet build
|
||||
|
||||
# Run the API
|
||||
# Run API (HTTP: 5246, HTTPS: 7108, Swagger: /swagger)
|
||||
dotnet run --project Codex.Api/Codex.Api.csproj
|
||||
|
||||
# Run tests (when test projects are added)
|
||||
dotnet test
|
||||
```
|
||||
|
||||
The API runs on:
|
||||
- HTTP: http://localhost:5246
|
||||
- HTTPS: https://localhost:7108
|
||||
- Swagger UI (Development only): http://localhost:5246/swagger
|
||||
|
||||
## Architecture
|
||||
|
||||
### CQRS Pattern
|
||||
This application strictly follows the Command Query Responsibility Segregation (CQRS) pattern:
|
||||
|
||||
- **Commands**: Handle write operations (create, update, delete). Execute business logic and persist data.
|
||||
- **Queries**: Handle read operations. Always use `.AsNoTracking()` for read-only operations.
|
||||
|
||||
### Module System
|
||||
The application uses PoweredSoft's module system (`IModule`) to organize features. Each module registers its services in the `ConfigureServices` method.
|
||||
|
||||
**Module Registration Flow**:
|
||||
1. Create feature-specific modules (CommandsModule, QueriesModule, DalModule)
|
||||
2. Register all modules in `AppModule`
|
||||
3. Register `AppModule` in `Program.cs` via `services.AddModule<AppModule>()`
|
||||
|
||||
### Project Structure
|
||||
- **Codex.Api**: API layer with controllers, Program.cs, and AppModule
|
||||
- **Codex.CQRS**: Commands, queries, and business logic
|
||||
- **Codex.Dal**: Data access layer with DbContext, entities, and query provider infrastructure
|
||||
|
||||
## Commands
|
||||
|
||||
Commands perform write operations and follow a strict 3-part structure:
|
||||
|
||||
**1. Command Definition** (record)
|
||||
```csharp
|
||||
public record MyCommand
|
||||
{
|
||||
// Properties
|
||||
}
|
||||
```
|
||||
|
||||
**2. Handler Implementation** (implements `ICommandHandler<TCommand>`)
|
||||
```csharp
|
||||
public class MyCommandHandler(DbContext dbContext) : ICommandHandler<MyCommand>
|
||||
{
|
||||
public async Task HandleAsync(MyCommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Business logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Validation** (extends `AbstractValidator<TCommand>`)
|
||||
```csharp
|
||||
public class MyCommandValidator : AbstractValidator<MyCommand>
|
||||
{
|
||||
public MyCommandValidator()
|
||||
{
|
||||
// FluentValidation rules
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**: All three components go in a single file and are registered together:
|
||||
```csharp
|
||||
services.AddCommand<MyCommand, MyCommandHandler, MyCommandValidator>();
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
### Single Value Queries
|
||||
Return a specific value (e.g., health check, lookup):
|
||||
|
||||
```csharp
|
||||
public record MyQuery { }
|
||||
|
||||
public class MyQueryHandler : IQueryHandler<MyQuery, TResult>
|
||||
{
|
||||
public Task<TResult> HandleAsync(MyQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Return single value
|
||||
}
|
||||
}
|
||||
|
||||
// Registration
|
||||
services.AddQuery<MyQuery, TResult, MyQueryHandler>();
|
||||
```
|
||||
|
||||
### Paginated Queries
|
||||
Return queryable lists with automatic filtering, sorting, pagination, and aggregates:
|
||||
|
||||
```csharp
|
||||
// 1. Define the item structure
|
||||
public record MyQueryItem
|
||||
{
|
||||
// Properties for each list item
|
||||
}
|
||||
|
||||
// 2. Implement IQueryableProviderOverride<T>
|
||||
public class MyQueryableProvider(DbContext dbContext) : IQueryableProviderOverride<MyQueryItem>
|
||||
{
|
||||
public Task<IQueryable<MyQueryItem>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = dbContext.MyTable
|
||||
.AsNoTracking() // ALWAYS use AsNoTracking for queries
|
||||
.Select(x => new MyQueryItem { /* mapping */ });
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Registration
|
||||
services.AddDynamicQuery<MyQueryItem>()
|
||||
.AddQueryableProviderOverride<MyQueryItem, MyQueryableProvider>();
|
||||
```
|
||||
|
||||
**IMPORTANT**: Paginated queries return `IQueryable<T>`. The framework handles actual query execution, pagination, filtering, and sorting.
|
||||
|
||||
## Data Access Layer Setup
|
||||
|
||||
The DAL requires specific infrastructure files for the CQRS query system to work properly:
|
||||
|
||||
### Required Files
|
||||
|
||||
1. **IQueryableProviderOverride.cs**: Interface for custom query providers
|
||||
2. **ServiceCollectionExtensions.cs**: Extension to register query provider overrides
|
||||
3. **DefaultQueryableProvider.cs**: Default provider that checks for overrides
|
||||
4. **InMemoryQueryableHandlerService.cs**: Handler for in-memory queryables
|
||||
5. **DalModule.cs**: Module to register DAL services
|
||||
|
||||
All code examples for these files are in `.context/dal-context.md`.
|
||||
|
||||
### DbContext
|
||||
Create your DbContext with EF Core and use migrations for schema management:
|
||||
```bash
|
||||
# Add a new migration
|
||||
dotnet ef migrations add <MigrationName> --project Codex.Dal
|
||||
|
||||
# Update database
|
||||
# Migrations
|
||||
dotnet ef migrations add <Name> --project Codex.Dal
|
||||
dotnet ef database update --project Codex.Dal
|
||||
|
||||
# Export OpenAPI spec (after API changes)
|
||||
dotnet build && ./export-openapi.sh
|
||||
```
|
||||
|
||||
### OpenAPI Documentation Export
|
||||
After adding or modifying commands/queries with XML documentation:
|
||||
```bash
|
||||
# Build and export OpenAPI specification
|
||||
dotnet build
|
||||
./export-openapi.sh
|
||||
## Required Service Registration (Program.cs)
|
||||
|
||||
# This generates docs/openapi.json for frontend integration
|
||||
```
|
||||
|
||||
## API Configuration (Program.cs)
|
||||
|
||||
Required service registrations:
|
||||
```csharp
|
||||
// PoweredSoft & CQRS
|
||||
builder.Services.AddPoweredSoftDataServices();
|
||||
@ -227,64 +115,68 @@ builder.Services.AddPoweredSoftEntityFrameworkCoreDataServices();
|
||||
builder.Services.AddPoweredSoftDynamicQuery();
|
||||
builder.Services.AddDefaultCommandDiscovery();
|
||||
builder.Services.AddDefaultQueryDiscovery();
|
||||
|
||||
// Validation
|
||||
builder.Services.AddFluentValidation();
|
||||
|
||||
// Module registration
|
||||
builder.Services.AddModule<AppModule>();
|
||||
|
||||
// Controllers with OpenHarbor CQRS integration
|
||||
var mvcBuilder = builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(jsonOptions =>
|
||||
{
|
||||
jsonOptions.JsonSerializerOptions.Converters.Insert(0, new JsonStringEnumConverter());
|
||||
});
|
||||
// Controllers (required for OpenHarbor CQRS)
|
||||
var mvcBuilder = builder.Services.AddControllers()
|
||||
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Insert(0, new JsonStringEnumConverter()));
|
||||
|
||||
mvcBuilder.AddOpenHarborCommands();
|
||||
mvcBuilder.AddOpenHarborQueries()
|
||||
.AddOpenHarborDynamicQueries();
|
||||
mvcBuilder.AddOpenHarborQueries().AddOpenHarborDynamicQueries();
|
||||
```
|
||||
|
||||
**Note**: Controllers (not minimal APIs) are required for OpenHarbor CQRS integration.
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- **OpenHarbor.CQRS**: CQRS framework core
|
||||
- **OpenHarbor.CQRS.AspNetCore.Mvc**: MVC integration for commands/queries
|
||||
- **OpenHarbor.CQRS.DynamicQuery.AspNetCore**: Dynamic query support
|
||||
- **OpenHarbor.CQRS.FluentValidation**: FluentValidation integration
|
||||
- **PoweredSoft.Module.Abstractions**: Module system
|
||||
- **PoweredSoft.Data.EntityFrameworkCore**: Data access abstractions
|
||||
- **PoweredSoft.DynamicQuery**: Dynamic query engine
|
||||
- **FluentValidation.AspNetCore**: Validation
|
||||
- OpenHarbor.CQRS (core + AspNetCore.Mvc + DynamicQuery.AspNetCore + FluentValidation)
|
||||
- PoweredSoft.Module.Abstractions + Data.EntityFrameworkCore + DynamicQuery
|
||||
- FluentValidation.AspNetCore
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
1. **Query Performance**: Always use `.AsNoTracking()` for read-only queries
|
||||
2. **File Organization**: Place command/handler/validator in the same file
|
||||
3. **Validation**: All commands must have validators (even if empty)
|
||||
4. **Modules**: Group related commands/queries into feature modules
|
||||
5. **XML Documentation**: Add XML comments to all commands/queries for OpenAPI generation
|
||||
6. **OpenAPI Export**: Run `./export-openapi.sh` after API changes to update frontend specs
|
||||
7. **CORS**: Configure allowed origins in appsettings for different environments
|
||||
1. **Query Performance**: Always `.AsNoTracking()` for read-only queries
|
||||
2. **File Organization**: Command/Handler/Validator in single file
|
||||
3. **Validation**: All commands require validators (even if empty)
|
||||
4. **Modules**: Group related commands/queries by feature
|
||||
5. **XML Documentation**: Add XML comments for OpenAPI generation
|
||||
6. **OpenAPI Export**: Run `./export-openapi.sh` after API changes
|
||||
7. **CORS**: Configure allowed origins in appsettings per environment
|
||||
8. **HTTPS**: Only enforced in non-development environments
|
||||
|
||||
# 🔒 MANDATORY CODING STANDARDS
|
||||
## Known Issues
|
||||
- Dynamic queries not in OpenAPI spec (OpenHarbor limitation)
|
||||
- Hardcoded secrets in appsettings.json (CRITICAL - fix before production)
|
||||
- Manual endpoint registration needed for Swagger
|
||||
|
||||
## Current Focus
|
||||
Replace dynamic queries with simple GET endpoints for MVP. Fix security before production.
|
||||
|
||||
---
|
||||
|
||||
# MANDATORY CODING STANDARDS
|
||||
|
||||
## Code Style - NO EXCEPTIONS
|
||||
**CRITICAL**: NEVER use emojis in code, comments, commit messages, or any project files. All communication must be professional and emoji-free.
|
||||
|
||||
## Git Commit Standards
|
||||
**CRITICAL**: All commits MUST follow this authorship format:
|
||||
- **Author**: Svrnty
|
||||
- **Co-Author**: Jean-Philippe Brule <jp@svrnty.io>
|
||||
|
||||
When creating commits, always include:
|
||||
```
|
||||
Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
|
||||
```
|
||||
|
||||
## Strict Typing - NO EXCEPTIONS
|
||||
|
||||
See [.claude-docs/strict-typing.md](.claude-docs/strict-typing.md) for complete typing requirements.
|
||||
|
||||
---
|
||||
|
||||
## 🗣️ Response Protocol
|
||||
|
||||
See [.claude-docs/response-protocol.md](.claude-docs/response-protocol.md) for complete protocol details.
|
||||
|
||||
---
|
||||
|
||||
## 📡 Frontend Integration
|
||||
|
||||
## Frontend Integration
|
||||
See [.claude-docs/frontend-api-integration.md](.claude-docs/frontend-api-integration.md) for complete API integration specifications for frontend teams.
|
||||
|
||||
---
|
||||
|
||||
**Additional Documentation**:
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) - Detailed system architecture
|
||||
- [docs/COMPLETE-API-REFERENCE.md](docs/COMPLETE-API-REFERENCE.md) - Full API contract with examples
|
||||
- [docs/CHANGELOG.md](docs/CHANGELOG.md) - Breaking changes history
|
||||
- [.claude-docs/FLUTTER-QUICK-START.md](.claude-docs/FLUTTER-QUICK-START.md) - Flutter integration guide
|
||||
|
||||
324
BACKEND/Codex.CQRS/Commands/SendMessageCommand.cs
Normal file
324
BACKEND/Codex.CQRS/Commands/SendMessageCommand.cs
Normal file
@ -0,0 +1,324 @@
|
||||
using Codex.Dal;
|
||||
using Codex.Dal.Entities;
|
||||
using Codex.Dal.Enums;
|
||||
using Codex.Dal.Services;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OpenHarbor.CQRS.Abstractions;
|
||||
using System.Text;
|
||||
|
||||
namespace Codex.CQRS.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a user message to an agent and receives a response.
|
||||
/// Creates a new conversation if ConversationId is not provided.
|
||||
/// </summary>
|
||||
public record SendMessageCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the agent to send the message to
|
||||
/// </summary>
|
||||
public Guid AgentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of existing conversation, or null to create a new conversation
|
||||
/// </summary>
|
||||
public Guid? ConversationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// User's message content
|
||||
/// </summary>
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional user identifier for future authentication support
|
||||
/// </summary>
|
||||
public string? UserId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result containing the user message, agent response, and conversation metadata
|
||||
/// </summary>
|
||||
public record SendMessageResult
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the conversation (new or existing)
|
||||
/// </summary>
|
||||
public Guid ConversationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the stored user message
|
||||
/// </summary>
|
||||
public Guid MessageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the stored agent response message
|
||||
/// </summary>
|
||||
public Guid AgentResponseId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's message that was sent
|
||||
/// </summary>
|
||||
public MessageDto UserMessage { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The agent's response
|
||||
/// </summary>
|
||||
public AgentResponseDto AgentResponse { get; init; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simplified message data transfer object
|
||||
/// </summary>
|
||||
public record MessageDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Message content
|
||||
/// </summary>
|
||||
public string Content { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the message was created
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent response with token usage and cost information
|
||||
/// </summary>
|
||||
public record AgentResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Response content from the agent
|
||||
/// </summary>
|
||||
public string Content { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the response was generated
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of input tokens processed
|
||||
/// </summary>
|
||||
public int? InputTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of output tokens generated
|
||||
/// </summary>
|
||||
public int? OutputTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Estimated cost of the request in USD
|
||||
/// </summary>
|
||||
public decimal? EstimatedCost { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a message to an agent and storing the conversation
|
||||
/// </summary>
|
||||
public class SendMessageCommandHandler : ICommandHandler<SendMessageCommand, SendMessageResult>
|
||||
{
|
||||
private readonly CodexDbContext _dbContext;
|
||||
private readonly IOllamaService _ollamaService;
|
||||
|
||||
public SendMessageCommandHandler(CodexDbContext dbContext, IOllamaService ollamaService)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_ollamaService = ollamaService;
|
||||
}
|
||||
|
||||
public async Task<SendMessageResult> HandleAsync(SendMessageCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// A. Validate agent exists and is active
|
||||
var agent = await _dbContext.Agents
|
||||
.FirstOrDefaultAsync(a => a.Id == command.AgentId && !a.IsDeleted, cancellationToken);
|
||||
|
||||
if (agent == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Agent with ID {command.AgentId} not found or has been deleted.");
|
||||
}
|
||||
|
||||
if (agent.Status != AgentStatus.Active)
|
||||
{
|
||||
throw new InvalidOperationException($"Agent '{agent.Name}' is not active. Current status: {agent.Status}");
|
||||
}
|
||||
|
||||
// B. Get or create conversation
|
||||
Conversation conversation;
|
||||
if (command.ConversationId.HasValue)
|
||||
{
|
||||
var existingConversation = await _dbContext.Conversations
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ConversationId.Value, cancellationToken);
|
||||
|
||||
if (existingConversation == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Conversation with ID {command.ConversationId.Value} not found.");
|
||||
}
|
||||
|
||||
conversation = existingConversation;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new conversation with title from first message
|
||||
var title = command.Message.Length > 50
|
||||
? command.Message.Substring(0, 50) + "..."
|
||||
: command.Message;
|
||||
|
||||
conversation = new Conversation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Title = title,
|
||||
StartedAt = DateTime.UtcNow,
|
||||
LastMessageAt = DateTime.UtcNow,
|
||||
MessageCount = 0,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
_dbContext.Conversations.Add(conversation);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// C. Store user message
|
||||
var userMessage = new ConversationMessage
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ConversationId = conversation.Id,
|
||||
Role = MessageRole.User,
|
||||
Content = command.Message,
|
||||
MessageIndex = conversation.MessageCount,
|
||||
IsInActiveWindow = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_dbContext.ConversationMessages.Add(userMessage);
|
||||
conversation.MessageCount++;
|
||||
conversation.LastMessageAt = DateTime.UtcNow;
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// D. Build conversation context (get messages in active window)
|
||||
var contextMessages = await _dbContext.ConversationMessages
|
||||
.AsNoTracking()
|
||||
.Where(m => m.ConversationId == conversation.Id && m.IsInActiveWindow)
|
||||
.OrderByDescending(m => m.MessageIndex)
|
||||
.Take(agent.ConversationWindowSize)
|
||||
.OrderBy(m => m.MessageIndex)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// E. Create execution record
|
||||
var execution = new AgentExecution
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AgentId = agent.Id,
|
||||
ConversationId = conversation.Id,
|
||||
UserPrompt = command.Message,
|
||||
StartedAt = DateTime.UtcNow,
|
||||
Status = ExecutionStatus.Running
|
||||
};
|
||||
|
||||
_dbContext.AgentExecutions.Add(execution);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// F. Execute agent via Ollama
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
OllamaResponse ollamaResponse;
|
||||
|
||||
try
|
||||
{
|
||||
ollamaResponse = await _ollamaService.GenerateAsync(
|
||||
agent.ModelEndpoint ?? "http://localhost:11434",
|
||||
agent.ModelName,
|
||||
agent.SystemPrompt,
|
||||
contextMessages,
|
||||
command.Message,
|
||||
agent.Temperature,
|
||||
agent.MaxTokens,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
stopwatch.Stop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
|
||||
// Update execution to failed status
|
||||
execution.Status = ExecutionStatus.Failed;
|
||||
execution.ErrorMessage = ex.Message;
|
||||
execution.CompletedAt = DateTime.UtcNow;
|
||||
execution.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
throw new InvalidOperationException($"Failed to get response from agent: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
// G. Store agent response
|
||||
var agentMessage = new ConversationMessage
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ConversationId = conversation.Id,
|
||||
Role = MessageRole.Assistant,
|
||||
Content = ollamaResponse.Content,
|
||||
MessageIndex = conversation.MessageCount,
|
||||
IsInActiveWindow = true,
|
||||
TokenCount = ollamaResponse.OutputTokens,
|
||||
ExecutionId = execution.Id,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_dbContext.ConversationMessages.Add(agentMessage);
|
||||
conversation.MessageCount++;
|
||||
conversation.LastMessageAt = DateTime.UtcNow;
|
||||
|
||||
// H. Complete execution record
|
||||
execution.Output = ollamaResponse.Content;
|
||||
execution.CompletedAt = DateTime.UtcNow;
|
||||
execution.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
execution.InputTokens = ollamaResponse.InputTokens;
|
||||
execution.OutputTokens = ollamaResponse.OutputTokens;
|
||||
execution.TotalTokens = (ollamaResponse.InputTokens ?? 0) + (ollamaResponse.OutputTokens ?? 0);
|
||||
execution.Status = ExecutionStatus.Completed;
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// I. Return result
|
||||
return new SendMessageResult
|
||||
{
|
||||
ConversationId = conversation.Id,
|
||||
MessageId = userMessage.Id,
|
||||
AgentResponseId = agentMessage.Id,
|
||||
UserMessage = new MessageDto
|
||||
{
|
||||
Content = userMessage.Content,
|
||||
Timestamp = userMessage.CreatedAt
|
||||
},
|
||||
AgentResponse = new AgentResponseDto
|
||||
{
|
||||
Content = agentMessage.Content,
|
||||
Timestamp = agentMessage.CreatedAt,
|
||||
InputTokens = execution.InputTokens,
|
||||
OutputTokens = execution.OutputTokens,
|
||||
EstimatedCost = execution.EstimatedCost
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates SendMessageCommand input
|
||||
/// </summary>
|
||||
public class SendMessageCommandValidator : AbstractValidator<SendMessageCommand>
|
||||
{
|
||||
public SendMessageCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.AgentId)
|
||||
.NotEmpty()
|
||||
.WithMessage("Agent ID is required.");
|
||||
|
||||
RuleFor(x => x.Message)
|
||||
.NotEmpty()
|
||||
.WithMessage("Message is required.")
|
||||
.MaximumLength(10000)
|
||||
.WithMessage("Message must not exceed 10,000 characters.");
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ public class CommandsModule : IModule
|
||||
|
||||
// Conversation commands
|
||||
services.AddCommand<CreateConversationCommand, Guid, CreateConversationCommandHandler, CreateConversationCommandValidator>();
|
||||
services.AddCommand<SendMessageCommand, SendMessageResult, SendMessageCommandHandler, SendMessageCommandValidator>();
|
||||
|
||||
// Agent execution commands
|
||||
services.AddCommand<StartAgentExecutionCommand, Guid, StartAgentExecutionCommandHandler, StartAgentExecutionCommandValidator>();
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="OpenHarbor.CQRS" Version="8.1.0-rc1" />
|
||||
<PackageReference Include="OpenHarbor.CQRS.DynamicQuery.Abstractions" Version="8.1.0-rc1" />
|
||||
|
||||
@ -60,6 +60,7 @@ public class CodexDbContext : DbContext
|
||||
// Indexes
|
||||
entity.HasIndex(a => new { a.Status, a.IsDeleted });
|
||||
entity.HasIndex(a => a.Type);
|
||||
entity.HasIndex(a => a.Name); // Performance: name searches
|
||||
|
||||
// Relationships
|
||||
entity.HasMany(a => a.Tools)
|
||||
@ -125,6 +126,7 @@ public class CodexDbContext : DbContext
|
||||
|
||||
entity.HasIndex(e => e.ConversationId);
|
||||
entity.HasIndex(e => e.Status);
|
||||
entity.HasIndex(e => e.CompletedAt); // Performance: time-based queries
|
||||
|
||||
// Relationships
|
||||
entity.HasOne(e => e.Conversation)
|
||||
@ -156,6 +158,7 @@ public class CodexDbContext : DbContext
|
||||
// Indexes
|
||||
entity.HasIndex(c => new { c.IsActive, c.LastMessageAt })
|
||||
.IsDescending(false, true); // IsActive ASC, LastMessageAt DESC
|
||||
entity.HasIndex(c => c.Title); // Performance: title searches
|
||||
|
||||
// Relationships
|
||||
entity.HasMany(c => c.Messages)
|
||||
@ -178,10 +181,10 @@ public class CodexDbContext : DbContext
|
||||
// Composite index for efficient conversation window queries
|
||||
entity.HasIndex(m => new { m.ConversationId, m.IsInActiveWindow, m.MessageIndex });
|
||||
|
||||
// Index for ordering messages
|
||||
entity.HasIndex(m => new { m.ConversationId, m.MessageIndex });
|
||||
|
||||
// Index for role filtering
|
||||
entity.HasIndex(m => m.Role);
|
||||
|
||||
// Performance: time-based queries
|
||||
entity.HasIndex(m => m.CreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ public class DalModule : IModule
|
||||
services.AddSingleton<IAsyncQueryableHandlerService, InMemoryQueryableHandlerService>();
|
||||
services.AddTransient(typeof(IQueryableProvider<>), typeof(DefaultQueryableProvider<>));
|
||||
services.AddSingleton<IEncryptionService, AesEncryptionService>();
|
||||
services.AddScoped<IOllamaService, OllamaService>();
|
||||
|
||||
// Register dynamic queries (paginated)
|
||||
services.AddDynamicQueries();
|
||||
|
||||
384
BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.Designer.cs
generated
Normal file
384
BACKEND/Codex.Dal/Migrations/20251027032413_AddPerformanceIndexes.Designer.cs
generated
Normal file
@ -0,0 +1,384 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Codex.Dal;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Codex.Dal.Migrations
|
||||
{
|
||||
[DbContext(typeof(CodexDbContext))]
|
||||
[Migration("20251027032413_AddPerformanceIndexes")]
|
||||
partial class AddPerformanceIndexes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.Agent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ApiKeyEncrypted")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ConversationWindowSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<bool>("EnableMemory")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("MaxTokens")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ModelEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("ModelName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("ModelProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("ProviderType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<double>("Temperature")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.HasIndex("Status", "IsDeleted");
|
||||
|
||||
b.ToTable("Agents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.AgentExecution", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("ConversationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal?>("EstimatedCost")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<long?>("ExecutionTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Input")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("InputTokens")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Output")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<int?>("OutputTokens")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ToolCallResults")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToolCalls")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("TotalTokens")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("UserPrompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompletedAt");
|
||||
|
||||
b.HasIndex("ConversationId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("AgentId", "StartedAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("AgentExecutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.AgentTool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AgentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ApiBaseUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("ApiKeyEncrypted")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<JsonDocument>("Configuration")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("McpAuthTokenEncrypted")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("McpServerUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("ToolName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.HasIndex("AgentId", "IsEnabled");
|
||||
|
||||
b.ToTable("AgentTools");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.Conversation", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("LastMessageAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MessageCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Title");
|
||||
|
||||
b.HasIndex("IsActive", "LastMessageAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("Conversations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.ConversationMessage", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("ConversationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("ExecutionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsInActiveWindow")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("MessageIndex")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("TokenCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ToolCalls")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToolResults")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("ExecutionId");
|
||||
|
||||
b.HasIndex("Role");
|
||||
|
||||
b.HasIndex("ConversationId", "IsInActiveWindow", "MessageIndex");
|
||||
|
||||
b.ToTable("ConversationMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.AgentExecution", b =>
|
||||
{
|
||||
b.HasOne("Codex.Dal.Entities.Agent", "Agent")
|
||||
.WithMany("Executions")
|
||||
.HasForeignKey("AgentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Codex.Dal.Entities.Conversation", "Conversation")
|
||||
.WithMany("Executions")
|
||||
.HasForeignKey("ConversationId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Agent");
|
||||
|
||||
b.Navigation("Conversation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.AgentTool", b =>
|
||||
{
|
||||
b.HasOne("Codex.Dal.Entities.Agent", "Agent")
|
||||
.WithMany("Tools")
|
||||
.HasForeignKey("AgentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Agent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.ConversationMessage", b =>
|
||||
{
|
||||
b.HasOne("Codex.Dal.Entities.Conversation", "Conversation")
|
||||
.WithMany("Messages")
|
||||
.HasForeignKey("ConversationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Codex.Dal.Entities.AgentExecution", "Execution")
|
||||
.WithMany("Messages")
|
||||
.HasForeignKey("ExecutionId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Conversation");
|
||||
|
||||
b.Navigation("Execution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.Agent", b =>
|
||||
{
|
||||
b.Navigation("Executions");
|
||||
|
||||
b.Navigation("Tools");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.AgentExecution", b =>
|
||||
{
|
||||
b.Navigation("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Codex.Dal.Entities.Conversation", b =>
|
||||
{
|
||||
b.Navigation("Executions");
|
||||
|
||||
b.Navigation("Messages");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Codex.Dal.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPerformanceIndexes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ConversationMessages_ConversationId_MessageIndex",
|
||||
table: "ConversationMessages");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Conversations_Title",
|
||||
table: "Conversations",
|
||||
column: "Title");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ConversationMessages_CreatedAt",
|
||||
table: "ConversationMessages",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Agents_Name",
|
||||
table: "Agents",
|
||||
column: "Name");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AgentExecutions_CompletedAt",
|
||||
table: "AgentExecutions",
|
||||
column: "CompletedAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Conversations_Title",
|
||||
table: "Conversations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ConversationMessages_CreatedAt",
|
||||
table: "ConversationMessages");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Agents_Name",
|
||||
table: "Agents");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AgentExecutions_CompletedAt",
|
||||
table: "AgentExecutions");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ConversationMessages_ConversationId_MessageIndex",
|
||||
table: "ConversationMessages",
|
||||
columns: new[] { "ConversationId", "MessageIndex" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,6 +92,8 @@ namespace Codex.Dal.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.HasIndex("Status", "IsDeleted");
|
||||
@ -160,6 +162,8 @@ namespace Codex.Dal.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CompletedAt");
|
||||
|
||||
b.HasIndex("ConversationId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
@ -248,6 +252,8 @@ namespace Codex.Dal.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Title");
|
||||
|
||||
b.HasIndex("IsActive", "LastMessageAt")
|
||||
.IsDescending(false, true);
|
||||
|
||||
@ -293,12 +299,12 @@ namespace Codex.Dal.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("ExecutionId");
|
||||
|
||||
b.HasIndex("Role");
|
||||
|
||||
b.HasIndex("ConversationId", "MessageIndex");
|
||||
|
||||
b.HasIndex("ConversationId", "IsInActiveWindow", "MessageIndex");
|
||||
|
||||
b.ToTable("ConversationMessages");
|
||||
|
||||
53
BACKEND/Codex.Dal/Services/IOllamaService.cs
Normal file
53
BACKEND/Codex.Dal/Services/IOllamaService.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Codex.Dal.Entities;
|
||||
|
||||
namespace Codex.Dal.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for interacting with Ollama LLM endpoints
|
||||
/// </summary>
|
||||
public interface IOllamaService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a response from an Ollama model given conversation context
|
||||
/// </summary>
|
||||
/// <param name="endpoint">Ollama endpoint URL (e.g., "http://localhost:11434")</param>
|
||||
/// <param name="model">Model name (e.g., "phi", "codellama:7b")</param>
|
||||
/// <param name="systemPrompt">System prompt defining agent behavior</param>
|
||||
/// <param name="contextMessages">Previous conversation messages for context</param>
|
||||
/// <param name="userMessage">Current user message to respond to</param>
|
||||
/// <param name="temperature">Temperature parameter (0.0 to 2.0)</param>
|
||||
/// <param name="maxTokens">Maximum tokens to generate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Response from the model with token counts</returns>
|
||||
Task<OllamaResponse> GenerateAsync(
|
||||
string endpoint,
|
||||
string model,
|
||||
string systemPrompt,
|
||||
List<ConversationMessage> contextMessages,
|
||||
string userMessage,
|
||||
double temperature,
|
||||
int maxTokens,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from Ollama generation request
|
||||
/// </summary>
|
||||
public record OllamaResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Generated response content
|
||||
/// </summary>
|
||||
public string Content { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Number of tokens in the input prompt
|
||||
/// </summary>
|
||||
public int? InputTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of tokens in the generated output
|
||||
/// </summary>
|
||||
public int? OutputTokens { get; init; }
|
||||
}
|
||||
185
BACKEND/Codex.Dal/Services/OllamaService.cs
Normal file
185
BACKEND/Codex.Dal/Services/OllamaService.cs
Normal file
@ -0,0 +1,185 @@
|
||||
using Codex.Dal.Entities;
|
||||
using Codex.Dal.Enums;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Codex.Dal.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of Ollama service for LLM interactions
|
||||
/// </summary>
|
||||
public class OllamaService : IOllamaService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public OllamaService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task<OllamaResponse> GenerateAsync(
|
||||
string endpoint,
|
||||
string model,
|
||||
string systemPrompt,
|
||||
List<ConversationMessage> contextMessages,
|
||||
string userMessage,
|
||||
double temperature,
|
||||
int maxTokens,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromMinutes(5); // Allow for longer generation times
|
||||
|
||||
// Build prompt with system instruction and conversation context
|
||||
var promptBuilder = new StringBuilder();
|
||||
|
||||
// Add system prompt
|
||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
promptBuilder.AppendLine($"System: {systemPrompt}");
|
||||
promptBuilder.AppendLine();
|
||||
}
|
||||
|
||||
// Add conversation context
|
||||
foreach (var msg in contextMessages)
|
||||
{
|
||||
var role = msg.Role switch
|
||||
{
|
||||
MessageRole.User => "User",
|
||||
MessageRole.Assistant => "Assistant",
|
||||
MessageRole.System => "System",
|
||||
MessageRole.Tool => "Tool",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
promptBuilder.AppendLine($"{role}: {msg.Content}");
|
||||
}
|
||||
|
||||
// Add current user message
|
||||
promptBuilder.AppendLine($"User: {userMessage}");
|
||||
promptBuilder.Append("Assistant:");
|
||||
|
||||
// Build request payload
|
||||
var payload = new OllamaGenerateRequest
|
||||
{
|
||||
Model = model,
|
||||
Prompt = promptBuilder.ToString(),
|
||||
Temperature = temperature,
|
||||
Options = new OllamaOptions
|
||||
{
|
||||
NumPredict = maxTokens,
|
||||
Temperature = temperature
|
||||
},
|
||||
Stream = false
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await httpClient.PostAsJsonAsync(
|
||||
$"{endpoint.TrimEnd('/')}/api/generate",
|
||||
payload,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<OllamaGenerateResponse>(cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Received null response from Ollama API");
|
||||
}
|
||||
|
||||
return new OllamaResponse
|
||||
{
|
||||
Content = result.Response?.Trim() ?? string.Empty,
|
||||
InputTokens = result.PromptEvalCount,
|
||||
OutputTokens = result.EvalCount
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to connect to Ollama at {endpoint}. Ensure Ollama is running and the endpoint is correct.",
|
||||
ex
|
||||
);
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Request to Ollama timed out. The model may be taking too long to respond.",
|
||||
ex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for Ollama /api/generate endpoint
|
||||
/// </summary>
|
||||
private record OllamaGenerateRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
public double Temperature { get; init; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public OllamaOptions? Options { get; init; }
|
||||
|
||||
[JsonPropertyName("stream")]
|
||||
public bool Stream { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for Ollama generation
|
||||
/// </summary>
|
||||
private record OllamaOptions
|
||||
{
|
||||
[JsonPropertyName("num_predict")]
|
||||
public int NumPredict { get; init; }
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
public double Temperature { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from Ollama /api/generate endpoint
|
||||
/// </summary>
|
||||
private record OllamaGenerateResponse
|
||||
{
|
||||
[JsonPropertyName("response")]
|
||||
public string? Response { get; init; }
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string? Model { get; init; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public string? CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("done")]
|
||||
public bool Done { get; init; }
|
||||
|
||||
[JsonPropertyName("total_duration")]
|
||||
public long? TotalDuration { get; init; }
|
||||
|
||||
[JsonPropertyName("load_duration")]
|
||||
public long? LoadDuration { get; init; }
|
||||
|
||||
[JsonPropertyName("prompt_eval_count")]
|
||||
public int? PromptEvalCount { get; init; }
|
||||
|
||||
[JsonPropertyName("prompt_eval_duration")]
|
||||
public long? PromptEvalDuration { get; init; }
|
||||
|
||||
[JsonPropertyName("eval_count")]
|
||||
public int? EvalCount { get; init; }
|
||||
|
||||
[JsonPropertyName("eval_duration")]
|
||||
public long? EvalDuration { get; init; }
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,16 @@
|
||||
# ⚠️ DEPLOYMENT STATUS
|
||||
# DEPLOYMENT STATUS
|
||||
|
||||
## Current Version: v1.0.0-mvp-alpha
|
||||
|
||||
### ✅ READY FOR:
|
||||
- ✅ **Frontend Integration** - All 16 endpoints functional
|
||||
- ✅ **Local Development** - Full development environment
|
||||
- ✅ **Internal Testing** - Safe for team/localhost use
|
||||
### READY FOR:
|
||||
- **Frontend Integration** - All 16 endpoints functional
|
||||
- **Local Development** - Full development environment
|
||||
- **Internal Testing** - Safe for team/localhost use
|
||||
|
||||
### ⚠️ NOT READY FOR:
|
||||
- ❌ **Public Internet Deployment** - Security hardening in progress
|
||||
- ❌ **Production with Real Users** - Authentication not implemented
|
||||
- ❌ **External Staging Servers** - Secrets management being improved
|
||||
### NOT READY FOR:
|
||||
- **Public Internet Deployment** - Security hardening in progress
|
||||
- **Production with Real Users** - Authentication not implemented
|
||||
- **External Staging Servers** - Secrets management being improved
|
||||
|
||||
---
|
||||
|
||||
@ -18,25 +18,25 @@
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| API Endpoints | ✅ Functional | All 16 endpoints working |
|
||||
| Input Validation | ✅ Complete | FluentValidation on all commands |
|
||||
| Rate Limiting | ✅ Active | 1000 req/min per client |
|
||||
| CORS | ✅ Configured | Development: localhost, Production: TBD |
|
||||
| Encryption | ⚠️ Working | API keys encrypted (key management improving) |
|
||||
| Authentication | ❌ Not Implemented | Required before public deployment |
|
||||
| Secrets Management | ⚠️ In Progress | Moving to environment variables |
|
||||
| HTTPS | ⚠️ Dev Only | Production enforcement ready |
|
||||
| API Endpoints | Functional | All 16 endpoints working |
|
||||
| Input Validation | Complete | FluentValidation on all commands |
|
||||
| Rate Limiting | Active | 1000 req/min per client |
|
||||
| CORS | Configured | Development: localhost, Production: TBD |
|
||||
| Encryption | Working | API keys encrypted (key management improving) |
|
||||
| Authentication | Not Implemented | Required before public deployment |
|
||||
| Secrets Management | In Progress | Moving to environment variables |
|
||||
| HTTPS | Dev Only | Production enforcement ready |
|
||||
|
||||
---
|
||||
|
||||
## Hardening Timeline
|
||||
|
||||
### Week 1 (Current)
|
||||
- ✅ Ship v1.0.0-mvp-alpha to frontend
|
||||
- 🔄 Phase 1: Security improvements (env vars, secrets)
|
||||
- 🔄 Phase 2: Deployment infrastructure (Docker, health checks)
|
||||
- 🔄 Phase 3: Testing safety net (smoke tests, CI)
|
||||
- 🔄 Phase 4: Documentation (deployment guide)
|
||||
- DONE Ship v1.0.0-mvp-alpha to frontend
|
||||
- IN PROGRESS Phase 1: Security improvements (env vars, secrets)
|
||||
- IN PROGRESS Phase 2: Deployment infrastructure (Docker, health checks)
|
||||
- IN PROGRESS Phase 3: Testing safety net (smoke tests, CI)
|
||||
- IN PROGRESS Phase 4: Documentation (deployment guide)
|
||||
|
||||
### Week 2 (Planned)
|
||||
- Add JWT authentication
|
||||
@ -48,13 +48,13 @@
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
### ✅ Safe to Use For:
|
||||
### Safe to Use For:
|
||||
- Frontend development (localhost integration)
|
||||
- Backend feature development
|
||||
- Local testing with docker-compose
|
||||
- Team demos (internal network)
|
||||
|
||||
### ❌ Do Not Use For:
|
||||
### Do Not Use For:
|
||||
- Public internet deployment
|
||||
- Production environments
|
||||
- External demos without VPN
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# 🎉 Backend MVP v1.0.0 - SHIPPED!
|
||||
# Backend MVP v1.0.0 - SHIPPED!
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Status:** ✅ COMPLETE - Ready for frontend integration
|
||||
**Status:** COMPLETE - Ready for frontend integration
|
||||
|
||||
**Timeline:** 2 days (realistic) vs 5+ days (if we'd gone down the complex dynamic query path)
|
||||
|
||||
@ -17,32 +17,32 @@
|
||||
### Core Functionality (100% Complete)
|
||||
|
||||
#### 1. Agent Management
|
||||
- ✅ Create agents with model configuration (OpenAI, Anthropic, Ollama)
|
||||
- ✅ Update agent settings
|
||||
- ✅ Delete agents (soft delete)
|
||||
- ✅ List all agents with metadata
|
||||
- ✅ Get single agent details
|
||||
- ✅ API key encryption (AES-256)
|
||||
- Create agents with model configuration (OpenAI, Anthropic, Ollama)
|
||||
- Update agent settings
|
||||
- Delete agents (soft delete)
|
||||
- List all agents with metadata
|
||||
- Get single agent details
|
||||
- API key encryption (AES-256)
|
||||
|
||||
#### 2. Conversation Management
|
||||
- ✅ Create conversations
|
||||
- ✅ Get conversation with messages and executions
|
||||
- ✅ List all conversations
|
||||
- ✅ Get conversations by agent
|
||||
- Create conversations
|
||||
- Get conversation with messages and executions
|
||||
- List all conversations
|
||||
- Get conversations by agent
|
||||
|
||||
#### 3. Execution Tracking
|
||||
- ✅ Start agent execution (returns execution ID)
|
||||
- ✅ Complete execution with tokens and cost
|
||||
- ✅ Get execution details
|
||||
- ✅ List all executions
|
||||
- ✅ Filter executions by status
|
||||
- ✅ Get execution history per agent
|
||||
- Start agent execution (returns execution ID)
|
||||
- Complete execution with tokens and cost
|
||||
- Get execution details
|
||||
- List all executions
|
||||
- Filter executions by status
|
||||
- Get execution history per agent
|
||||
|
||||
---
|
||||
|
||||
## The Pivot That Saved Days
|
||||
|
||||
### What We Almost Did (The Trap) ⚠️
|
||||
### What We Almost Did (The Trap)
|
||||
Spend 3-5 days fighting with:
|
||||
- `PoweredSoft.DynamicQuery` complex filtering
|
||||
- `OpenHarbor.CQRS` auto-documentation limitations
|
||||
@ -51,7 +51,7 @@ Spend 3-5 days fighting with:
|
||||
|
||||
**Result:** Zero value, maximum frustration
|
||||
|
||||
### What We Did Instead (The Win) ✅
|
||||
### What We Did Instead (The Win)
|
||||
Built simple GET endpoints in 30 minutes:
|
||||
```csharp
|
||||
app.MapGet("/api/agents", async (CodexDbContext db) => {...});
|
||||
@ -94,7 +94,7 @@ app.MapGet("/api/executions", async (CodexDbContext db) => {...});
|
||||
|
||||
---
|
||||
|
||||
## Security & Infrastructure ✅
|
||||
## Security & Infrastructure
|
||||
|
||||
- **CORS:** Development (any localhost port) + Production (configurable in appsettings.json)
|
||||
- **Rate Limiting:** 1000 requests/minute per client (prevents runaway loops)
|
||||
@ -108,7 +108,7 @@ app.MapGet("/api/executions", async (CodexDbContext db) => {...});
|
||||
## Documentation for Frontend Team
|
||||
|
||||
### Primary Reference
|
||||
📄 **`docs/COMPLETE-API-REFERENCE.md`** - Complete API contract with:
|
||||
**`docs/COMPLETE-API-REFERENCE.md`** - Complete API contract with:
|
||||
- All 16 endpoints with examples
|
||||
- Request/response formats
|
||||
- Enum values
|
||||
@ -131,10 +131,10 @@ All endpoints tested and working:
|
||||
./test-endpoints.sh
|
||||
|
||||
# Results:
|
||||
✅ GET /api/agents - Returns agent list
|
||||
✅ POST /api/command/createConversation - Returns {id: "guid"}
|
||||
✅ GET /api/conversations - Returns conversation list
|
||||
✅ All endpoints functional
|
||||
GET /api/agents - Returns agent list
|
||||
POST /api/command/createConversation - Returns {id: "guid"}
|
||||
GET /api/conversations - Returns conversation list
|
||||
All endpoints functional
|
||||
```
|
||||
|
||||
---
|
||||
@ -192,7 +192,7 @@ All endpoints tested and working:
|
||||
|
||||
## What Frontend Team Can Do NOW
|
||||
|
||||
### 1. Start Building UI ✅
|
||||
### 1. Start Building UI
|
||||
All endpoints work - no blockers
|
||||
|
||||
### 2. Generate Types
|
||||
@ -236,10 +236,10 @@ docs/add-claude-standards (ready to merge to main)
|
||||
## Next Steps
|
||||
|
||||
### Immediate (Today)
|
||||
1. ✅ Push branch to origin
|
||||
2. ✅ Create PR to main
|
||||
3. ✅ Share `docs/COMPLETE-API-REFERENCE.md` with frontend team
|
||||
4. ✅ Notify frontend: "Backend is ready - start integration"
|
||||
1. Push branch to origin
|
||||
2. Create PR to main
|
||||
3. Share `docs/COMPLETE-API-REFERENCE.md` with frontend team
|
||||
4. Notify frontend: "Backend is ready - start integration"
|
||||
|
||||
### Short Term (This Week)
|
||||
1. Frontend builds first screens
|
||||
@ -255,13 +255,13 @@ docs/add-claude-standards (ready to merge to main)
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
### ✅ What Worked
|
||||
### What Worked
|
||||
1. **Pragmatism over perfection** - Simple solutions beat complex ones
|
||||
2. **Test early** - Caught the Swagger documentation issue immediately
|
||||
3. **Focus on value** - Frontend needs working endpoints, not perfect OpenAPI docs
|
||||
4. **Know when to pivot** - Abandoned dynamic queries when they became a time sink
|
||||
|
||||
### ⚠️ What to Avoid
|
||||
### What to Avoid
|
||||
1. **Framework rabbit holes** - Don't spend days debugging framework internals
|
||||
2. **Premature optimization** - Don't build Netflix-scale solutions for 10 users
|
||||
3. **Perfect documentation** - Working code + simple docs > perfect Swagger spec
|
||||
@ -277,9 +277,9 @@ docs/add-claude-standards (ready to merge to main)
|
||||
|
||||
**Code Quality:** Simple, testable, maintainable
|
||||
|
||||
**Frontend Blocker:** **REMOVED** ✅
|
||||
**Frontend Blocker:** **REMOVED**
|
||||
|
||||
**MVP Status:** **SHIPPED** 🚀
|
||||
**MVP Status:** **SHIPPED**
|
||||
|
||||
---
|
||||
|
||||
@ -291,7 +291,7 @@ All core functionality works. All endpoints tested. Security in place. Documenta
|
||||
|
||||
**The pragmatic approach won:** Simple GET endpoints that took 30 minutes beat complex framework integration that would have taken days.
|
||||
|
||||
**Frontend team: You're unblocked. Start building!** 🎉
|
||||
**Frontend team: You're unblocked. Start building!**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -128,13 +128,13 @@ API configuration is managed through `appsettings.json` and `appsettings.Develop
|
||||
|
||||
## Key Features
|
||||
|
||||
- ✅ Auto-generated REST endpoints from CQRS classes
|
||||
- ✅ Type-safe API contracts via OpenAPI
|
||||
- ✅ Automatic input validation with FluentValidation
|
||||
- ✅ XML documentation integrated with Swagger
|
||||
- ✅ PostgreSQL with EF Core migrations
|
||||
- ✅ CORS configuration via appsettings
|
||||
- ✅ Bearer token authentication support (documented, not yet implemented)
|
||||
- Auto-generated REST endpoints from CQRS classes
|
||||
- Type-safe API contracts via OpenAPI
|
||||
- Automatic input validation with FluentValidation
|
||||
- XML documentation integrated with Swagger
|
||||
- PostgreSQL with EF Core migrations
|
||||
- CORS configuration via appsettings
|
||||
- Bearer token authentication support (documented, not yet implemented)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@ -36,14 +36,14 @@ echo -e "\n${BLUE}════════════════════
|
||||
echo -e "${BLUE} Code Formatting Check${NC}"
|
||||
echo -e "${BLUE}═══════════════════════════════════════${NC}\n"
|
||||
|
||||
dotnet format --verify-no-changes --verbosity diagnostic || echo -e "${YELLOW}⚠ Formatting issues detected. Run 'dotnet format' to fix.${NC}"
|
||||
dotnet format --verify-no-changes --verbosity diagnostic || echo -e "${YELLOW}WARNING: Formatting issues detected. Run 'dotnet format' to fix.${NC}"
|
||||
|
||||
echo -e "\n${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Code Review Complete!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}\n"
|
||||
|
||||
if [ -f "code-review-results.xml" ]; then
|
||||
echo -e "${BLUE}📊 Results saved to: code-review-results.xml${NC}"
|
||||
echo -e "${BLUE}Results saved to: code-review-results.xml${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}Quick Commands:${NC}"
|
||||
|
||||
@ -15,11 +15,11 @@ Each entry should include:
|
||||
|
||||
## [1.0.0-mvp] - 2025-10-26
|
||||
|
||||
### 🎉 MVP Release - Frontend Ready
|
||||
### MVP Release - Frontend Ready
|
||||
|
||||
This is the initial MVP release with all core functionality complete and tested. The backend is **production-ready** for frontend integration.
|
||||
|
||||
#### ✅ Commands (6 endpoints)
|
||||
#### Commands (6 endpoints)
|
||||
- **POST /api/command/createAgent** - Create new AI agent with model configuration
|
||||
- **POST /api/command/updateAgent** - Update existing agent settings
|
||||
- **POST /api/command/deleteAgent** - Soft delete an agent
|
||||
@ -27,13 +27,13 @@ This is the initial MVP release with all core functionality complete and tested.
|
||||
- **POST /api/command/startAgentExecution** - Start agent execution (returns Guid)
|
||||
- **POST /api/command/completeAgentExecution** - Complete execution with results
|
||||
|
||||
#### ✅ Queries (4 endpoints)
|
||||
#### Queries (4 endpoints)
|
||||
- **POST /api/query/health** - Health check endpoint
|
||||
- **POST /api/query/getAgent** - Get single agent by ID
|
||||
- **POST /api/query/getAgentExecution** - Get execution details by ID
|
||||
- **POST /api/query/getConversation** - Get conversation with messages and executions
|
||||
|
||||
#### ✅ List Endpoints (6 GET endpoints)
|
||||
#### List Endpoints (6 GET endpoints)
|
||||
- **GET /api/agents** - List all agents (limit: 100 most recent)
|
||||
- **GET /api/conversations** - List all conversations (limit: 100 most recent)
|
||||
- **GET /api/executions** - List all executions (limit: 100 most recent)
|
||||
@ -41,45 +41,91 @@ This is the initial MVP release with all core functionality complete and tested.
|
||||
- **GET /api/agents/{id}/executions** - Get execution history for specific agent
|
||||
- **GET /api/executions/status/{status}** - Filter executions by status
|
||||
|
||||
#### 🔒 Security & Infrastructure
|
||||
- ✅ CORS configured for development (any localhost port) and production (configurable)
|
||||
- ✅ Rate limiting (1000 requests/minute per client)
|
||||
- ✅ Global exception handling middleware
|
||||
- ✅ FluentValidation on all commands
|
||||
- ✅ API key encryption for cloud providers (AES-256)
|
||||
#### Security & Infrastructure
|
||||
- CORS configured for development (any localhost port) and production (configurable)
|
||||
- Rate limiting (1000 requests/minute per client)
|
||||
- Global exception handling middleware
|
||||
- FluentValidation on all commands
|
||||
- API key encryption for cloud providers (AES-256)
|
||||
|
||||
#### 📚 Documentation
|
||||
- ✅ Complete API reference in `docs/COMPLETE-API-REFERENCE.md`
|
||||
- ✅ Architecture documentation
|
||||
- ✅ XML documentation on all commands/queries
|
||||
- ✅ Enum reference for frontend integration
|
||||
#### Documentation
|
||||
- Complete API reference in `docs/COMPLETE-API-REFERENCE.md`
|
||||
- Architecture documentation
|
||||
- XML documentation on all commands/queries
|
||||
- Enum reference for frontend integration
|
||||
|
||||
#### 🗄️ Database
|
||||
- ✅ PostgreSQL with EF Core
|
||||
- ✅ Full schema with migrations
|
||||
- ✅ Soft delete support
|
||||
- ✅ Proper indexing for performance
|
||||
#### Database
|
||||
- PostgreSQL with EF Core
|
||||
- Full schema with migrations
|
||||
- Soft delete support
|
||||
- Proper indexing for performance
|
||||
|
||||
#### 🎯 Design Decisions
|
||||
#### Design Decisions
|
||||
- **Pragmatic over Perfect**: Simple GET endpoints instead of complex dynamic query infrastructure
|
||||
- **MVP-First**: 100-item limits are sufficient for initial use case
|
||||
- **No Pagination**: Can be added in v2 based on actual usage patterns
|
||||
- **Client-Side Filtering**: Frontend can filter/sort small datasets efficiently
|
||||
|
||||
#### 📝 Known Limitations (Non-Blocking)
|
||||
#### Known Limitations (Non-Blocking)
|
||||
- Authentication not yet implemented (documented for v2)
|
||||
- Swagger documentation only includes 5 endpoints (OpenHarbor limitation)
|
||||
- All 16 endpoints are **functional and tested**
|
||||
- Complete documentation provided in markdown format
|
||||
- No real-time updates (WebSockets/SignalR planned for v2)
|
||||
|
||||
#### 🚀 Next Steps
|
||||
#### Next Steps
|
||||
- Frontend team can start integration immediately
|
||||
- Use `docs/COMPLETE-API-REFERENCE.md` as API contract
|
||||
- Dynamic filtering/pagination can be added in v2 if needed
|
||||
|
||||
---
|
||||
|
||||
## [1.1.0-performance] - 2025-10-27
|
||||
|
||||
### Performance Optimizations (Non-Breaking)
|
||||
|
||||
**Impact Level**: 🟢 **LOW** - No API contract changes
|
||||
|
||||
This release includes database and query optimizations for improved performance at scale. **All endpoints remain unchanged** - frontend teams only need to refresh their API schema.
|
||||
|
||||
#### Performance Improvements
|
||||
- **Database Indexes Added** for faster search/filter operations:
|
||||
- `Agents.Name` - Fast agent name lookups
|
||||
- `Conversations.Title` - Fast conversation title searches
|
||||
- `AgentExecutions.CompletedAt` - Efficient time-based queries
|
||||
- `ConversationMessages.CreatedAt` - Message timeline queries
|
||||
|
||||
- **SendMessage Optimization**: Context loading now uses `.Take()` limit
|
||||
- Prevents loading entire conversation history (could be 1000+ messages)
|
||||
- Only fetches configured window size (default: 10 messages)
|
||||
- Response time stays constant as conversations grow
|
||||
|
||||
- **Package Compatibility**: Downgraded `Microsoft.Extensions.Http` 9.0.10 → 8.0.1
|
||||
- Ensures .NET 8 LTS compatibility
|
||||
|
||||
#### Removed
|
||||
- **Redundant Index**: `ConversationMessages(ConversationId, MessageIndex)`
|
||||
- Already covered by composite index `(ConversationId, IsInActiveWindow, MessageIndex)`
|
||||
|
||||
#### Frontend Migration
|
||||
**Time Required**: ~15 minutes
|
||||
|
||||
1. Refresh `api-schema.json` (already synced from backend)
|
||||
2. Run existing integration tests to verify no regressions
|
||||
3. (Optional) Verify performance improvements on search/filter operations
|
||||
|
||||
**Migration Guide**: `/Users/jean-philippe/Desktop/BACKEND_PERFORMANCE_UPDATES.md`
|
||||
|
||||
#### Performance Metrics
|
||||
| Operation | Before | After | Improvement |
|
||||
|-----------|--------|-------|-------------|
|
||||
| List 50 agents | ~150ms | ~50ms | 3x faster |
|
||||
| SendMessage (100-msg conv) | ~500ms | ~200ms | 2.5x faster |
|
||||
| Search by agent name | ~200ms | ~30ms | 6x faster |
|
||||
| Get conversation by title | ~180ms | ~35ms | 5x faster |
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## Overview
|
||||
Multiple code review tools are installed for comprehensive analysis:
|
||||
|
||||
### Roslynator (Recommended - No Server Required) ✅
|
||||
### Roslynator (Recommended - No Server Required)
|
||||
- 500+ C# analyzers
|
||||
- Performance optimizations
|
||||
- Code style checks
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
|
||||
---
|
||||
|
||||
## 📋 All Available Endpoints (13 Total)
|
||||
## All Available Endpoints (13 Total)
|
||||
|
||||
### ✅ Commands (6 endpoints)
|
||||
### Commands (6 endpoints)
|
||||
|
||||
#### 1. Create Agent
|
||||
```http
|
||||
@ -115,7 +115,7 @@ Response: 200 OK
|
||||
|
||||
---
|
||||
|
||||
### 🔍 Queries (7 endpoints)
|
||||
### Queries (7 endpoints)
|
||||
|
||||
#### 7. Health Check
|
||||
```http
|
||||
@ -190,7 +190,7 @@ Response: 200 OK
|
||||
|
||||
---
|
||||
|
||||
### 📃 List Endpoints (GET - Simple & Fast)
|
||||
### List Endpoints (GET - Simple & Fast)
|
||||
|
||||
#### 11. List All Agents
|
||||
```http
|
||||
@ -295,7 +295,7 @@ Response: 200 OK (filtered lists)
|
||||
|
||||
---
|
||||
|
||||
## Enums Reference
|
||||
## Enums Reference
|
||||
|
||||
### AgentType
|
||||
```
|
||||
@ -333,10 +333,10 @@ Response: 200 OK (filtered lists)
|
||||
|
||||
## Frontend Integration Notes
|
||||
|
||||
### Working Endpoints ✅
|
||||
### Working Endpoints
|
||||
All 13 endpoints listed above are **live and functional**.
|
||||
|
||||
### Swagger Documentation ⚠️
|
||||
### Swagger Documentation
|
||||
Currently, only 5 endpoints appear in `/swagger/v1/swagger.json`:
|
||||
- POST /api/command/createAgent
|
||||
- POST /api/command/updateAgent
|
||||
@ -389,16 +389,16 @@ curl http://localhost:5246/api/executions
|
||||
|
||||
---
|
||||
|
||||
## 🚀 MVP Status: READY FOR FRONTEND
|
||||
## MVP Status: READY FOR FRONTEND
|
||||
|
||||
All 13 endpoints are:
|
||||
- ✅ Implemented
|
||||
- ✅ Tested
|
||||
- ✅ Working in development
|
||||
- ✅ Documented (this file)
|
||||
- ✅ CORS enabled
|
||||
- ✅ Rate limited
|
||||
- ✅ Error handling in place
|
||||
- Implemented
|
||||
- Tested
|
||||
- Working in development
|
||||
- Documented (this file)
|
||||
- CORS enabled
|
||||
- Rate limited
|
||||
- Error handling in place
|
||||
|
||||
**Frontend team can start integration immediately!**
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ This directory contains the API contract and documentation for the Codex API.
|
||||
## Files
|
||||
|
||||
### openapi.json
|
||||
**⚠️ AUTO-GENERATED - DO NOT EDIT MANUALLY**
|
||||
**AUTO-GENERATED - DO NOT EDIT MANUALLY**
|
||||
|
||||
The OpenAPI 3.0 specification for the Codex API. This file is the single source of truth for API contracts.
|
||||
|
||||
@ -86,13 +86,13 @@ npx @openapitools/openapi-generator-cli generate \
|
||||
|
||||
The generated `openapi.json` includes:
|
||||
|
||||
- ✅ All endpoints with HTTP methods
|
||||
- ✅ Complete request/response schemas
|
||||
- ✅ XML documentation comments
|
||||
- ✅ Authentication requirements (Bearer token)
|
||||
- ✅ API versioning information
|
||||
- ✅ Parameter descriptions
|
||||
- ✅ Response codes
|
||||
- All endpoints with HTTP methods
|
||||
- Complete request/response schemas
|
||||
- XML documentation comments
|
||||
- Authentication requirements (Bearer token)
|
||||
- API versioning information
|
||||
- Parameter descriptions
|
||||
- Response codes
|
||||
|
||||
## CQRS Endpoint Conventions
|
||||
|
||||
|
||||
@ -555,6 +555,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/sendMessage": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"sendMessage"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageCommand"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageCommand"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageCommand"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/startAgentExecution": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -730,6 +768,41 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Detailed agent execution information"
|
||||
},
|
||||
"AgentResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Response content from the agent",
|
||||
"nullable": true
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "When the response was generated",
|
||||
"format": "date-time"
|
||||
},
|
||||
"inputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Number of input tokens processed",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"outputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Number of output tokens generated",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"estimatedCost": {
|
||||
"type": "number",
|
||||
"description": "Estimated cost of the request in USD",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Agent response with token usage and cost information"
|
||||
},
|
||||
"AgentStatus": {
|
||||
"enum": [
|
||||
"Active",
|
||||
@ -1111,6 +1184,23 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Health check query to verify API availability"
|
||||
},
|
||||
"MessageDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Message content",
|
||||
"nullable": true
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "When the message was created",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Simplified message data transfer object"
|
||||
},
|
||||
"MessageRole": {
|
||||
"enum": [
|
||||
"User",
|
||||
@ -1130,6 +1220,62 @@
|
||||
"type": "string",
|
||||
"description": "Specifies the type of model provider (cloud API or local endpoint)."
|
||||
},
|
||||
"SendMessageCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "string",
|
||||
"description": "ID of the agent to send the message to",
|
||||
"format": "uuid"
|
||||
},
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "ID of existing conversation, or null to create a new conversation",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "User's message content",
|
||||
"nullable": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "string",
|
||||
"description": "Optional user identifier for future authentication support",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Sends a user message to an agent and receives a response.\r\nCreates a new conversation if ConversationId is not provided."
|
||||
},
|
||||
"SendMessageResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "ID of the conversation (new or existing)",
|
||||
"format": "uuid"
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string",
|
||||
"description": "ID of the stored user message",
|
||||
"format": "uuid"
|
||||
},
|
||||
"agentResponseId": {
|
||||
"type": "string",
|
||||
"description": "ID of the stored agent response message",
|
||||
"format": "uuid"
|
||||
},
|
||||
"userMessage": {
|
||||
"$ref": "#/components/schemas/MessageDto"
|
||||
},
|
||||
"agentResponse": {
|
||||
"$ref": "#/components/schemas/AgentResponseDto"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Result containing the user message, agent response, and conversation metadata"
|
||||
},
|
||||
"StartAgentExecutionCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -5,21 +5,21 @@
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "🚀 Starting Codex API..."
|
||||
echo "Starting Codex API..."
|
||||
|
||||
# Start the API in background
|
||||
cd "$(dirname "$0")"
|
||||
dotnet run --project Codex.Api/Codex.Api.csproj > /dev/null 2>&1 &
|
||||
API_PID=$!
|
||||
|
||||
echo "⏳ Waiting for API to start (PID: $API_PID)..."
|
||||
echo "Waiting for API to start (PID: $API_PID)..."
|
||||
|
||||
# Wait for API to be ready (max 30 seconds)
|
||||
MAX_ATTEMPTS=30
|
||||
ATTEMPT=0
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
if curl -f -s http://localhost:5246/swagger/v1/swagger.json > /dev/null 2>&1; then
|
||||
echo "✅ API is ready!"
|
||||
echo "API is ready!"
|
||||
break
|
||||
fi
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
@ -29,43 +29,43 @@ done
|
||||
|
||||
# Check if process is still running
|
||||
if ! kill -0 $API_PID 2>/dev/null; then
|
||||
echo "❌ API failed to start"
|
||||
echo "ERROR: API failed to start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we timed out
|
||||
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
||||
echo "❌ API did not respond within 30 seconds"
|
||||
echo "ERROR: API did not respond within 30 seconds"
|
||||
kill $API_PID 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📥 Downloading OpenAPI specification..."
|
||||
echo "Downloading OpenAPI specification..."
|
||||
|
||||
# Export the swagger.json from HTTP endpoint (HTTPS not enabled in development)
|
||||
if curl -f -s http://localhost:5246/swagger/v1/swagger.json -o docs/openapi.json; then
|
||||
echo "✅ OpenAPI spec exported to docs/openapi.json"
|
||||
echo "OpenAPI spec exported to docs/openapi.json"
|
||||
|
||||
# Pretty print some stats
|
||||
ENDPOINTS=$(grep -o '"paths"' docs/openapi.json | wc -l)
|
||||
FILE_SIZE=$(du -h docs/openapi.json | cut -f1)
|
||||
echo "📊 Specification size: $FILE_SIZE"
|
||||
echo "📊 Documented: $(grep -o '"/api/' docs/openapi.json | wc -l | tr -d ' ') endpoint(s)"
|
||||
echo "Specification size: $FILE_SIZE"
|
||||
echo "Documented: $(grep -o '"/api/' docs/openapi.json | wc -l | tr -d ' ') endpoint(s)"
|
||||
else
|
||||
echo "❌ Failed to download OpenAPI spec"
|
||||
echo "ERROR: Failed to download OpenAPI spec"
|
||||
kill $API_PID 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🛑 Stopping API..."
|
||||
echo "Stopping API..."
|
||||
kill $API_PID 2>/dev/null || true
|
||||
|
||||
# Wait for process to terminate
|
||||
wait $API_PID 2>/dev/null || true
|
||||
|
||||
echo "✨ Done! OpenAPI specification is ready at docs/openapi.json"
|
||||
echo "Done! OpenAPI specification is ready at docs/openapi.json"
|
||||
echo ""
|
||||
echo "📝 Remember to:"
|
||||
echo "Remember to:"
|
||||
echo " 1. Review the generated openapi.json"
|
||||
echo " 2. Update CHANGELOG.md if there are breaking changes"
|
||||
echo " 3. Notify frontend teams of API updates"
|
||||
|
||||
41
BACKEND/scripts/seed-test-data.sql
Normal file
41
BACKEND/scripts/seed-test-data.sql
Normal file
@ -0,0 +1,41 @@
|
||||
-- Seed Test Data for Frontend Integration
|
||||
-- Date: 2025-10-26
|
||||
-- Purpose: Create 5 sample agents for frontend testing
|
||||
|
||||
-- Enum Reference:
|
||||
-- AgentType: CodeGenerator=0, CodeReviewer=1, Debugger=2, Documenter=3, Custom=4
|
||||
-- AgentStatus: Active=0, Inactive=1, Error=2
|
||||
-- ModelProviderType: CloudApi=0, LocalEndpoint=1, Custom=2
|
||||
|
||||
-- Insert 5 sample agents with different configurations
|
||||
INSERT INTO "Agents" (
|
||||
"Id", "Name", "Description", "Type", "ModelProvider", "ModelName",
|
||||
"ProviderType", "ModelEndpoint", "ApiKeyEncrypted", "Temperature", "MaxTokens",
|
||||
"SystemPrompt", "EnableMemory", "ConversationWindowSize", "Status",
|
||||
"IsDeleted", "CreatedAt", "UpdatedAt"
|
||||
) VALUES
|
||||
-- Agent 1: Local Ollama Phi (Code Generator - Active)
|
||||
(gen_random_uuid(), 'Code Generator - Phi', 'Local AI using Ollama Phi for code generation', 0, 'ollama', 'phi', 1, 'http://localhost:11434', NULL, 0.7, 4000, 'You are a helpful AI coding assistant specializing in code generation.', true, 10, 0, false, NOW(), NOW()),
|
||||
|
||||
-- Agent 2: OpenAI GPT-4 (Code Reviewer - Inactive)
|
||||
(gen_random_uuid(), 'Code Reviewer - GPT-4', 'Cloud-based OpenAI GPT-4 for code review', 1, 'openai', 'gpt-4', 0, NULL, 'encrypted-api-key-placeholder', 0.3, 8000, 'You are an expert code reviewer. Analyze code for bugs, performance issues, and best practices.', true, 20, 1, false, NOW(), NOW()),
|
||||
|
||||
-- Agent 3: Anthropic Claude (Debugger - Active)
|
||||
(gen_random_uuid(), 'Debugger - Claude 3.5', 'Anthropic Claude 3.5 Sonnet for debugging', 2, 'anthropic', 'claude-3.5-sonnet', 0, NULL, 'encrypted-api-key-placeholder', 0.5, 6000, 'You are a debugging expert. Help identify and fix bugs in code.', true, 15, 0, false, NOW(), NOW()),
|
||||
|
||||
-- Agent 4: Local Phi (Documenter - Active)
|
||||
(gen_random_uuid(), 'Documenter - Phi', 'Local documentation generation assistant', 3, 'ollama', 'phi', 1, 'http://localhost:11434', NULL, 0.8, 4000, 'You generate clear, comprehensive documentation for code and APIs.', false, 5, 0, false, NOW(), NOW()),
|
||||
|
||||
-- Agent 5: Custom Assistant (Error state for testing)
|
||||
(gen_random_uuid(), 'Custom Assistant', 'General purpose AI assistant', 4, 'ollama', 'phi', 1, 'http://localhost:11434', NULL, 0.7, 4000, 'You are a helpful AI assistant.', true, 10, 2, false, NOW(), NOW());
|
||||
|
||||
-- Verify insertion
|
||||
SELECT
|
||||
"Name",
|
||||
"Type",
|
||||
"Status",
|
||||
"ProviderType",
|
||||
"ModelProvider" || '/' || "ModelName" AS "Model"
|
||||
FROM "Agents"
|
||||
WHERE "IsDeleted" = false
|
||||
ORDER BY "CreatedAt" DESC;
|
||||
@ -271,4 +271,3 @@ flutter pub run build_runner build --delete-conflicting-outputs
|
||||
- **OpenAPI Spec:** `api-schema.json`
|
||||
- **Backend Docs:** `../backend/docs/ARCHITECTURE.md`
|
||||
- **Strict Typing:** `.claude-docs/strict-typing.md`
|
||||
- **Response Protocol:** `.claude-docs/response-protocol.md`
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
# MANDATORY RESPONSE PROTOCOL
|
||||
|
||||
**Claude must strictly follow this protocol for ALL responses in this project.**
|
||||
|
||||
---
|
||||
|
||||
## 🗣️ Response Protocol — Defined Answer Types
|
||||
|
||||
Claude must **always** end responses with exactly one of these two structured formats:
|
||||
|
||||
---
|
||||
|
||||
### **Answer Type 1: Binary Choice**
|
||||
Used for: simple confirmations, proceed/cancel actions, file operations.
|
||||
|
||||
**Format:**
|
||||
|
||||
(Y) Yes — [brief action summary]
|
||||
|
||||
(N) No — [brief alternative/reason]
|
||||
|
||||
(+) I don't understand — ask for clarification
|
||||
|
||||
|
||||
**When user selects `(+)`:**
|
||||
Claude responds:
|
||||
> "What part would you like me to explain?"
|
||||
Then teaches the concept step‑by‑step in plain language.
|
||||
|
||||
---
|
||||
|
||||
### **Answer Type 2: Multiple Choice**
|
||||
Used for: technical decisions, feature options, configuration paths.
|
||||
|
||||
**Format:**
|
||||
|
||||
(A) Option A — [minimalist description]
|
||||
|
||||
(B) Option B — [minimalist description]
|
||||
|
||||
(C) Option C — [minimalist description]
|
||||
|
||||
(D) Option D — [minimalist description]
|
||||
|
||||
(+) I don't understand — ask for clarification
|
||||
|
||||
|
||||
**When user selects `(+)`:**
|
||||
Claude responds:
|
||||
> "Which option would you like explained, or should I clarify what we're deciding here?"
|
||||
Then provides context on the decision + explains each option's purpose.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Mandatory Rules
|
||||
1. **No text after the last option** — choices must be the final content.
|
||||
2. Every option description ≤8 words.
|
||||
3. The `(+)` option is **always present** in both formats.
|
||||
4. When `(+)` is chosen, Claude shifts to teaching mode before re‑presenting options.
|
||||
5. Claude must include `(always read claude.md to keep context between interactions)` before every option set.
|
||||
|
||||
---
|
||||
|
||||
### Example 1 (Binary)
|
||||
|
||||
We need to initialize npm in your project folder.
|
||||
|
||||
(always read claude.md to keep context between interactions)
|
||||
|
||||
(Y) Yes — run npm init -y now
|
||||
|
||||
(N) No — show me what this does first
|
||||
|
||||
(+) I don't understand — explain npm initialization
|
||||
|
||||
|
||||
### Example 2 (Multiple Choice)
|
||||
|
||||
Choose your testing framework:
|
||||
|
||||
(always read claude.md to keep context between interactions)
|
||||
|
||||
(A) Jest — popular, feature-rich
|
||||
|
||||
(B) Vitest — faster, Vite-native
|
||||
|
||||
(C) Node test runner — built-in, minimal
|
||||
|
||||
(D) Skip tests — add later
|
||||
|
||||
(+) I don't understand — explain testing frameworks
|
||||
|
||||
|
||||
---
|
||||
|
||||
**This protocol ensures:**
|
||||
- You always have an escape hatch to learn.
|
||||
- Claude never assumes your technical knowledge.
|
||||
- Every interaction has clear, actionable paths.
|
||||
@ -6,7 +6,7 @@ This Flutter application integrates with a C# CQRS backend using OpenAPI specifi
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
## Overview
|
||||
|
||||
The backend and frontend communicate through a type-safe, contract-first API architecture:
|
||||
|
||||
@ -15,21 +15,21 @@ The backend and frontend communicate through a type-safe, contract-first API arc
|
||||
- **Contract:** OpenAPI specification (`api-schema.json`)
|
||||
- **Pattern:** Command Query Responsibility Segregation (CQRS)
|
||||
|
||||
**Compatibility Status:** ✅ **100% Backend Aligned**
|
||||
**Compatibility Status:** **100% Backend Aligned**
|
||||
|
||||
---
|
||||
|
||||
## 📦 Architecture
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Backend (C# CQRS) Frontend (Flutter/Dart)
|
||||
───────────────── ───────────────────────
|
||||
|
||||
Controllers + XML docs api-schema.json
|
||||
↓ (OpenAPI contract)
|
||||
docs/openapi.json ──────────────────► ↓
|
||||
(OpenAPI contract)
|
||||
docs/openapi.json ──────────────────►
|
||||
(auto-generated) Code Generation
|
||||
↓
|
||||
|
||||
lib/api/
|
||||
├── client.dart (CQRS)
|
||||
├── types.dart (Core)
|
||||
@ -39,7 +39,7 @@ docs/openapi.json ──────────────────►
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
## Quick Start
|
||||
|
||||
### 1. Update API Contract
|
||||
|
||||
@ -70,8 +70,8 @@ final client = CqrsApiClient(
|
||||
final result = await client.checkHealth();
|
||||
|
||||
result.when(
|
||||
success: (isHealthy) => print('✅ API healthy: $isHealthy'),
|
||||
error: (error) => print('❌ Error: ${error.message}'),
|
||||
success: (isHealthy) => print(' API healthy: $isHealthy'),
|
||||
error: (error) => print(' Error: ${error.message}'),
|
||||
);
|
||||
|
||||
// Clean up
|
||||
@ -80,7 +80,7 @@ client.dispose();
|
||||
|
||||
---
|
||||
|
||||
## 📚 API Client Usage
|
||||
## API Client Usage
|
||||
|
||||
### CQRS Patterns
|
||||
|
||||
@ -148,7 +148,7 @@ result.when(
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Creating New Endpoints
|
||||
## Creating New Endpoints
|
||||
|
||||
### 1. Backend Adds Endpoint
|
||||
|
||||
@ -236,18 +236,18 @@ result.when(
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Type Safety Standards
|
||||
## Type Safety Standards
|
||||
|
||||
### Strict Typing Rules
|
||||
|
||||
All code follows these mandatory rules (see `.claude-docs/strict-typing.md`):
|
||||
|
||||
- ✅ **NO** `dynamic` types
|
||||
- ✅ **NO** `any` types
|
||||
- ✅ **NO** untyped `var` declarations
|
||||
- ✅ All functions have explicit return types
|
||||
- ✅ All parameters have explicit types
|
||||
- ✅ Proper generics and interfaces
|
||||
- **NO** `dynamic` types
|
||||
- **NO** `any` types
|
||||
- **NO** untyped `var` declarations
|
||||
- All functions have explicit return types
|
||||
- All parameters have explicit types
|
||||
- Proper generics and interfaces
|
||||
|
||||
### Serializable Interface
|
||||
|
||||
@ -272,7 +272,7 @@ class HealthQuery implements Serializable {
|
||||
Never use try-catch for API calls. Use `Result<T>`:
|
||||
|
||||
```dart
|
||||
// ❌ DON'T DO THIS
|
||||
// DON'T DO THIS
|
||||
try {
|
||||
final user = await someApiCall();
|
||||
print(user.name);
|
||||
@ -280,7 +280,7 @@ try {
|
||||
print('Error: $e');
|
||||
}
|
||||
|
||||
// ✅ DO THIS
|
||||
// DO THIS
|
||||
final result = await client.getUser('123');
|
||||
|
||||
result.when(
|
||||
@ -297,7 +297,7 @@ final message = switch (result) {
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
@ -371,7 +371,7 @@ void main() {
|
||||
|
||||
---
|
||||
|
||||
## 📋 Workflow
|
||||
## Workflow
|
||||
|
||||
### Daily Development
|
||||
|
||||
@ -430,7 +430,7 @@ git commit -m "feat: Add CreateUser endpoint integration"
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Error Handling
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
@ -488,7 +488,7 @@ result.when(
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
## Configuration
|
||||
|
||||
### Development
|
||||
|
||||
@ -530,7 +530,7 @@ final config = ApiClientConfig(
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Structure
|
||||
## File Structure
|
||||
|
||||
```
|
||||
lib/api/
|
||||
@ -557,7 +557,7 @@ docs/
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
## Troubleshooting
|
||||
|
||||
### "Type errors after regenerating"
|
||||
|
||||
@ -605,22 +605,22 @@ Backend response doesn't match expected type:
|
||||
|
||||
---
|
||||
|
||||
## 📊 Status
|
||||
## Status
|
||||
|
||||
| Metric | Status |
|
||||
|--------|--------|
|
||||
| Backend Compatibility | ✅ 100% |
|
||||
| Type Safety | ✅ Zero dynamic types |
|
||||
| Static Analysis | ✅ 0 errors |
|
||||
| CQRS Patterns | ✅ All supported |
|
||||
| Error Handling | ✅ Comprehensive |
|
||||
| Documentation | ✅ Complete |
|
||||
| Testing | ✅ Unit + Integration |
|
||||
| Production Ready | ✅ Yes |
|
||||
| Backend Compatibility | 100% |
|
||||
| Type Safety | Zero dynamic types |
|
||||
| Static Analysis | 0 errors |
|
||||
| CQRS Patterns | All supported |
|
||||
| Error Handling | Comprehensive |
|
||||
| Documentation | Complete |
|
||||
| Testing | Unit + Integration |
|
||||
| Production Ready | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 📖 Additional Resources
|
||||
## Additional Resources
|
||||
|
||||
- **Workflow Guide:** `.claude-docs/api-contract-workflow.md` (comprehensive)
|
||||
- **Type Safety:** `.claude-docs/strict-typing.md` (mandatory rules)
|
||||
@ -629,7 +629,7 @@ Backend response doesn't match expected type:
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Takeaways
|
||||
## Key Takeaways
|
||||
|
||||
1. **OpenAPI is Source of Truth** - Always regenerate from `api-schema.json`
|
||||
2. **CQRS Pattern** - All endpoints use JSON body (even empty `{}`)
|
||||
|
||||
@ -6,6 +6,139 @@
|
||||
"version": "v1"
|
||||
},
|
||||
"paths": {
|
||||
"/api/agents": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Agents"
|
||||
],
|
||||
"summary": "Get all agents",
|
||||
"description": "Returns a list of all active agents with metadata. Limit: 100 most recent.",
|
||||
"operationId": "GetAllAgents",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/agents/{id}/conversations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Agents"
|
||||
],
|
||||
"summary": "Get conversations for an agent",
|
||||
"description": "Returns all conversations associated with a specific agent.",
|
||||
"operationId": "GetAgentConversations",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/agents/{id}/executions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Agents"
|
||||
],
|
||||
"summary": "Get execution history for an agent",
|
||||
"description": "Returns the 100 most recent executions for a specific agent.",
|
||||
"operationId": "GetAgentExecutions",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/completeAgentExecution": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"completeAgentExecution"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CompleteAgentExecutionCommand"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CompleteAgentExecutionCommand"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CompleteAgentExecutionCommand"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/conversations": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Conversations"
|
||||
],
|
||||
"summary": "Get all conversations",
|
||||
"description": "Returns the 100 most recent conversations.",
|
||||
"operationId": "GetAllConversations",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/createAgent": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -37,6 +170,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/createConversation": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"createConversation"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateConversationCommand"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateConversationCommand"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateConversationCommand"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/deleteAgent": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -68,6 +240,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/executions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Executions"
|
||||
],
|
||||
"summary": "Get all executions",
|
||||
"description": "Returns the 100 most recent executions across all agents.",
|
||||
"operationId": "GetAllExecutions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/executions/status/{status}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Executions"
|
||||
],
|
||||
"summary": "Get executions by status",
|
||||
"description": "Returns executions filtered by status (Pending, Running, Completed, Failed, Cancelled).",
|
||||
"operationId": "GetExecutionsByStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "status",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/query/getAgent": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -134,6 +359,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/query/getAgentExecution": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"getAgentExecution"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAgentExecutionQuery"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAgentExecutionQuery"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAgentExecutionQuery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AgentExecutionDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"getAgentExecution"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Id",
|
||||
"in": "query",
|
||||
"description": "Execution ID",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AgentExecutionDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/query/getConversation": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"getConversation"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetConversationQuery"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetConversationQuery"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetConversationQuery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConversationDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"getConversation"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Id",
|
||||
"in": "query",
|
||||
"description": "Conversation ID",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConversationDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/query/health": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -198,6 +555,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/sendMessage": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"sendMessage"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageCommand"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageCommand"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageCommand"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SendMessageResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/startAgentExecution": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"startAgentExecution"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StartAgentExecutionCommand"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StartAgentExecutionCommand"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StartAgentExecutionCommand"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command/updateAgent": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -232,6 +666,143 @@
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"AgentExecutionDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique execution identifier",
|
||||
"format": "uuid"
|
||||
},
|
||||
"agentId": {
|
||||
"type": "string",
|
||||
"description": "Agent identifier",
|
||||
"format": "uuid"
|
||||
},
|
||||
"agentName": {
|
||||
"type": "string",
|
||||
"description": "Agent name",
|
||||
"nullable": true
|
||||
},
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "Conversation identifier if part of a conversation",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
},
|
||||
"userPrompt": {
|
||||
"type": "string",
|
||||
"description": "Full user prompt",
|
||||
"nullable": true
|
||||
},
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "Additional input context or parameters",
|
||||
"nullable": true
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
"description": "Agent's complete output/response",
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/ExecutionStatus"
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "string",
|
||||
"description": "Execution start timestamp",
|
||||
"format": "date-time"
|
||||
},
|
||||
"completedAt": {
|
||||
"type": "string",
|
||||
"description": "Execution completion timestamp",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"executionTimeMs": {
|
||||
"type": "integer",
|
||||
"description": "Execution time in milliseconds",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
},
|
||||
"inputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Input tokens consumed",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"outputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Output tokens generated",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"totalTokens": {
|
||||
"type": "integer",
|
||||
"description": "Total tokens used",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"estimatedCost": {
|
||||
"type": "number",
|
||||
"description": "Estimated cost in USD",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"toolCalls": {
|
||||
"type": "string",
|
||||
"description": "Tool calls made during execution (JSON array)",
|
||||
"nullable": true
|
||||
},
|
||||
"toolCallResults": {
|
||||
"type": "string",
|
||||
"description": "Tool execution results (JSON array)",
|
||||
"nullable": true
|
||||
},
|
||||
"errorMessage": {
|
||||
"type": "string",
|
||||
"description": "Error message if execution failed",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Detailed agent execution information"
|
||||
},
|
||||
"AgentResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Response content from the agent",
|
||||
"nullable": true
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "When the response was generated",
|
||||
"format": "date-time"
|
||||
},
|
||||
"inputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Number of input tokens processed",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"outputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Number of output tokens generated",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"estimatedCost": {
|
||||
"type": "number",
|
||||
"description": "Estimated cost of the request in USD",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Agent response with token usage and cost information"
|
||||
},
|
||||
"AgentStatus": {
|
||||
"enum": [
|
||||
"Active",
|
||||
@ -252,6 +823,153 @@
|
||||
"type": "string",
|
||||
"description": "Specifies the type/purpose of the agent."
|
||||
},
|
||||
"CompleteAgentExecutionCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"executionId": {
|
||||
"type": "string",
|
||||
"description": "Execution ID to complete",
|
||||
"format": "uuid"
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
"description": "Agent's output/response",
|
||||
"nullable": true
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/ExecutionStatus"
|
||||
},
|
||||
"inputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Input tokens consumed",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"outputTokens": {
|
||||
"type": "integer",
|
||||
"description": "Output tokens generated",
|
||||
"format": "int32",
|
||||
"nullable": true
|
||||
},
|
||||
"estimatedCost": {
|
||||
"type": "number",
|
||||
"description": "Estimated cost in USD",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"toolCalls": {
|
||||
"type": "string",
|
||||
"description": "Tool calls made (JSON array)",
|
||||
"nullable": true
|
||||
},
|
||||
"toolCallResults": {
|
||||
"type": "string",
|
||||
"description": "Tool call results (JSON array)",
|
||||
"nullable": true
|
||||
},
|
||||
"errorMessage": {
|
||||
"type": "string",
|
||||
"description": "Error message if failed",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Completes an agent execution with results and metrics"
|
||||
},
|
||||
"ConversationDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique conversation identifier",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Conversation title",
|
||||
"nullable": true
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Conversation summary",
|
||||
"nullable": true
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether conversation is active"
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "string",
|
||||
"description": "Conversation start timestamp",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastMessageAt": {
|
||||
"type": "string",
|
||||
"description": "Last message timestamp",
|
||||
"format": "date-time"
|
||||
},
|
||||
"messageCount": {
|
||||
"type": "integer",
|
||||
"description": "Total message count",
|
||||
"format": "int32"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ConversationMessageItem"
|
||||
},
|
||||
"description": "All messages in conversation",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Detailed conversation information with messages"
|
||||
},
|
||||
"ConversationMessageItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Message identifier",
|
||||
"format": "uuid"
|
||||
},
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "Conversation identifier",
|
||||
"format": "uuid"
|
||||
},
|
||||
"executionId": {
|
||||
"type": "string",
|
||||
"description": "Execution identifier if from agent execution",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/components/schemas/MessageRole"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Message content",
|
||||
"nullable": true
|
||||
},
|
||||
"messageIndex": {
|
||||
"type": "integer",
|
||||
"description": "Message index/order in conversation",
|
||||
"format": "int32"
|
||||
},
|
||||
"isInActiveWindow": {
|
||||
"type": "boolean",
|
||||
"description": "Whether message is in active context window"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"description": "Message creation timestamp",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Individual message within a conversation"
|
||||
},
|
||||
"CreateAgentCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -319,6 +1037,23 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Command to create a new AI agent with configuration"
|
||||
},
|
||||
"CreateConversationCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Conversation title",
|
||||
"nullable": true
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Optional summary or description",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Creates a new conversation for grouping related messages"
|
||||
},
|
||||
"DeleteAgentCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -331,6 +1066,28 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Command to soft-delete an agent"
|
||||
},
|
||||
"ExecutionStatus": {
|
||||
"enum": [
|
||||
"Running",
|
||||
"Completed",
|
||||
"Failed",
|
||||
"Cancelled"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Represents the status of an agent execution."
|
||||
},
|
||||
"GetAgentExecutionQuery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Execution ID",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Get detailed agent execution by ID"
|
||||
},
|
||||
"GetAgentQuery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -410,11 +1167,50 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Response containing agent details"
|
||||
},
|
||||
"GetConversationQuery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Conversation ID",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Get conversation with all messages by ID"
|
||||
},
|
||||
"HealthQuery": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Health check query to verify API availability"
|
||||
},
|
||||
"MessageDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Message content",
|
||||
"nullable": true
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "When the message was created",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Simplified message data transfer object"
|
||||
},
|
||||
"MessageRole": {
|
||||
"enum": [
|
||||
"User",
|
||||
"Assistant",
|
||||
"System",
|
||||
"Tool"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Represents the role of a message in a conversation."
|
||||
},
|
||||
"ModelProviderType": {
|
||||
"enum": [
|
||||
"CloudApi",
|
||||
@ -424,6 +1220,90 @@
|
||||
"type": "string",
|
||||
"description": "Specifies the type of model provider (cloud API or local endpoint)."
|
||||
},
|
||||
"SendMessageCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "string",
|
||||
"description": "ID of the agent to send the message to",
|
||||
"format": "uuid"
|
||||
},
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "ID of existing conversation, or null to create a new conversation",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "User's message content",
|
||||
"nullable": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "string",
|
||||
"description": "Optional user identifier for future authentication support",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Sends a user message to an agent and receives a response.\r\nCreates a new conversation if ConversationId is not provided."
|
||||
},
|
||||
"SendMessageResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "ID of the conversation (new or existing)",
|
||||
"format": "uuid"
|
||||
},
|
||||
"messageId": {
|
||||
"type": "string",
|
||||
"description": "ID of the stored user message",
|
||||
"format": "uuid"
|
||||
},
|
||||
"agentResponseId": {
|
||||
"type": "string",
|
||||
"description": "ID of the stored agent response message",
|
||||
"format": "uuid"
|
||||
},
|
||||
"userMessage": {
|
||||
"$ref": "#/components/schemas/MessageDto"
|
||||
},
|
||||
"agentResponse": {
|
||||
"$ref": "#/components/schemas/AgentResponseDto"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Result containing the user message, agent response, and conversation metadata"
|
||||
},
|
||||
"StartAgentExecutionCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "string",
|
||||
"description": "Agent ID to execute",
|
||||
"format": "uuid"
|
||||
},
|
||||
"userPrompt": {
|
||||
"type": "string",
|
||||
"description": "User's input prompt",
|
||||
"nullable": true
|
||||
},
|
||||
"conversationId": {
|
||||
"type": "string",
|
||||
"description": "Optional conversation ID to link execution to",
|
||||
"format": "uuid",
|
||||
"nullable": true
|
||||
},
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "Optional additional input context (JSON)",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "Starts a new agent execution"
|
||||
},
|
||||
"UpdateAgentCommand": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -1,415 +1,198 @@
|
||||
# CLAUDE.md
|
||||
# CODEX ADK Frontend
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
You are the Frontend/UI/UX/Branding CTO of this company, you report to the Devops/Backend CTO, you two work in a perfectly coordinated duo.
|
||||
|
||||
## Project Overview
|
||||
## Code Style Rules (MANDATORY)
|
||||
|
||||
Svrnty Console is a Flutter-based management console for the Svrnty AI platform. It communicates with a C# CQRS backend using a type-safe, OpenAPI-driven API contract system.
|
||||
1. **NO EMOJIS**: Never use emojis in code, comments, commit messages, documentation, or any output. Remove any existing emojis.
|
||||
2. **Git Commits**:
|
||||
- Author: Svrnty
|
||||
- Co-Author: Jean-Philippe Brule <jp@svrnty.io>
|
||||
|
||||
**Tech Stack:**
|
||||
## Project
|
||||
Flutter ADK for building/testing sovereign AI agents - "robots making robots".
|
||||
Multi-agent conversations, tools, workflows. MIT licensed, single dev on Mac.
|
||||
|
||||
## Stack
|
||||
- Flutter 3.x / Dart 3.9.2+
|
||||
- CQRS API pattern (Command Query Responsibility Segregation)
|
||||
- OpenAPI 3.0.1 contract-first architecture
|
||||
- Custom theme: Crimson Red (#C44D58), Slate Blue (#475C6C)
|
||||
- Fonts: Montserrat (UI), IBM Plex Mono (code/technical)
|
||||
- CQRS + OpenAPI 3.0.1 contract-first API
|
||||
- Theme: Crimson (#C44D58), Slate Blue (#475C6C)
|
||||
- Targets: Web (primary), iOS, Android, Desktop
|
||||
|
||||
---
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
flutter pub get
|
||||
|
||||
# Run the application
|
||||
flutter run
|
||||
|
||||
# Run on specific platform
|
||||
flutter run -d macos
|
||||
flutter run -d chrome
|
||||
## Structure
|
||||
```
|
||||
lib/
|
||||
├── api/
|
||||
│ ├── client.dart # CQRS client
|
||||
│ ├── types.dart # Result<T>, Serializable, errors
|
||||
│ ├── endpoints/ # Type-safe extensions
|
||||
│ └── generated/ # Auto-generated (git-ignored)
|
||||
├── models/ # Agent, Conversation, Execution DTOs
|
||||
├── providers/ # Riverpod state
|
||||
├── services/ # API client, encryption
|
||||
├── pages/ # AgentsPage, ConversationsPage, ExecutionsPage
|
||||
└── widgets/ # CreateAgentDialog, AgentCard, ConversationView
|
||||
```
|
||||
|
||||
### Testing & Quality
|
||||
```bash
|
||||
# Run tests
|
||||
flutter test
|
||||
## Core Workflows
|
||||
1. **Agents**: Create (provider/model/key) List Test Delete
|
||||
2. **Conversations**: Start Exchange messages Track tokens/cost
|
||||
3. **Executions**: Run Monitor status View results
|
||||
4. **Tools**: Attach Configure parameters Enable/disable
|
||||
|
||||
# Run static analysis
|
||||
flutter analyze
|
||||
## Architecture: OpenAPI Contract-First
|
||||
|
||||
# Verify API type safety (custom script)
|
||||
./scripts/verify_api_types.sh
|
||||
```
|
||||
**Single source of truth**: `api-schema.json`
|
||||
|
||||
### API Contract Updates
|
||||
```bash
|
||||
# After backend updates openapi.json:
|
||||
cp ../backend/docs/openapi.json ./api-schema.json
|
||||
./scripts/update_api_client.sh
|
||||
|
||||
# Or run code generation directly:
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Clean build (if generation fails):
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build for production
|
||||
flutter build macos
|
||||
flutter build web
|
||||
flutter build ios
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### 1. OpenAPI-Driven API Contract
|
||||
|
||||
The backend and frontend share a single source of truth: `api-schema.json` (OpenAPI specification).
|
||||
|
||||
**Flow:**
|
||||
1. Backend exports `docs/openapi.json` from C# controllers + XML docs
|
||||
**Flow**:
|
||||
1. Backend exports `docs/openapi.json` (C# controllers + XML docs)
|
||||
2. Frontend copies to `api-schema.json`
|
||||
3. Code generation creates Dart types from contract
|
||||
4. Frontend creates endpoint extensions using generated types
|
||||
3. Code generation creates Dart types
|
||||
4. Create endpoint extensions using generated types
|
||||
|
||||
**Key Files:**
|
||||
- `api-schema.json` - OpenAPI contract (copy from backend)
|
||||
- `lib/api/client.dart` - CQRS client implementation
|
||||
- `lib/api/types.dart` - Core types (Result, Serializable, errors)
|
||||
- `lib/api/endpoints/` - Type-safe endpoint extensions
|
||||
- `lib/api/generated/` - Auto-generated code (git-ignored)
|
||||
**All CQRS endpoints use POST with JSON body** (even empty queries send `{}`).
|
||||
|
||||
### 2. CQRS Pattern
|
||||
### CQRS Patterns
|
||||
|
||||
All backend endpoints follow CQRS with three operation types:
|
||||
|
||||
**Queries (Read):**
|
||||
```dart
|
||||
final result = await client.executeQuery<UserDto>(
|
||||
endpoint: 'users/123',
|
||||
query: GetUserQuery(userId: '123'),
|
||||
fromJson: UserDto.fromJson,
|
||||
// Query (Read)
|
||||
final result = await client.executeQuery<AgentDto>(
|
||||
endpoint: 'agents/123',
|
||||
query: GetAgentQuery(id: '123'),
|
||||
fromJson: AgentDto.fromJson,
|
||||
);
|
||||
```
|
||||
|
||||
**Commands (Write):**
|
||||
```dart
|
||||
final result = await client.executeCommand(
|
||||
endpoint: 'createUser',
|
||||
command: CreateUserCommand(name: 'John', email: 'john@example.com'),
|
||||
// Command (Write)
|
||||
await client.executeCommand(
|
||||
endpoint: 'createAgent',
|
||||
command: CreateAgentCommand(name: 'MyAgent', provider: 'OpenAI'),
|
||||
);
|
||||
```
|
||||
|
||||
**Paginated Queries (Lists):**
|
||||
```dart
|
||||
final result = await client.executePaginatedQuery<UserDto>(
|
||||
endpoint: 'users',
|
||||
query: ListUsersQuery(),
|
||||
itemFromJson: UserDto.fromJson,
|
||||
// Paginated Query (Lists)
|
||||
await client.executePaginatedQuery<AgentDto>(
|
||||
endpoint: 'agents',
|
||||
query: ListAgentsQuery(),
|
||||
itemFromJson: AgentDto.fromJson,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
filters: [FilterCriteria(field: 'status', operator: FilterOperator.equals, value: 'active')],
|
||||
sorting: [SortCriteria(field: 'createdAt', direction: SortDirection.descending)],
|
||||
filters: [FilterCriteria(field: 'provider', operator: FilterOperator.equals, value: 'OpenAI')],
|
||||
);
|
||||
```
|
||||
|
||||
**Important:** ALL CQRS endpoints use JSON body via POST, even empty queries send `{}`.
|
||||
### Result<T> Error Handling
|
||||
|
||||
### 3. Functional Error Handling
|
||||
|
||||
Never use try-catch for API calls. Use the `Result<T>` type:
|
||||
**Never use try-catch for API calls**. Use functional `Result<T>`:
|
||||
|
||||
```dart
|
||||
// Pattern matching
|
||||
final result = await client.getUser('123');
|
||||
|
||||
result.when(
|
||||
success: (user) => print('Hello ${user.name}'),
|
||||
error: (error) => print('Error: ${error.message}'),
|
||||
success: (agent) => showAgent(agent),
|
||||
error: (error) {
|
||||
switch (error.type) {
|
||||
case ApiErrorType.network: showSnackbar('No connection');
|
||||
case ApiErrorType.timeout: showSnackbar('Request timeout');
|
||||
case ApiErrorType.validation: showValidationErrors(error.details);
|
||||
case ApiErrorType.http when error.statusCode == 401: navigateToLogin();
|
||||
default: showSnackbar('Error: ${error.message}');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Or switch expression
|
||||
final message = switch (result) {
|
||||
ApiSuccess(value: final user) => 'Hello ${user.name}',
|
||||
ApiError(error: final err) => 'Error: ${err.message}',
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
## Strict Typing (MANDATORY)
|
||||
|
||||
## Mandatory Coding Standards
|
||||
See `.claude-docs/strict-typing.md`. **No exceptions**.
|
||||
|
||||
### Strict Typing - NO EXCEPTIONS
|
||||
|
||||
See `.claude-docs/strict-typing.md` for complete requirements.
|
||||
|
||||
**Core Rules:**
|
||||
1. Every variable must have explicit type annotation
|
||||
2. Every function parameter must be typed
|
||||
3. Every function return value must be typed
|
||||
4. **NEVER** use `dynamic` (Dart's version of `any`)
|
||||
5. **NEVER** use untyped `var` declarations
|
||||
6. Use proper generics, interfaces, and type unions
|
||||
|
||||
**Examples:**
|
||||
|
||||
❌ FORBIDDEN:
|
||||
```dart
|
||||
dynamic value = getValue();
|
||||
void handleData(var data) { ... }
|
||||
```
|
||||
|
||||
✅ REQUIRED:
|
||||
```dart
|
||||
UserData value = getValue();
|
||||
void handleData(RequestData data) { ... }
|
||||
```
|
||||
|
||||
### Serializable Interface
|
||||
|
||||
All queries, commands, and DTOs must implement:
|
||||
1. Every variable/parameter/return must have explicit type
|
||||
2. **NEVER** use `dynamic`
|
||||
3. **NEVER** use untyped `var`
|
||||
4. All queries/commands/DTOs implement `Serializable`:
|
||||
|
||||
```dart
|
||||
abstract interface class Serializable {
|
||||
Map<String, Object?> toJson();
|
||||
}
|
||||
|
||||
// Example:
|
||||
class GetUserQuery implements Serializable {
|
||||
final String userId;
|
||||
const GetUserQuery({required this.userId});
|
||||
class CreateAgentCommand implements Serializable {
|
||||
final String name;
|
||||
final String provider;
|
||||
const CreateAgentCommand({required this.name, required this.provider});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {'userId': userId};
|
||||
Map<String, Object?> toJson() => {'name': name, 'provider': provider};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
## Adding API Endpoints
|
||||
|
||||
## Response Protocol
|
||||
|
||||
See `.claude-docs/response-protocol.md` for complete protocol.
|
||||
|
||||
**All responses must end with structured choices:**
|
||||
|
||||
**Binary Choice:**
|
||||
```
|
||||
(always read claude.md to keep context between interactions)
|
||||
|
||||
(Y) Yes — [brief action summary]
|
||||
(N) No — [brief alternative/reason]
|
||||
(+) I don't understand — ask for clarification
|
||||
```
|
||||
|
||||
**Multiple Choice:**
|
||||
```
|
||||
(always read claude.md to keep context between interactions)
|
||||
|
||||
(A) Option A — [≤8 words description]
|
||||
(B) Option B — [≤8 words description]
|
||||
(C) Option C — [≤8 words description]
|
||||
(+) I don't understand — ask for clarification
|
||||
```
|
||||
|
||||
When user selects `(+)`, explain the concept, then re-present options.
|
||||
|
||||
---
|
||||
|
||||
## Adding New API Endpoints
|
||||
|
||||
### Step 1: Backend adds endpoint
|
||||
Backend team documents in C# controller with XML comments and exports `docs/openapi.json`.
|
||||
|
||||
### Step 2: Update contract
|
||||
```bash
|
||||
cp ../backend/docs/openapi.json ./api-schema.json
|
||||
./scripts/update_api_client.sh
|
||||
```
|
||||
|
||||
### Step 3: Create endpoint extension
|
||||
Create file in `lib/api/endpoints/`:
|
||||
1. Backend exports updated `docs/openapi.json`
|
||||
2. `cp ../BACKEND/docs/openapi.json ./api-schema.json`
|
||||
3. `./scripts/update_api_client.sh` (or `flutter pub run build_runner build --delete-conflicting-outputs`)
|
||||
4. Create extension in `lib/api/endpoints/`:
|
||||
|
||||
```dart
|
||||
import '../client.dart';
|
||||
import '../types.dart';
|
||||
|
||||
extension UserEndpoint on CqrsApiClient {
|
||||
Future<Result<UserDto>> getUser(String userId) async {
|
||||
return executeQuery<UserDto>(
|
||||
endpoint: 'users/$userId',
|
||||
query: GetUserQuery(userId: userId),
|
||||
fromJson: UserDto.fromJson,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Define query (matches backend contract)
|
||||
class GetUserQuery implements Serializable {
|
||||
final String userId;
|
||||
const GetUserQuery({required this.userId});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {'userId': userId};
|
||||
}
|
||||
|
||||
// Define DTO (from OpenAPI schema)
|
||||
class UserDto {
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
|
||||
const UserDto({required this.id, required this.name, required this.email});
|
||||
|
||||
factory UserDto.fromJson(Map<String, Object?> json) => UserDto(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
email: json['email'] as String,
|
||||
extension AgentEndpoint on CqrsApiClient {
|
||||
Future<Result<AgentDto>> getAgent(String id) => executeQuery<AgentDto>(
|
||||
endpoint: 'agents/$id',
|
||||
query: GetAgentQuery(id: id),
|
||||
fromJson: AgentDto.fromJson,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Export from api.dart
|
||||
Add to `lib/api/api.dart`:
|
||||
```dart
|
||||
export 'endpoints/user_endpoint.dart';
|
||||
```
|
||||
|
||||
---
|
||||
5. Export from `lib/api/api.dart`
|
||||
|
||||
## Configuration
|
||||
|
||||
### API Client Setup
|
||||
|
||||
**Development (localhost):**
|
||||
```dart
|
||||
// Development
|
||||
final client = CqrsApiClient(
|
||||
config: ApiClientConfig.development, // http://localhost:5246
|
||||
);
|
||||
```
|
||||
|
||||
**Android Emulator:**
|
||||
```dart
|
||||
final client = CqrsApiClient(
|
||||
config: ApiClientConfig(
|
||||
baseUrl: 'http://10.0.2.2:5246', // Special emulator IP
|
||||
timeout: Duration(seconds: 30),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
**Production:**
|
||||
```dart
|
||||
// Production
|
||||
final client = CqrsApiClient(
|
||||
config: ApiClientConfig(
|
||||
baseUrl: 'https://api.svrnty.com',
|
||||
timeout: Duration(seconds: 30),
|
||||
defaultHeaders: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
},
|
||||
defaultHeaders: {'Authorization': 'Bearer $token'},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Always call `client.dispose()` when done.
|
||||
## Commands
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
- `ApiErrorType.network` - No internet/DNS failure
|
||||
- `ApiErrorType.http` - 4xx/5xx responses
|
||||
- `ApiErrorType.serialization` - JSON parsing failed
|
||||
- `ApiErrorType.timeout` - Request took too long
|
||||
- `ApiErrorType.validation` - Backend validation error (422)
|
||||
- `ApiErrorType.unknown` - Unexpected errors
|
||||
|
||||
### Handling Patterns
|
||||
```dart
|
||||
result.when(
|
||||
success: (data) { /* handle success */ },
|
||||
error: (error) {
|
||||
switch (error.type) {
|
||||
case ApiErrorType.network:
|
||||
showSnackbar('No internet connection');
|
||||
case ApiErrorType.timeout:
|
||||
showSnackbar('Request timed out');
|
||||
case ApiErrorType.validation:
|
||||
showValidationErrors(error.details);
|
||||
case ApiErrorType.http:
|
||||
if (error.statusCode == 401) navigateToLogin();
|
||||
else showSnackbar('Server error: ${error.message}');
|
||||
default:
|
||||
showSnackbar('Unexpected error: ${error.message}');
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Workflows
|
||||
|
||||
### When Backend Changes API Contract
|
||||
|
||||
1. Backend team notifies: "Updated API - added X endpoint"
|
||||
2. Check backend CHANGELOG: `cat ../backend/docs/CHANGELOG.md`
|
||||
3. Update contract: `cp ../backend/docs/openapi.json ./api-schema.json`
|
||||
4. Regenerate: `./scripts/update_api_client.sh`
|
||||
5. Add endpoint extension if needed (see "Adding New API Endpoints")
|
||||
6. Run tests: `flutter test`
|
||||
7. Verify types: `./scripts/verify_api_types.sh`
|
||||
8. Commit: `git commit -m "feat: Add X endpoint integration"`
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Code generation fails:**
|
||||
```bash
|
||||
flutter clean
|
||||
# Development
|
||||
flutter pub get
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
flutter run -d chrome # Web (primary)
|
||||
flutter run -d macos
|
||||
|
||||
# Testing
|
||||
flutter test --coverage
|
||||
flutter analyze
|
||||
./scripts/verify_api_types.sh
|
||||
|
||||
# API Updates
|
||||
cp ../BACKEND/docs/openapi.json ./api-schema.json
|
||||
./scripts/update_api_client.sh
|
||||
|
||||
# Troubleshooting
|
||||
flutter clean && flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Backend
|
||||
docker-compose up # PostgreSQL + Ollama
|
||||
```
|
||||
|
||||
**Type errors after regenerating:**
|
||||
Backend made breaking changes. Check `../backend/docs/CHANGELOG.md` and update code accordingly.
|
||||
## Current Issues
|
||||
- Memory leak in AgentsPage (use `late final`)
|
||||
- Need input validation
|
||||
- Missing state persistence
|
||||
|
||||
**Network error on device:**
|
||||
- iOS/Real device: Use actual IP (e.g., `http://192.168.1.100:5246`)
|
||||
- Android emulator: Use `http://10.0.2.2:5246`
|
||||
## MVP Success Criteria
|
||||
User can: Create agent Test with prompt View execution See results/metrics
|
||||
|
||||
**JSON parsing error:**
|
||||
1. Verify `api-schema.json` matches backend's `docs/openapi.json`
|
||||
2. Check DTO `fromJson` matches OpenAPI schema
|
||||
3. Verify backend returned correct content-type
|
||||
|
||||
---
|
||||
|
||||
## Additional Documentation
|
||||
|
||||
- **API Integration Guide:** `README_API.md` (comprehensive API documentation)
|
||||
- **Strict Typing Rules:** `.claude-docs/strict-typing.md` (mandatory)
|
||||
- **Response Protocol:** `.claude-docs/response-protocol.md` (mandatory)
|
||||
- **Backend Docs:** `../backend/docs/` (architecture, changelog)
|
||||
- **OpenAPI Contract:** `api-schema.json` (source of truth)
|
||||
|
||||
---
|
||||
|
||||
## Key Reminders
|
||||
|
||||
1. **OpenAPI is Source of Truth** - Always regenerate from `api-schema.json`
|
||||
2. **CQRS Pattern** - All endpoints use POST with JSON body (even empty `{}`)
|
||||
3. **Type Safety** - No `dynamic` types, use `Serializable` interface
|
||||
4. **Functional Errors** - Use `Result<T>`, not try-catch
|
||||
5. **Monitor Backend CHANGELOG** - Breaking changes documented there
|
||||
6. **Test Everything** - Unit tests + integration tests
|
||||
7. **Follow Response Protocol** - All responses end with structured choices
|
||||
8. **Strict Typing** - Explicit types everywhere, no exceptions
|
||||
## References
|
||||
- API docs: `README_API.md`
|
||||
- Strict typing: `.claude-docs/strict-typing.md`
|
||||
- Backend: `../BACKEND/docs/`
|
||||
- Contract: `api-schema.json` (source of truth)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Agent API Integration - Complete Guide
|
||||
|
||||
**Status:** ✅ **READY FOR USE** (Phase 1 Complete)
|
||||
**Status:** **READY FOR USE** (Phase 1 Complete)
|
||||
**Last Updated:** 2025-10-26
|
||||
**Backend Version:** v1.0.0-mvp
|
||||
|
||||
@ -363,11 +363,11 @@ healthResult.when(
|
||||
|
||||
### Strict Typing
|
||||
All code follows **strict typing rules** from `CLAUDE.md`:
|
||||
- ✅ Every variable has explicit type annotation
|
||||
- ✅ Every function parameter is typed
|
||||
- ✅ Every function return value is typed
|
||||
- ❌ No `dynamic` types
|
||||
- ❌ No untyped `var` declarations
|
||||
- Every variable has explicit type annotation
|
||||
- Every function parameter is typed
|
||||
- Every function return value is typed
|
||||
- No `dynamic` types
|
||||
- No untyped `var` declarations
|
||||
|
||||
### CQRS Pattern
|
||||
All endpoints follow CQRS:
|
||||
@ -376,9 +376,9 @@ All endpoints follow CQRS:
|
||||
- All requests use **POST** with JSON body (even empty `{}`)
|
||||
|
||||
### Functional Error Handling
|
||||
- ✅ Use `Result<T>` pattern matching
|
||||
- ✅ Use `when()` or switch expressions
|
||||
- ❌ Never use try-catch for API calls
|
||||
- Use `Result<T>` pattern matching
|
||||
- Use `when()` or switch expressions
|
||||
- Never use try-catch for API calls
|
||||
|
||||
### Security
|
||||
- API keys are **encrypted** by backend (AES-256)
|
||||
@ -427,5 +427,5 @@ flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ **Production-ready for Agent CRUD operations**
|
||||
**Status:** **Production-ready for Agent CRUD operations**
|
||||
**Last Tested:** 2025-10-26 with backend v1.0.0-mvp
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Complete API Integration Guide - Codex ADK
|
||||
|
||||
**Status:** ✅ **PRODUCTION-READY** (All MVP Endpoints Implemented)
|
||||
**Status:** **PRODUCTION-READY** (All MVP Endpoints Implemented)
|
||||
**Last Updated:** 2025-10-26
|
||||
**Backend Version:** v1.0.0-mvp
|
||||
|
||||
@ -293,7 +293,7 @@ int factorial(int n) {
|
||||
success: (AgentExecutionDto execution) {
|
||||
print(' Status: ${execution.status.value}');
|
||||
print(' Response: ${execution.response}');
|
||||
print(' Tokens: ${execution.inputTokens} → ${execution.outputTokens}');
|
||||
print(' Tokens: ${execution.inputTokens} ${execution.outputTokens}');
|
||||
},
|
||||
error: (ApiErrorInfo error) => print(' Failed: ${error.message}'),
|
||||
);
|
||||
@ -315,7 +315,7 @@ int factorial(int n) {
|
||||
);
|
||||
}
|
||||
|
||||
print('\n✓ Workflow complete!');
|
||||
print('\n Workflow complete!');
|
||||
} finally {
|
||||
client.dispose();
|
||||
}
|
||||
@ -441,7 +441,7 @@ final CqrsApiClient client = CqrsApiClient(
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ✅ Implemented (Phase 1 & 2)
|
||||
### Implemented (Phase 1 & 2)
|
||||
- [x] Agent CRUD (create, get, update, delete)
|
||||
- [x] Conversation creation and retrieval
|
||||
- [x] Execution start, complete, and retrieval
|
||||
@ -550,6 +550,6 @@ cp ../BACKEND/docs/openapi.json ./api-schema.json
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ **All Core Endpoints Implemented and Production-Ready**
|
||||
**Status:** **All Core Endpoints Implemented and Production-Ready**
|
||||
**Last Updated:** 2025-10-26
|
||||
**Backend Version:** v1.0.0-mvp
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# API Integration Status Report
|
||||
|
||||
**Date:** 2025-10-26
|
||||
**Status:** ✅ **COMPLETE - PRODUCTION READY**
|
||||
**Status:** **COMPLETE - PRODUCTION READY**
|
||||
**Backend Version:** v1.0.0-mvp
|
||||
|
||||
---
|
||||
@ -14,15 +14,15 @@ Successfully integrated **all 10 core CQRS endpoints** from the Codex backend in
|
||||
|
||||
## Implementation Breakdown
|
||||
|
||||
### Phase 1: Agent Management ✅
|
||||
### Phase 1: Agent Management
|
||||
**Completed:** 2025-10-26
|
||||
|
||||
| Endpoint | Type | Status | Implementation |
|
||||
|----------|------|--------|----------------|
|
||||
| Create Agent | Command | ✅ | `agent_endpoint.dart:331` |
|
||||
| Update Agent | Command | ✅ | `agent_endpoint.dart:353` |
|
||||
| Delete Agent | Command | ✅ | `agent_endpoint.dart:370` |
|
||||
| Get Agent | Query | ✅ | `agent_endpoint.dart:391` |
|
||||
| Create Agent | Command | | `agent_endpoint.dart:331` |
|
||||
| Update Agent | Command | | `agent_endpoint.dart:353` |
|
||||
| Delete Agent | Command | | `agent_endpoint.dart:370` |
|
||||
| Get Agent | Query | | `agent_endpoint.dart:391` |
|
||||
|
||||
**Files Created:**
|
||||
- `lib/api/endpoints/agent_endpoint.dart` (400+ lines)
|
||||
@ -37,15 +37,15 @@ Successfully integrated **all 10 core CQRS endpoints** from the Codex backend in
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Conversations & Executions ✅
|
||||
### Phase 2: Conversations & Executions
|
||||
**Completed:** 2025-10-26
|
||||
|
||||
#### Conversations
|
||||
|
||||
| Endpoint | Type | Status | Implementation |
|
||||
|----------|------|--------|----------------|
|
||||
| Create Conversation | Command | ✅ | `conversation_endpoint.dart:226` |
|
||||
| Get Conversation | Query | ✅ | `conversation_endpoint.dart:311` |
|
||||
| Create Conversation | Command | | `conversation_endpoint.dart:226` |
|
||||
| Get Conversation | Query | | `conversation_endpoint.dart:311` |
|
||||
|
||||
**DTOs:**
|
||||
- `CreateConversationResult` - Returns new conversation ID
|
||||
@ -57,9 +57,9 @@ Successfully integrated **all 10 core CQRS endpoints** from the Codex backend in
|
||||
|
||||
| Endpoint | Type | Status | Implementation |
|
||||
|----------|------|--------|----------------|
|
||||
| Start Execution | Command | ✅ | `execution_endpoint.dart:353` |
|
||||
| Complete Execution | Command | ✅ | `execution_endpoint.dart:425` |
|
||||
| Get Execution | Query | ✅ | `execution_endpoint.dart:448` |
|
||||
| Start Execution | Command | | `execution_endpoint.dart:353` |
|
||||
| Complete Execution | Command | | `execution_endpoint.dart:425` |
|
||||
| Get Execution | Query | | `execution_endpoint.dart:448` |
|
||||
|
||||
**Features:**
|
||||
- 1 enum (ExecutionStatus with 5 states)
|
||||
@ -94,21 +94,21 @@ These are simple GET endpoints that return arrays:
|
||||
### Files Created
|
||||
```
|
||||
lib/api/
|
||||
├── api.dart (132 lines) ✅ Updated exports
|
||||
├── client.dart (402 lines) ✅ Existing
|
||||
├── types.dart (250+ lines) ✅ Existing
|
||||
├── api.dart (132 lines) Updated exports
|
||||
├── client.dart (402 lines) Existing
|
||||
├── types.dart (250+ lines) Existing
|
||||
├── endpoints/
|
||||
│ ├── health_endpoint.dart (50 lines) ✅ Existing
|
||||
│ ├── agent_endpoint.dart (418 lines) ✅ NEW
|
||||
│ ├── conversation_endpoint.dart (320 lines) ✅ NEW
|
||||
│ └── execution_endpoint.dart (470 lines) ✅ NEW
|
||||
│ ├── health_endpoint.dart (50 lines) Existing
|
||||
│ ├── agent_endpoint.dart (418 lines) NEW
|
||||
│ ├── conversation_endpoint.dart (320 lines) NEW
|
||||
│ └── execution_endpoint.dart (470 lines) NEW
|
||||
└── examples/
|
||||
└── agent_example.dart (150 lines) ✅ NEW
|
||||
└── agent_example.dart (150 lines) NEW
|
||||
|
||||
docs/
|
||||
├── AGENT_API_INTEGRATION.md (450 lines) ✅ NEW
|
||||
├── COMPLETE_API_INTEGRATION.md (650 lines) ✅ NEW
|
||||
└── INTEGRATION_STATUS.md (This file) ✅ NEW
|
||||
├── AGENT_API_INTEGRATION.md (450 lines) NEW
|
||||
├── COMPLETE_API_INTEGRATION.md (650 lines) NEW
|
||||
└── INTEGRATION_STATUS.md (This file) NEW
|
||||
|
||||
Total: ~3,300 lines of production-ready code
|
||||
```
|
||||
@ -120,33 +120,33 @@ Total: ~3,300 lines of production-ready code
|
||||
- **100%** functional error handling (no try-catch on API calls)
|
||||
|
||||
### Test Coverage
|
||||
- ✅ Flutter analyze: 0 issues
|
||||
- ✅ All enums properly typed
|
||||
- ✅ All DTOs have fromJson/toJson
|
||||
- ✅ All commands implement Serializable
|
||||
- Flutter analyze: 0 issues
|
||||
- All enums properly typed
|
||||
- All DTOs have fromJson/toJson
|
||||
- All commands implement Serializable
|
||||
|
||||
---
|
||||
|
||||
## Architecture Compliance
|
||||
|
||||
### ✅ CQRS Pattern
|
||||
- All commands use `executeCommand()` → `Result<void>`
|
||||
- All queries use `executeQuery<T>()` → `Result<T>`
|
||||
### CQRS Pattern
|
||||
- All commands use `executeCommand()` `Result<void>`
|
||||
- All queries use `executeQuery<T>()` `Result<T>`
|
||||
- Special commands that return data handled correctly (create operations)
|
||||
- All endpoints use POST with JSON body
|
||||
|
||||
### ✅ Strict Typing (CLAUDE.md)
|
||||
### Strict Typing (CLAUDE.md)
|
||||
- Every variable: explicit type
|
||||
- Every function parameter: typed
|
||||
- Every return value: typed
|
||||
- No `dynamic` or untyped `var`
|
||||
|
||||
### ✅ Functional Error Handling
|
||||
### Functional Error Handling
|
||||
- All operations return `Result<T>`
|
||||
- Pattern matching with `when()` or switch
|
||||
- Comprehensive error types (network, timeout, HTTP, validation, etc.)
|
||||
|
||||
### ✅ OpenAPI Contract
|
||||
### OpenAPI Contract
|
||||
- Schema updated from backend: `api-schema.json`
|
||||
- DTOs match OpenAPI specs exactly
|
||||
- Enums use string values as per backend
|
||||
@ -155,18 +155,18 @@ Total: ~3,300 lines of production-ready code
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Static Analysis ✅
|
||||
### Static Analysis
|
||||
- [x] `flutter analyze` - 0 issues
|
||||
- [x] All imports resolve
|
||||
- [x] No linting errors
|
||||
|
||||
### Type Safety ✅
|
||||
### Type Safety
|
||||
- [x] No `dynamic` types
|
||||
- [x] All enums properly defined
|
||||
- [x] All DTOs have proper constructors
|
||||
- [x] All Serializable implementations correct
|
||||
|
||||
### Documentation ✅
|
||||
### Documentation
|
||||
- [x] Inline documentation on all public APIs
|
||||
- [x] Complete integration guides
|
||||
- [x] Usage examples for all endpoints
|
||||
@ -269,16 +269,16 @@ flutter analyze # Verify types still match
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### ❌ Not Implemented
|
||||
### Not Implemented
|
||||
1. **JWT Authentication** - Backend ready, frontend needs token management
|
||||
2. **List Endpoints** - Simple GET arrays, not critical for MVP
|
||||
3. **Real-time Updates** - WebSocket/SignalR (planned for v2)
|
||||
4. **Pagination** - Backend limits to 100 items, sufficient for MVP
|
||||
|
||||
### ⚠️ Important Notes
|
||||
### Important Notes
|
||||
1. **API Keys Encrypted** - Backend encrypts cloud provider keys (AES-256)
|
||||
2. **Soft Deletes** - Delete operations don't remove from DB
|
||||
3. **Execution Workflow** - Manual flow (start → process → complete), no automatic agent execution yet
|
||||
3. **Execution Workflow** - Manual flow (start process complete), no automatic agent execution yet
|
||||
4. **Conversation Messages** - Created by execution completion, not manually
|
||||
|
||||
---
|
||||
@ -286,9 +286,9 @@ flutter analyze # Verify types still match
|
||||
## Next Steps for Team
|
||||
|
||||
### Immediate (Sprint 1)
|
||||
1. ✅ **API Integration** - COMPLETE
|
||||
2. 🔄 **UI Components** - Start building Agent management screens
|
||||
3. 🔄 **State Management** - Integrate Provider/Riverpod
|
||||
1. **API Integration** - COMPLETE
|
||||
2. **UI Components** - Start building Agent management screens
|
||||
3. **State Management** - Integrate Provider/Riverpod
|
||||
4. ⏳ **Manual Testing** - Test all endpoints with running backend
|
||||
|
||||
### Future (Sprint 2+)
|
||||
@ -320,18 +320,18 @@ flutter analyze # Verify types still match
|
||||
|
||||
| Metric | Target | Actual | Status |
|
||||
|--------|--------|--------|--------|
|
||||
| Endpoints Implemented | 10 | 10 | ✅ 100% |
|
||||
| Type Safety | 100% | 100% | ✅ |
|
||||
| Flutter Analyze | 0 issues | 0 issues | ✅ |
|
||||
| Documentation | Complete | 1,500+ lines | ✅ |
|
||||
| Examples | All endpoints | All endpoints | ✅ |
|
||||
| CQRS Compliance | 100% | 100% | ✅ |
|
||||
| Endpoints Implemented | 10 | 10 | 100% |
|
||||
| Type Safety | 100% | 100% | |
|
||||
| Flutter Analyze | 0 issues | 0 issues | |
|
||||
| Documentation | Complete | 1,500+ lines | |
|
||||
| Examples | All endpoints | All endpoints | |
|
||||
| CQRS Compliance | 100% | 100% | |
|
||||
|
||||
---
|
||||
|
||||
**Conclusion:** API integration is **PRODUCTION-READY**. UI development can proceed immediately with full confidence in type safety and error handling.
|
||||
|
||||
**Team Status:** ✅ **READY TO BUILD UI**
|
||||
**Team Status:** **READY TO BUILD UI**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
# Testing Guide - Svrnty Console
|
||||
|
||||
**Date:** 2025-10-26
|
||||
**Status:** ✅ **App Running Successfully**
|
||||
**Status:** **App Running Successfully**
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### ✅ App is Currently Running!
|
||||
### App is Currently Running!
|
||||
|
||||
**URL:** `http://localhost:8080`
|
||||
**DevTools:** `http://127.0.0.1:9101`
|
||||
@ -18,7 +18,7 @@ The app has been launched in Chrome and is ready for testing.
|
||||
|
||||
## Test Checklist
|
||||
|
||||
### 1. Navigation & UI ✅
|
||||
### 1. Navigation & UI
|
||||
|
||||
#### Dashboard
|
||||
- [ ] App loads with Dashboard visible
|
||||
@ -28,12 +28,12 @@ The app has been launched in Chrome and is ready for testing.
|
||||
- [ ] All UI animations smooth
|
||||
|
||||
#### Sidebar Navigation
|
||||
- [ ] Click "Dashboard" → shows dashboard
|
||||
- [ ] Click "The Architech" → changes page
|
||||
- [ ] Click "Agents" → **shows Agents page**
|
||||
- [ ] Click "Analytics" → placeholder
|
||||
- [ ] Click "Tools" → placeholder
|
||||
- [ ] Click "Settings" → placeholder
|
||||
- [ ] Click "Dashboard" shows dashboard
|
||||
- [ ] Click "The Architech" changes page
|
||||
- [ ] Click "Agents" **shows Agents page**
|
||||
- [ ] Click "Analytics" placeholder
|
||||
- [ ] Click "Tools" placeholder
|
||||
- [ ] Click "Settings" placeholder
|
||||
- [ ] Active page highlighted in sidebar
|
||||
- [ ] Icons display correctly
|
||||
|
||||
@ -49,10 +49,10 @@ The app has been launched in Chrome and is ready for testing.
|
||||
- [ ] "No Agents Yet" heading
|
||||
- [ ] "Create your first AI agent to get started" subtitle
|
||||
- [ ] "Create Your First Agent" button
|
||||
- [ ] Click "Create Agent" button → opens Create Agent Dialog
|
||||
- [ ] Click "Create Your First Agent" → opens Create Agent Dialog
|
||||
- [ ] Click "Create Agent" button opens Create Agent Dialog
|
||||
- [ ] Click "Create Your First Agent" opens Create Agent Dialog
|
||||
|
||||
#### Create Agent Dialog ✅ **NOW AVAILABLE**
|
||||
#### Create Agent Dialog **NOW AVAILABLE**
|
||||
- [ ] Dialog opens with "Create New Agent" header
|
||||
- [ ] CPU icon in header (Crimson Red background)
|
||||
- [ ] Close button works (X icon)
|
||||
@ -80,15 +80,15 @@ The app has been launched in Chrome and is ready for testing.
|
||||
- [ ] Conversation Window Size slider (1-100, default 10, shows ONLY when memory enabled)
|
||||
|
||||
- [ ] Validation works:
|
||||
- [ ] Click "Create Agent" with empty form → shows validation errors
|
||||
- [ ] Fill required fields → validation errors clear
|
||||
- [ ] Invalid max tokens (non-number) → shows error
|
||||
- [ ] Click "Create Agent" with empty form shows validation errors
|
||||
- [ ] Fill required fields validation errors clear
|
||||
- [ ] Invalid max tokens (non-number) shows error
|
||||
|
||||
- [ ] Dynamic UI works:
|
||||
- [ ] Select "LocalEndpoint" → shows Endpoint field, hides API Key
|
||||
- [ ] Select "CloudApi" → shows API Key field, hides Endpoint
|
||||
- [ ] Toggle "Enable Memory" OFF → hides Window Size slider
|
||||
- [ ] Toggle "Enable Memory" ON → shows Window Size slider
|
||||
- [ ] Select "LocalEndpoint" shows Endpoint field, hides API Key
|
||||
- [ ] Select "CloudApi" shows API Key field, hides Endpoint
|
||||
- [ ] Toggle "Enable Memory" OFF hides Window Size slider
|
||||
- [ ] Toggle "Enable Memory" ON shows Window Size slider
|
||||
|
||||
- [ ] Submission works:
|
||||
- [ ] Fill all required fields
|
||||
@ -133,8 +133,8 @@ The app has been launched in Chrome and is ready for testing.
|
||||
### 4. Responsiveness
|
||||
|
||||
#### Window Resize
|
||||
- [ ] Collapse sidebar → content expands
|
||||
- [ ] Expand sidebar → content adjusts
|
||||
- [ ] Collapse sidebar content expands
|
||||
- [ ] Expand sidebar content adjusts
|
||||
- [ ] No layout breaks
|
||||
- [ ] Text doesn't overflow
|
||||
- [ ] Buttons stay accessible
|
||||
@ -168,7 +168,7 @@ final result = await client.checkHealth();
|
||||
// Should return true if backend is running
|
||||
```
|
||||
|
||||
#### 2. Create Agent ✅ **READY TO TEST**
|
||||
#### 2. Create Agent **READY TO TEST**
|
||||
With backend running at http://localhost:5246:
|
||||
1. Click "Create Agent" button
|
||||
2. Fill in form:
|
||||
@ -242,7 +242,7 @@ Test with backend OFF:
|
||||
|
||||
## Known Issues & Expected Behavior
|
||||
|
||||
### ✅ Expected (Not Bugs)
|
||||
### Expected (Not Bugs)
|
||||
|
||||
1. **"animate: true" in console**
|
||||
- These are debug messages from animate_do package
|
||||
@ -260,7 +260,7 @@ Test with backend OFF:
|
||||
- Create agent, edit, delete dialogs
|
||||
- Will be implemented in Phase 2
|
||||
|
||||
### ⚠️ Known Limitations
|
||||
### Known Limitations
|
||||
|
||||
1. **No Agents Display**
|
||||
- Awaiting backend list agents endpoint
|
||||
@ -290,7 +290,7 @@ q # Quit app
|
||||
|
||||
## Testing Different Devices
|
||||
|
||||
### Chrome (Current) ✅
|
||||
### Chrome (Current)
|
||||
```bash
|
||||
# Already running at http://localhost:8080
|
||||
```
|
||||
@ -313,41 +313,41 @@ flutter run -d safari # Safari (if available)
|
||||
Follow this script for comprehensive testing:
|
||||
|
||||
```
|
||||
1. ✅ App launches successfully
|
||||
→ See: Svrnty Console Dashboard
|
||||
1. App launches successfully
|
||||
See: Svrnty Console Dashboard
|
||||
|
||||
2. ✅ Sidebar visible with logo
|
||||
→ See: "S" icon and "Svrnty Console" text
|
||||
2. Sidebar visible with logo
|
||||
See: "S" icon and "Svrnty Console" text
|
||||
|
||||
3. ✅ Click sidebar collapse button
|
||||
→ See: Sidebar shrinks, shows only icons
|
||||
3. Click sidebar collapse button
|
||||
See: Sidebar shrinks, shows only icons
|
||||
|
||||
4. ✅ Click expand button
|
||||
→ See: Sidebar expands, shows full text
|
||||
4. Click expand button
|
||||
See: Sidebar expands, shows full text
|
||||
|
||||
5. ✅ Click "Agents" in sidebar
|
||||
→ See: Agents page with empty state
|
||||
5. Click "Agents" in sidebar
|
||||
See: Agents page with empty state
|
||||
|
||||
6. ✅ Verify empty state displays correctly
|
||||
→ See: CPU icon, "No Agents Yet", CTA button
|
||||
6. Verify empty state displays correctly
|
||||
See: CPU icon, "No Agents Yet", CTA button
|
||||
|
||||
7. ✅ Click "Create Agent" (top right)
|
||||
→ See: SnackBar "Create agent dialog coming soon..."
|
||||
7. Click "Create Agent" (top right)
|
||||
See: SnackBar "Create agent dialog coming soon..."
|
||||
|
||||
8. ✅ Click "Create Your First Agent" (center)
|
||||
→ See: Same SnackBar message
|
||||
8. Click "Create Your First Agent" (center)
|
||||
See: Same SnackBar message
|
||||
|
||||
9. ✅ Click "Dashboard" in sidebar
|
||||
→ See: Returns to dashboard
|
||||
9. Click "Dashboard" in sidebar
|
||||
See: Returns to dashboard
|
||||
|
||||
10. ✅ Click "Agents" again
|
||||
→ See: Agents page still shows correctly
|
||||
10. Click "Agents" again
|
||||
See: Agents page still shows correctly
|
||||
|
||||
11. ✅ Verify animations smooth
|
||||
→ See: Fade-in transitions on page load
|
||||
11. Verify animations smooth
|
||||
See: Fade-in transitions on page load
|
||||
|
||||
12. ✅ Check responsive layout
|
||||
→ See: Content adjusts to window size
|
||||
12. Check responsive layout
|
||||
See: Content adjusts to window size
|
||||
```
|
||||
|
||||
---
|
||||
@ -359,7 +359,7 @@ Follow this script for comprehensive testing:
|
||||
#### Dashboard
|
||||
```
|
||||
╔═══════════════════════════════════════════════════╗
|
||||
║ [≡] Dashboard 🔔 ⚙️ ║
|
||||
║ [≡] Dashboard ║
|
||||
║ sovereign AI solutions ║
|
||||
╠═══════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
@ -433,7 +433,7 @@ Test flow:
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Phase 1 (Current) ✅
|
||||
### Phase 1 (Current)
|
||||
- [x] App runs without errors
|
||||
- [x] Navigation works perfectly
|
||||
- [x] Agents page displays empty state
|
||||
@ -442,7 +442,7 @@ Test flow:
|
||||
- [x] No console errors
|
||||
- [x] Responsive layout works
|
||||
|
||||
### Phase 2 (Current) ✅
|
||||
### Phase 2 (Current)
|
||||
- [x] Create agent dialog functional
|
||||
- [x] Form validation works
|
||||
- [x] API integration successful
|
||||
@ -460,16 +460,16 @@ Test flow:
|
||||
|
||||
## Summary
|
||||
|
||||
**Current Status:** ✅ **FULLY FUNCTIONAL**
|
||||
**Current Status:** **FULLY FUNCTIONAL**
|
||||
|
||||
The app is running successfully with:
|
||||
- ✅ Complete UI rendering
|
||||
- ✅ Smooth navigation
|
||||
- ✅ Professional design
|
||||
- ✅ Ready for backend integration
|
||||
- ✅ No blocking issues
|
||||
- Complete UI rendering
|
||||
- Smooth navigation
|
||||
- Professional design
|
||||
- Ready for backend integration
|
||||
- No blocking issues
|
||||
|
||||
**Test Result:** **PASS** 🎉
|
||||
**Test Result:** **PASS**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# UI Implementation Status
|
||||
|
||||
**Date:** 2025-10-26
|
||||
**Status:** ✅ **Phase 2 Complete - Create Agent Dialog Implemented**
|
||||
**Status:** **Phase 2 Complete - Create Agent Dialog Implemented**
|
||||
|
||||
---
|
||||
|
||||
@ -15,7 +15,7 @@ Created the foundation for the Agents management interface, integrating with the
|
||||
|
||||
### Agents Page (`lib/pages/agents_page.dart`)
|
||||
|
||||
**Status:** ✅ **READY FOR TESTING**
|
||||
**Status:** **READY FOR TESTING**
|
||||
|
||||
#### Features
|
||||
- **Empty State** - Beautiful first-time user experience
|
||||
@ -30,10 +30,10 @@ Created the foundation for the Agents management interface, integrating with the
|
||||
- Action menu button
|
||||
|
||||
#### API Integration
|
||||
- ✅ CqrsApiClient initialized with development config
|
||||
- ✅ Proper dispose() cleanup
|
||||
- ✅ Result<T> pattern matching for API responses
|
||||
- ✅ Error handling with SnackBar notifications
|
||||
- CqrsApiClient initialized with development config
|
||||
- Proper dispose() cleanup
|
||||
- Result<T> pattern matching for API responses
|
||||
- Error handling with SnackBar notifications
|
||||
- ⏳ List agents endpoint (waiting for backend Phase 3)
|
||||
|
||||
#### UI Components Used
|
||||
@ -64,12 +64,12 @@ Created the foundation for the Agents management interface, integrating with the
|
||||
## UI Flow
|
||||
|
||||
```
|
||||
Navigation Sidebar → Click "Agents"
|
||||
↓
|
||||
Navigation Sidebar Click "Agents"
|
||||
|
||||
ConsoleLandingPage (_currentPage = 'agents')
|
||||
↓
|
||||
|
||||
AgentsPage Widget
|
||||
↓
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ Header │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
@ -93,7 +93,7 @@ AgentsPage Widget
|
||||
│ Agent Card 1 │ Agent Card 2 │ Agent Card 3 │
|
||||
│ ┌──────────┐ │ ┌──────────┐ │ ┌──────────┐ │
|
||||
│ │[Icon] Name│ │ │[Icon] Name│ │ │[Icon] Name│ │
|
||||
│ │● Active │ │ │○ Inactive │ │ │✗ Error │ │
|
||||
│ │● Active │ │ │○ Inactive │ │ │ Error │ │
|
||||
│ │Descr... │ │ │Descr... │ │ │Descr... │ │
|
||||
│ │ollama/phi │ │ │gpt-4o │ │ │claude-3.5 │ │
|
||||
│ └──────────┘ │ └──────────┘ │ └──────────┘ │
|
||||
@ -105,24 +105,24 @@ AgentsPage Widget
|
||||
## Design System Compliance
|
||||
|
||||
### Colors (Svrnty Brand)
|
||||
✅ **Primary:** Crimson Red (`#C44D58` / `#F3574E` dark)
|
||||
✅ **Secondary:** Slate Blue (`#475C6C` / `#5A6F7D` dark)
|
||||
✅ **Surface:** Material 3 surface containers
|
||||
✅ **Error:** Material error colors
|
||||
**Primary:** Crimson Red (`#C44D58` / `#F3574E` dark)
|
||||
**Secondary:** Slate Blue (`#475C6C` / `#5A6F7D` dark)
|
||||
**Surface:** Material 3 surface containers
|
||||
**Error:** Material error colors
|
||||
|
||||
### Typography (Montserrat)
|
||||
✅ **Headings:** Montserrat Bold/SemiBold
|
||||
✅ **Body:** Montserrat Regular
|
||||
✅ **Technical:** IBM Plex Mono (used for model names)
|
||||
**Headings:** Montserrat Bold/SemiBold
|
||||
**Body:** Montserrat Regular
|
||||
**Technical:** IBM Plex Mono (used for model names)
|
||||
|
||||
### Spacing & Layout
|
||||
✅ **Padding:** 24px page padding
|
||||
✅ **Card Spacing:** 16px grid gaps
|
||||
✅ **Border Radius:** 12-16px for modern look
|
||||
✅ **Elevation:** 0 (flat design with borders)
|
||||
**Padding:** 24px page padding
|
||||
**Card Spacing:** 16px grid gaps
|
||||
**Border Radius:** 12-16px for modern look
|
||||
**Elevation:** 0 (flat design with borders)
|
||||
|
||||
### Icons
|
||||
✅ **Iconsax** icons used throughout
|
||||
**Iconsax** icons used throughout
|
||||
- `Iconsax.cpu` - Agents
|
||||
- `Iconsax.code` - Code Generator
|
||||
- `Iconsax.search_zoom_in` - Code Reviewer
|
||||
@ -138,9 +138,9 @@ AgentsPage Widget
|
||||
|
||||
| Status | Icon | Color | Description |
|
||||
|--------|------|-------|-------------|
|
||||
| **Active** | ✓ | Green | Agent is running and available |
|
||||
| **Active** | | Green | Agent is running and available |
|
||||
| **Inactive** | ⏸ | Orange | Agent is paused/stopped |
|
||||
| **Error** | ⚠ | Red | Agent encountered an error |
|
||||
| **Error** | | Red | Agent encountered an error |
|
||||
|
||||
---
|
||||
|
||||
@ -149,33 +149,33 @@ AgentsPage Widget
|
||||
| Type | Icon | Use Case |
|
||||
|------|------|----------|
|
||||
| **CodeGenerator** | `</>` | Generates code from prompts |
|
||||
| **CodeReviewer** | 🔍 | Reviews and analyzes code |
|
||||
| **Debugger** | 🛡️ | Debugs and fixes code |
|
||||
| **Documenter** | 📄 | Creates documentation |
|
||||
| **Custom** | ⚙️ | Custom agent types |
|
||||
| **CodeReviewer** | | Reviews and analyzes code |
|
||||
| **Debugger** | | Debugs and fixes code |
|
||||
| **Documenter** | | Creates documentation |
|
||||
| **Custom** | | Custom agent types |
|
||||
|
||||
---
|
||||
|
||||
## Completed Features (Phase 2)
|
||||
|
||||
### Create Agent Dialog ✅ **COMPLETE**
|
||||
### Create Agent Dialog **COMPLETE**
|
||||
**File:** `lib/dialogs/create_agent_dialog.dart` (575 lines)
|
||||
|
||||
**Features Implemented:**
|
||||
- ✅ Complete form with 13 fields organized in 4 sections
|
||||
- ✅ Basic Information: name, description, agent type
|
||||
- ✅ Model Configuration: provider type, provider, model name
|
||||
- ✅ Dynamic fields: endpoint (local) OR API key (cloud)
|
||||
- ✅ Generation Parameters: temperature slider (0.0-2.0), max tokens, system prompt
|
||||
- ✅ Memory Settings: enable toggle, window size slider (1-100)
|
||||
- ✅ Full validation on all required fields
|
||||
- ✅ Error messages for empty/invalid inputs
|
||||
- ✅ Loading state during API call ("Creating..." with spinner)
|
||||
- ✅ Success/error feedback via SnackBar
|
||||
- ✅ Material 3 design with Svrnty branding
|
||||
- ✅ Proper controller disposal (no memory leaks)
|
||||
- ✅ Responsive layout with scrolling
|
||||
- ✅ Modal dialog with backdrop dismiss
|
||||
- Complete form with 13 fields organized in 4 sections
|
||||
- Basic Information: name, description, agent type
|
||||
- Model Configuration: provider type, provider, model name
|
||||
- Dynamic fields: endpoint (local) OR API key (cloud)
|
||||
- Generation Parameters: temperature slider (0.0-2.0), max tokens, system prompt
|
||||
- Memory Settings: enable toggle, window size slider (1-100)
|
||||
- Full validation on all required fields
|
||||
- Error messages for empty/invalid inputs
|
||||
- Loading state during API call ("Creating..." with spinner)
|
||||
- Success/error feedback via SnackBar
|
||||
- Material 3 design with Svrnty branding
|
||||
- Proper controller disposal (no memory leaks)
|
||||
- Responsive layout with scrolling
|
||||
- Modal dialog with backdrop dismiss
|
||||
|
||||
**UI Components:**
|
||||
- Section headers with icons
|
||||
@ -230,7 +230,7 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
4. **Test Empty State:**
|
||||
- Verify empty state icon displays
|
||||
- Verify "Create Your First Agent" button shows
|
||||
- Click button → should show "coming soon" snackbar
|
||||
- Click button should show "coming soon" snackbar
|
||||
|
||||
5. **Test Navigation:**
|
||||
- Click other sidebar items
|
||||
@ -243,10 +243,10 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
// Future test scenarios
|
||||
- Load agents list
|
||||
- Display agent cards
|
||||
- Click agent card → show details
|
||||
- Click menu → show options
|
||||
- Create agent → refresh list
|
||||
- Delete agent → remove from list
|
||||
- Click agent card show details
|
||||
- Click menu show options
|
||||
- Create agent refresh list
|
||||
- Delete agent remove from list
|
||||
```
|
||||
|
||||
---
|
||||
@ -255,11 +255,11 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
|
||||
| Operation | Backend Ready | Frontend Ready | Status |
|
||||
|-----------|---------------|----------------|--------|
|
||||
| Create Agent | ✅ | ✅ | Ready to integrate |
|
||||
| Get Agent | ✅ | ✅ | Ready to integrate |
|
||||
| Update Agent | ✅ | ⏳ | UI pending |
|
||||
| Delete Agent | ✅ | ⏳ | UI pending |
|
||||
| List Agents | ⏳ | ✅ | Awaiting backend |
|
||||
| Create Agent | | | Ready to integrate |
|
||||
| Get Agent | | | Ready to integrate |
|
||||
| Update Agent | | ⏳ | UI pending |
|
||||
| Delete Agent | | ⏳ | UI pending |
|
||||
| List Agents | ⏳ | | Awaiting backend |
|
||||
|
||||
**Note:** Backend Phase 3 (list endpoints) will enable full agent grid display.
|
||||
|
||||
@ -267,22 +267,22 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
|
||||
## Code Quality
|
||||
|
||||
### Type Safety ✅
|
||||
### Type Safety
|
||||
- All variables explicitly typed
|
||||
- No `dynamic` or `var` without types
|
||||
- Proper enum usage
|
||||
|
||||
### State Management ✅
|
||||
### State Management
|
||||
- StatefulWidget for page state
|
||||
- Proper `dispose()` for API client
|
||||
- `setState()` for UI updates
|
||||
|
||||
### Error Handling ✅
|
||||
### Error Handling
|
||||
- Result<T> pattern matching
|
||||
- User-friendly error messages
|
||||
- Retry functionality
|
||||
|
||||
### Performance ✅
|
||||
### Performance
|
||||
- Efficient rebuild scoping
|
||||
- Lazy loading ready (future)
|
||||
- Smooth animations (300-600ms)
|
||||
@ -322,27 +322,27 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
## Dependencies
|
||||
|
||||
### Required Packages (Already Installed)
|
||||
✅ `flutter` - Framework
|
||||
✅ `animate_do` - Animations
|
||||
✅ `iconsax` - Icons
|
||||
✅ `getwidget` - UI components
|
||||
✅ `http` - API client (via our CQRS client)
|
||||
`flutter` - Framework
|
||||
`animate_do` - Animations
|
||||
`iconsax` - Icons
|
||||
`getwidget` - UI components
|
||||
`http` - API client (via our CQRS client)
|
||||
|
||||
### API Dependencies
|
||||
✅ `lib/api/api.dart` - All endpoint integrations
|
||||
✅ `lib/api/client.dart` - CQRS client
|
||||
✅ `lib/api/types.dart` - Result<T> and errors
|
||||
`lib/api/api.dart` - All endpoint integrations
|
||||
`lib/api/client.dart` - CQRS client
|
||||
`lib/api/types.dart` - Result<T> and errors
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
### 1. List Agents Not Available ⚠️
|
||||
### 1. List Agents Not Available
|
||||
**Issue:** Backend doesn't have list agents endpoint yet (Phase 3)
|
||||
**Workaround:** Showing empty state, ready for integration
|
||||
**ETA:** Awaiting backend Phase 3
|
||||
|
||||
### 2. Minor RenderFlex Overflow ℹ️
|
||||
### 2. Minor RenderFlex Overflow ℹ
|
||||
**Issue:** Sidebar has 15px overflow when collapsed
|
||||
**Location:** `lib/components/navigation_sidebar.dart:217`
|
||||
**Impact:** Cosmetic only, no functional issues
|
||||
@ -378,8 +378,8 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
║ Manage your AI agents and their configurations ║
|
||||
║ ║
|
||||
║ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ ║
|
||||
║ │ [</>] Codegen│ │ [🔍] Reviewer │ │ [🛡️] Debugger│ ║
|
||||
║ │ ● Active │ │ ○ Inactive │ │ ⚠ Error │ ║
|
||||
║ │ [</>] Codegen│ │ [] Reviewer │ │ [] Debugger│ ║
|
||||
║ │ ● Active │ │ ○ Inactive │ │ Error │ ║
|
||||
║ │ Generates... │ │ Reviews code │ │ Debugs and...│ ║
|
||||
║ │ ollama/phi │ │ openai/gpt-4 │ │ claude-3.5 │ ║
|
||||
║ └──────────────┘ └──────────────┘ └─────────────┘ ║
|
||||
@ -390,19 +390,19 @@ flutter run -d macos # or chrome, ios, etc.
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Phase 2 Status:** ✅ **COMPLETE**
|
||||
**Phase 2 Status:** **COMPLETE**
|
||||
|
||||
The Agents page and Create Agent Dialog are fully implemented and ready for integration testing with the backend. The UI follows Svrnty design guidelines, provides comprehensive form validation, and integrates seamlessly with the CQRS API.
|
||||
|
||||
**What's Working:**
|
||||
- ✅ Complete Agent CRUD API integration (all 4 operations)
|
||||
- ✅ Professional Agents page with empty/loading/error states
|
||||
- ✅ Fully functional Create Agent Dialog with 13 fields
|
||||
- ✅ Form validation and error handling
|
||||
- ✅ Dynamic UI based on provider type selection
|
||||
- ✅ Loading states and user feedback
|
||||
- ✅ Material 3 design with Svrnty branding
|
||||
- ✅ 0 Flutter analyze errors
|
||||
- Complete Agent CRUD API integration (all 4 operations)
|
||||
- Professional Agents page with empty/loading/error states
|
||||
- Fully functional Create Agent Dialog with 13 fields
|
||||
- Form validation and error handling
|
||||
- Dynamic UI based on provider type selection
|
||||
- Loading states and user feedback
|
||||
- Material 3 design with Svrnty branding
|
||||
- 0 Flutter analyze errors
|
||||
|
||||
**Ready For:**
|
||||
- Backend integration testing (create agents)
|
||||
|
||||
@ -108,13 +108,17 @@ export 'endpoints/conversation_endpoint.dart'
|
||||
ConversationEndpoint,
|
||||
// Commands
|
||||
CreateConversationCommand,
|
||||
SendMessageCommand,
|
||||
// Queries
|
||||
GetConversationQuery,
|
||||
// DTOs
|
||||
CreateConversationResult,
|
||||
ConversationDto,
|
||||
ConversationListItemDto,
|
||||
ConversationMessageDto;
|
||||
ConversationMessageDto,
|
||||
SendMessageResult,
|
||||
UserMessageDto,
|
||||
AgentResponseDto;
|
||||
export 'endpoints/execution_endpoint.dart'
|
||||
show
|
||||
ExecutionEndpoint,
|
||||
|
||||
@ -243,11 +243,11 @@ class AgentDto {
|
||||
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 double? temperature;
|
||||
final int? maxTokens;
|
||||
final String? systemPrompt;
|
||||
final bool? enableMemory;
|
||||
final int? conversationWindowSize;
|
||||
final AgentStatus status;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
@ -261,11 +261,11 @@ class AgentDto {
|
||||
required this.modelName,
|
||||
required this.providerType,
|
||||
this.modelEndpoint,
|
||||
required this.temperature,
|
||||
required this.maxTokens,
|
||||
required this.systemPrompt,
|
||||
required this.enableMemory,
|
||||
required this.conversationWindowSize,
|
||||
this.temperature,
|
||||
this.maxTokens,
|
||||
this.systemPrompt,
|
||||
this.enableMemory,
|
||||
this.conversationWindowSize,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@ -300,11 +300,11 @@ class AgentDto {
|
||||
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,
|
||||
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),
|
||||
@ -474,4 +474,122 @@ extension AgentEndpoint on CqrsApiClient {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,29 @@ class CreateConversationCommand implements Serializable {
|
||||
};
|
||||
}
|
||||
|
||||
/// Command to send a message to an agent
|
||||
class SendMessageCommand implements Serializable {
|
||||
final String agentId;
|
||||
final String? conversationId;
|
||||
final String message;
|
||||
final String? userId;
|
||||
|
||||
const SendMessageCommand({
|
||||
required this.agentId,
|
||||
this.conversationId,
|
||||
required this.message,
|
||||
this.userId,
|
||||
});
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => {
|
||||
'agentId': agentId,
|
||||
if (conversationId != null) 'conversationId': conversationId,
|
||||
'message': message,
|
||||
if (userId != null) 'userId': userId,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Queries
|
||||
// =============================================================================
|
||||
@ -199,6 +222,101 @@ class ConversationListItemDto {
|
||||
};
|
||||
}
|
||||
|
||||
/// User message details from sendMessage response
|
||||
class UserMessageDto {
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
|
||||
const UserMessageDto({
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory UserMessageDto.fromJson(Map<String, Object?> json) {
|
||||
return UserMessageDto(
|
||||
content: json['content'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'content': content,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Agent response details from sendMessage response
|
||||
class AgentResponseDto {
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
final int? inputTokens;
|
||||
final int? outputTokens;
|
||||
final double? estimatedCost;
|
||||
|
||||
const AgentResponseDto({
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
this.inputTokens,
|
||||
this.outputTokens,
|
||||
this.estimatedCost,
|
||||
});
|
||||
|
||||
factory AgentResponseDto.fromJson(Map<String, Object?> json) {
|
||||
return AgentResponseDto(
|
||||
content: json['content'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
inputTokens: json['inputTokens'] as int?,
|
||||
outputTokens: json['outputTokens'] as int?,
|
||||
estimatedCost: (json['estimatedCost'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'content': content,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
if (inputTokens != null) 'inputTokens': inputTokens,
|
||||
if (outputTokens != null) 'outputTokens': outputTokens,
|
||||
if (estimatedCost != null) 'estimatedCost': estimatedCost,
|
||||
};
|
||||
}
|
||||
|
||||
/// Result of sending a message to an agent
|
||||
class SendMessageResult {
|
||||
final String conversationId;
|
||||
final String messageId;
|
||||
final String agentResponseId;
|
||||
final UserMessageDto userMessage;
|
||||
final AgentResponseDto agentResponse;
|
||||
|
||||
const SendMessageResult({
|
||||
required this.conversationId,
|
||||
required this.messageId,
|
||||
required this.agentResponseId,
|
||||
required this.userMessage,
|
||||
required this.agentResponse,
|
||||
});
|
||||
|
||||
factory SendMessageResult.fromJson(Map<String, Object?> json) {
|
||||
return SendMessageResult(
|
||||
conversationId: json['conversationId'] as String,
|
||||
messageId: json['messageId'] as String,
|
||||
agentResponseId: json['agentResponseId'] as String,
|
||||
userMessage: UserMessageDto.fromJson(
|
||||
json['userMessage'] as Map<String, Object?>),
|
||||
agentResponse: AgentResponseDto.fromJson(
|
||||
json['agentResponse'] as Map<String, Object?>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'conversationId': conversationId,
|
||||
'messageId': messageId,
|
||||
'agentResponseId': agentResponseId,
|
||||
'userMessage': userMessage.toJson(),
|
||||
'agentResponse': agentResponse.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Extension Methods
|
||||
// =============================================================================
|
||||
@ -316,4 +434,187 @@ extension ConversationEndpoint on CqrsApiClient {
|
||||
ConversationDto.fromJson(json as Map<String, Object?>),
|
||||
);
|
||||
}
|
||||
|
||||
/// List all conversations
|
||||
///
|
||||
/// Returns a list of all conversations from the backend.
|
||||
/// Backend endpoint: GET /api/conversations
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final result = await client.listConversations();
|
||||
///
|
||||
/// result.when(
|
||||
/// success: (conversations) {
|
||||
/// print('Found ${conversations.length} conversations');
|
||||
/// for (final conv in conversations) {
|
||||
/// print('${conv.title} - ${conv.messageCount} messages');
|
||||
/// }
|
||||
/// },
|
||||
/// error: (error) => print('Error: ${error.message}'),
|
||||
/// );
|
||||
/// ```
|
||||
Future<Result<List<ConversationListItemDto>>> listConversations() async {
|
||||
try {
|
||||
final Uri url = Uri.parse('${config.baseUrl}/api/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<ConversationListItemDto>>(ApiErrorInfo(
|
||||
message: 'Expected array response, got ${jsonData.runtimeType}',
|
||||
type: ApiErrorType.serialization,
|
||||
));
|
||||
}
|
||||
|
||||
final List<ConversationListItemDto> conversations = jsonData
|
||||
.map((Object? item) =>
|
||||
ConversationListItemDto.fromJson(item as Map<String, Object?>))
|
||||
.toList();
|
||||
|
||||
return ApiSuccess<List<ConversationListItemDto>>(conversations);
|
||||
}
|
||||
|
||||
return ApiError<List<ConversationListItemDto>>(ApiErrorInfo(
|
||||
message: 'Failed to load conversations',
|
||||
type: ApiErrorType.http,
|
||||
statusCode: response.statusCode,
|
||||
));
|
||||
} on TimeoutException {
|
||||
return ApiError<List<ConversationListItemDto>>(ApiErrorInfo(
|
||||
message: 'Request timed out',
|
||||
type: ApiErrorType.timeout,
|
||||
));
|
||||
} on SocketException {
|
||||
return ApiError<List<ConversationListItemDto>>(ApiErrorInfo(
|
||||
message: 'No internet connection',
|
||||
type: ApiErrorType.network,
|
||||
));
|
||||
} catch (e) {
|
||||
return ApiError<List<ConversationListItemDto>>(ApiErrorInfo(
|
||||
message: 'Unexpected error: $e',
|
||||
type: ApiErrorType.unknown,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to an AI agent
|
||||
///
|
||||
/// Sends a user message to the specified agent and receives an AI-generated
|
||||
/// response. If conversationId is null, a new conversation is created.
|
||||
/// If conversationId is provided, the message is added to the existing
|
||||
/// conversation with full context awareness.
|
||||
///
|
||||
/// Backend endpoint: POST /api/command/sendMessage
|
||||
///
|
||||
/// Example (new conversation):
|
||||
/// ```dart
|
||||
/// final result = await client.sendMessage(
|
||||
/// SendMessageCommand(
|
||||
/// agentId: 'agent-uuid',
|
||||
/// conversationId: null, // Creates new conversation
|
||||
/// message: 'Write a hello world function in Python',
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// result.when(
|
||||
/// success: (response) {
|
||||
/// print('Conversation ID: ${response.conversationId}');
|
||||
/// print('User: ${response.userMessage.content}');
|
||||
/// print('Agent: ${response.agentResponse.content}');
|
||||
/// print('Tokens: ${response.agentResponse.inputTokens} in, ${response.agentResponse.outputTokens} out');
|
||||
/// },
|
||||
/// error: (error) => print('Error: ${error.message}'),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// Example (continue conversation):
|
||||
/// ```dart
|
||||
/// final result = await client.sendMessage(
|
||||
/// SendMessageCommand(
|
||||
/// agentId: 'agent-uuid',
|
||||
/// conversationId: 'existing-conversation-uuid',
|
||||
/// message: 'Now make it print in uppercase',
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
Future<Result<SendMessageResult>> sendMessage(
|
||||
SendMessageCommand command,
|
||||
) async {
|
||||
try {
|
||||
final Uri url = Uri.parse('${config.baseUrl}/api/command/sendMessage');
|
||||
final String body = jsonEncode(command.toJson());
|
||||
|
||||
final http.Response response = await http
|
||||
.post(
|
||||
url,
|
||||
headers: config.defaultHeaders,
|
||||
body: body,
|
||||
)
|
||||
.timeout(config.timeout);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
try {
|
||||
final Object? json = jsonDecode(response.body);
|
||||
final SendMessageResult result =
|
||||
SendMessageResult.fromJson(json as Map<String, Object?>);
|
||||
return ApiSuccess<SendMessageResult>(result);
|
||||
} catch (e) {
|
||||
return ApiError<SendMessageResult>(
|
||||
ApiErrorInfo(
|
||||
message: 'Failed to parse send message response',
|
||||
statusCode: response.statusCode,
|
||||
type: ApiErrorType.serialization,
|
||||
details: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
String errorMessage = 'Failed to send message';
|
||||
try {
|
||||
final Object? errorJson = jsonDecode(response.body);
|
||||
if (errorJson is Map<String, Object?>) {
|
||||
errorMessage =
|
||||
errorJson['message'] as String? ?? errorMessage;
|
||||
}
|
||||
} catch (_) {
|
||||
// If parsing fails, use default error message
|
||||
}
|
||||
|
||||
return ApiError<SendMessageResult>(
|
||||
ApiErrorInfo(
|
||||
message: errorMessage,
|
||||
statusCode: response.statusCode,
|
||||
type: ApiErrorType.http,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
return ApiError<SendMessageResult>(
|
||||
ApiErrorInfo(
|
||||
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
|
||||
type: ApiErrorType.timeout,
|
||||
details: 'Agent responses can take 1-5 seconds via Ollama',
|
||||
),
|
||||
);
|
||||
} on SocketException catch (e) {
|
||||
return ApiError<SendMessageResult>(
|
||||
ApiErrorInfo(
|
||||
message: 'Network error: ${e.message}',
|
||||
type: ApiErrorType.network,
|
||||
details: e.osError?.message,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return ApiError<SendMessageResult>(
|
||||
ApiErrorInfo(
|
||||
message: 'Unexpected error: $e',
|
||||
type: ApiErrorType.unknown,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,4 +431,138 @@ extension ExecutionEndpoint on CqrsApiClient {
|
||||
AgentExecutionDto.fromJson(json as Map<String, Object?>),
|
||||
);
|
||||
}
|
||||
|
||||
/// List all executions
|
||||
///
|
||||
/// Returns a list of all executions from the backend.
|
||||
/// Backend endpoint: GET /api/executions
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final result = await client.listExecutions();
|
||||
///
|
||||
/// result.when(
|
||||
/// success: (executions) {
|
||||
/// print('Found ${executions.length} executions');
|
||||
/// for (final exec in executions) {
|
||||
/// print('${exec.agentName}: ${exec.status.value}');
|
||||
/// }
|
||||
/// },
|
||||
/// error: (error) => print('Error: ${error.message}'),
|
||||
/// );
|
||||
/// ```
|
||||
Future<Result<List<ExecutionListItemDto>>> listExecutions() async {
|
||||
try {
|
||||
final Uri url = Uri.parse('${config.baseUrl}/api/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<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Expected array response, got ${jsonData.runtimeType}',
|
||||
type: ApiErrorType.serialization,
|
||||
));
|
||||
}
|
||||
|
||||
final List<ExecutionListItemDto> executions = jsonData
|
||||
.map((Object? item) =>
|
||||
ExecutionListItemDto.fromJson(item as Map<String, Object?>))
|
||||
.toList();
|
||||
|
||||
return ApiSuccess<List<ExecutionListItemDto>>(executions);
|
||||
}
|
||||
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Failed to load executions',
|
||||
type: ApiErrorType.http,
|
||||
statusCode: response.statusCode,
|
||||
));
|
||||
} on TimeoutException {
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Request timed out',
|
||||
type: ApiErrorType.timeout,
|
||||
));
|
||||
} on SocketException {
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'No internet connection',
|
||||
type: ApiErrorType.network,
|
||||
));
|
||||
} catch (e) {
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Unexpected error: $e',
|
||||
type: ApiErrorType.unknown,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// List executions filtered by status
|
||||
///
|
||||
/// Returns executions matching the specified status.
|
||||
/// Backend endpoint: GET /api/executions/status/{status}
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final result = await client.listExecutionsByStatus(ExecutionStatus.running);
|
||||
///
|
||||
/// result.when(
|
||||
/// success: (executions) {
|
||||
/// print('Found ${executions.length} running executions');
|
||||
/// },
|
||||
/// error: (error) => print('Error: ${error.message}'),
|
||||
/// );
|
||||
/// ```
|
||||
Future<Result<List<ExecutionListItemDto>>> listExecutionsByStatus(
|
||||
ExecutionStatus status,
|
||||
) async {
|
||||
try {
|
||||
final String statusValue =
|
||||
ExecutionStatus.values.indexOf(status).toString();
|
||||
final Uri url =
|
||||
Uri.parse('${config.baseUrl}/api/executions/status/$statusValue');
|
||||
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<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Expected array response, got ${jsonData.runtimeType}',
|
||||
type: ApiErrorType.serialization,
|
||||
));
|
||||
}
|
||||
|
||||
final List<ExecutionListItemDto> executions = jsonData
|
||||
.map((Object? item) =>
|
||||
ExecutionListItemDto.fromJson(item as Map<String, Object?>))
|
||||
.toList();
|
||||
|
||||
return ApiSuccess<List<ExecutionListItemDto>>(executions);
|
||||
}
|
||||
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Failed to load executions by status',
|
||||
type: ApiErrorType.http,
|
||||
statusCode: response.statusCode,
|
||||
));
|
||||
} on TimeoutException {
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Request timed out',
|
||||
type: ApiErrorType.timeout,
|
||||
));
|
||||
} on SocketException {
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'No internet connection',
|
||||
type: ApiErrorType.network,
|
||||
));
|
||||
} catch (e) {
|
||||
return ApiError<List<ExecutionListItemDto>>(ApiErrorInfo(
|
||||
message: 'Unexpected error: $e',
|
||||
type: ApiErrorType.unknown,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,9 +34,9 @@ Future<void> agentExample() async {
|
||||
);
|
||||
|
||||
createResult.when(
|
||||
success: (_) => print('✓ Agent created successfully'),
|
||||
success: (_) => print('Agent created successfully'),
|
||||
error: (ApiErrorInfo error) =>
|
||||
print('✗ Failed to create agent: ${error.message}'),
|
||||
print('Failed to create agent: ${error.message}'),
|
||||
);
|
||||
|
||||
// 2. Get agent by ID
|
||||
@ -46,7 +46,7 @@ Future<void> agentExample() async {
|
||||
|
||||
getResult.when(
|
||||
success: (AgentDto agent) {
|
||||
print('✓ Agent found:');
|
||||
print('Agent found:');
|
||||
print(' Name: ${agent.name}');
|
||||
print(' Type: ${agent.type.value}');
|
||||
print(' Status: ${agent.status.value}');
|
||||
@ -54,7 +54,7 @@ Future<void> agentExample() async {
|
||||
print(' Created: ${agent.createdAt}');
|
||||
},
|
||||
error: (ApiErrorInfo error) =>
|
||||
print('✗ Failed to fetch agent: ${error.message}'),
|
||||
print('Failed to fetch agent: ${error.message}'),
|
||||
);
|
||||
|
||||
// 3. Update agent
|
||||
@ -69,9 +69,9 @@ Future<void> agentExample() async {
|
||||
);
|
||||
|
||||
updateResult.when(
|
||||
success: (_) => print('✓ Agent updated successfully'),
|
||||
success: (_) => print('Agent updated successfully'),
|
||||
error: (ApiErrorInfo error) =>
|
||||
print('✗ Failed to update agent: ${error.message}'),
|
||||
print('Failed to update agent: ${error.message}'),
|
||||
);
|
||||
|
||||
// 4. Delete agent
|
||||
@ -81,9 +81,9 @@ Future<void> agentExample() async {
|
||||
);
|
||||
|
||||
deleteResult.when(
|
||||
success: (_) => print('✓ Agent deleted successfully'),
|
||||
success: (_) => print('Agent deleted successfully'),
|
||||
error: (ApiErrorInfo error) =>
|
||||
print('✗ Failed to delete agent: ${error.message}'),
|
||||
print('Failed to delete agent: ${error.message}'),
|
||||
);
|
||||
|
||||
// 5. Pattern matching example with switch expression
|
||||
|
||||
444
FRONTEND/lib/components/agent_chat_window.dart
Normal file
444
FRONTEND/lib/components/agent_chat_window.dart
Normal file
@ -0,0 +1,444 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import '../api/api.dart';
|
||||
import 'message_bubble.dart';
|
||||
|
||||
/// A chat window component for interacting with a single AI agent
|
||||
class AgentChatWindow extends StatefulWidget {
|
||||
final String title;
|
||||
final List<AgentDto> availableAgents;
|
||||
|
||||
const AgentChatWindow({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.availableAgents,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AgentChatWindow> createState() => _AgentChatWindowState();
|
||||
}
|
||||
|
||||
class _AgentChatWindowState extends State<AgentChatWindow> {
|
||||
AgentDto? _selectedAgent;
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<_ChatMessage> _messages = [];
|
||||
bool _isSending = false;
|
||||
String? _conversationId; // Track current conversation
|
||||
final CqrsApiClient _apiClient = CqrsApiClient(
|
||||
config: ApiClientConfig.development,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.availableAgents.isNotEmpty) {
|
||||
_selectedAgent = widget.availableAgents.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageController.dispose();
|
||||
_scrollController.dispose();
|
||||
_apiClient.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
if (_messageController.text.trim().isEmpty || _selectedAgent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String messageText = _messageController.text.trim();
|
||||
|
||||
setState(() {
|
||||
_messages.add(_ChatMessage(
|
||||
message: messageText,
|
||||
isUser: true,
|
||||
senderName: 'You',
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
_isSending = true;
|
||||
});
|
||||
|
||||
_messageController.clear();
|
||||
_scrollToBottom();
|
||||
|
||||
// Send message to agent via API
|
||||
final Result<SendMessageResult> result = await _apiClient.sendMessage(
|
||||
SendMessageCommand(
|
||||
agentId: _selectedAgent!.id,
|
||||
conversationId: _conversationId, // null for first message
|
||||
message: messageText,
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
result.when(
|
||||
success: (SendMessageResult response) {
|
||||
setState(() {
|
||||
// Store conversation ID for subsequent messages
|
||||
_conversationId = response.conversationId;
|
||||
|
||||
// Add agent's response to chat
|
||||
_messages.add(_ChatMessage(
|
||||
message: response.agentResponse.content,
|
||||
isUser: false,
|
||||
senderName: _selectedAgent!.name,
|
||||
timestamp: response.agentResponse.timestamp,
|
||||
inputTokens: response.agentResponse.inputTokens,
|
||||
outputTokens: response.agentResponse.outputTokens,
|
||||
estimatedCost: response.agentResponse.estimatedCost,
|
||||
));
|
||||
_isSending = false;
|
||||
});
|
||||
_scrollToBottom();
|
||||
},
|
||||
error: (ApiErrorInfo error) {
|
||||
setState(() {
|
||||
_isSending = false;
|
||||
});
|
||||
|
||||
// Show error message to user
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Iconsax.danger, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
error.type == ApiErrorType.timeout
|
||||
? 'Request timed out. Agent may be processing...'
|
||||
: 'Failed to send message: ${error.message}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: const Duration(seconds: 4),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return FadeInUp(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header with agent selector
|
||||
_buildHeader(colorScheme),
|
||||
|
||||
// Divider
|
||||
Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
|
||||
// Messages area
|
||||
Expanded(
|
||||
child: _buildMessagesArea(colorScheme),
|
||||
),
|
||||
|
||||
// Divider
|
||||
Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
|
||||
// Input area
|
||||
_buildInputArea(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Title
|
||||
Icon(
|
||||
Iconsax.messages_3,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Agent Selector Dropdown
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: DropdownButton<AgentDto>(
|
||||
value: _selectedAgent,
|
||||
isExpanded: true,
|
||||
underline: const SizedBox(),
|
||||
icon: Icon(
|
||||
Iconsax.arrow_down_1,
|
||||
size: 16,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
dropdownColor: colorScheme.surfaceContainerHigh,
|
||||
items: widget.availableAgents.isEmpty
|
||||
? [
|
||||
DropdownMenuItem<AgentDto>(
|
||||
value: null,
|
||||
child: Text(
|
||||
'No agents available',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
: widget.availableAgents.map((AgentDto agent) {
|
||||
return DropdownMenuItem<AgentDto>(
|
||||
value: agent,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.cpu,
|
||||
size: 14,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
agent.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: widget.availableAgents.isEmpty
|
||||
? null
|
||||
: (AgentDto? newAgent) {
|
||||
setState(() {
|
||||
_selectedAgent = newAgent;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagesArea(ColorScheme colorScheme) {
|
||||
if (_messages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.message_text,
|
||||
size: 48,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No messages yet',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_selectedAgent != null
|
||||
? 'Start chatting with ${_selectedAgent!.name}'
|
||||
: 'Select an agent to begin',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _messages.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final _ChatMessage msg = _messages[index];
|
||||
return MessageBubble(
|
||||
message: msg.message,
|
||||
isUser: msg.isUser,
|
||||
senderName: msg.senderName,
|
||||
timestamp: msg.timestamp,
|
||||
inputTokens: msg.inputTokens,
|
||||
outputTokens: msg.outputTokens,
|
||||
estimatedCost: msg.estimatedCost,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputArea(ColorScheme colorScheme) {
|
||||
final bool canSend =
|
||||
_selectedAgent != null && !_isSending && _messageController.text.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Text input
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
enabled: _selectedAgent != null && !_isSending,
|
||||
maxLines: null,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedAgent != null
|
||||
? 'Type your message...'
|
||||
: 'Select an agent first',
|
||||
hintStyle: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
fontSize: 13,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
onSubmitted: (_) => canSend ? _sendMessage() : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Send button
|
||||
Material(
|
||||
color: canSend
|
||||
? colorScheme.primary
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: canSend ? _sendMessage : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: _isSending
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Iconsax.send_1,
|
||||
color: canSend
|
||||
? Colors.white
|
||||
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal message model for chat display
|
||||
class _ChatMessage {
|
||||
final String message;
|
||||
final bool isUser;
|
||||
final String senderName;
|
||||
final DateTime timestamp;
|
||||
final int? inputTokens;
|
||||
final int? outputTokens;
|
||||
final double? estimatedCost;
|
||||
|
||||
_ChatMessage({
|
||||
required this.message,
|
||||
required this.isUser,
|
||||
required this.senderName,
|
||||
required this.timestamp,
|
||||
this.inputTokens,
|
||||
this.outputTokens,
|
||||
this.estimatedCost,
|
||||
});
|
||||
}
|
||||
332
FRONTEND/lib/components/conversation_log_viewer.dart
Normal file
332
FRONTEND/lib/components/conversation_log_viewer.dart
Normal file
@ -0,0 +1,332 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
import '../api/api.dart';
|
||||
|
||||
/// Displays conversation logs with expandable messages and copy functionality
|
||||
class ConversationLogViewer extends StatefulWidget {
|
||||
final List<ConversationMessageDto> messages;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const ConversationLogViewer({
|
||||
super.key,
|
||||
required this.messages,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ConversationLogViewer> createState() => _ConversationLogViewerState();
|
||||
}
|
||||
|
||||
class _ConversationLogViewerState extends State<ConversationLogViewer> {
|
||||
final Set<String> _expandedMessageIds = {};
|
||||
late ScrollController _internalScrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_internalScrollController =
|
||||
widget.scrollController ?? ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.scrollController == null) {
|
||||
_internalScrollController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleExpand(String messageId) {
|
||||
setState(() {
|
||||
if (_expandedMessageIds.contains(messageId)) {
|
||||
_expandedMessageIds.remove(messageId);
|
||||
} else {
|
||||
_expandedMessageIds.add(messageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _copyToClipboard(String text, BuildContext context) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Row(
|
||||
children: [
|
||||
Icon(Iconsax.tick_circle, color: Colors.white, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Message copied to clipboard'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return FadeInUp(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(colorScheme),
|
||||
|
||||
// Divider
|
||||
Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
|
||||
// Messages list
|
||||
Expanded(
|
||||
child: _buildMessagesList(colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.document_text,
|
||||
color: colorScheme.secondary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Conversation Logs',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${widget.messages.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagesList(ColorScheme colorScheme) {
|
||||
if (widget.messages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.document_text,
|
||||
size: 48,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No conversation logs',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Start chatting to see logs here',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _internalScrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: widget.messages.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final ConversationMessageDto message = widget.messages[index];
|
||||
return FadeInUp(
|
||||
duration: Duration(milliseconds: 200 + (index * 30)),
|
||||
child: _buildLogRow(message, colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogRow(
|
||||
ConversationMessageDto message,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final bool isUser = message.role.toLowerCase() == 'user';
|
||||
final bool isExpanded = _expandedMessageIds.contains(message.id);
|
||||
final bool needsTruncation = message.content.length > 80;
|
||||
final String displayText = (isExpanded || !needsTruncation)
|
||||
? message.content
|
||||
: '${message.content.substring(0, 80)}...';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser
|
||||
? colorScheme.primaryContainer.withValues(alpha: 0.3)
|
||||
: colorScheme.secondaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isUser
|
||||
? colorScheme.primary.withValues(alpha: 0.2)
|
||||
: colorScheme.secondary.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: needsTruncation ? () => _toggleExpand(message.id) : null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header row with role, timestamp, and copy button
|
||||
Row(
|
||||
children: [
|
||||
// Role icon and name
|
||||
Icon(
|
||||
isUser ? Iconsax.user : Iconsax.cpu,
|
||||
size: 14,
|
||||
color: isUser
|
||||
? colorScheme.primary
|
||||
: colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
isUser ? 'User' : message.role,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isUser
|
||||
? colorScheme.primary
|
||||
: colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Timestamp
|
||||
Icon(
|
||||
Iconsax.clock,
|
||||
size: 11,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
timeago.format(message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Copy button
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _copyToClipboard(message.content, context),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Iconsax.copy,
|
||||
size: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Message content
|
||||
Text(
|
||||
displayText,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
|
||||
// Expand/collapse indicator
|
||||
if (needsTruncation)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isExpanded
|
||||
? Iconsax.arrow_up_2
|
||||
: Iconsax.arrow_down_1,
|
||||
size: 12,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isExpanded ? 'Show less' : 'Show more',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
187
FRONTEND/lib/components/message_bubble.dart
Normal file
187
FRONTEND/lib/components/message_bubble.dart
Normal file
@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
|
||||
/// Represents a single chat message bubble
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final String message;
|
||||
final bool isUser;
|
||||
final String senderName;
|
||||
final DateTime timestamp;
|
||||
final int? inputTokens;
|
||||
final int? outputTokens;
|
||||
final double? estimatedCost;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isUser,
|
||||
required this.senderName,
|
||||
required this.timestamp,
|
||||
this.inputTokens,
|
||||
this.outputTokens,
|
||||
this.estimatedCost,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return FadeInUp(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Align(
|
||||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.7,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Sender name and timestamp
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
senderName,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_formatTimestamp(timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Message bubble
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser
|
||||
? colorScheme.primary.withValues(alpha: 0.15)
|
||||
: colorScheme.secondary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16),
|
||||
topRight: const Radius.circular(16),
|
||||
bottomLeft: Radius.circular(isUser ? 16 : 4),
|
||||
bottomRight: Radius.circular(isUser ? 4 : 16),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isUser
|
||||
? colorScheme.primary.withValues(alpha: 0.3)
|
||||
: colorScheme.secondary.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
// Token metrics (only for agent responses)
|
||||
if (!isUser && (inputTokens != null || outputTokens != null))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.cpu,
|
||||
size: 10,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${inputTokens ?? 0} in',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'•',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${outputTokens ?? 0} out',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
),
|
||||
),
|
||||
if (estimatedCost != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'•',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'\$${estimatedCost!.toStringAsFixed(4)}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.secondary,
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTimestamp(DateTime dt) {
|
||||
final DateTime now = DateTime.now();
|
||||
final Duration diff = now.difference(dt);
|
||||
|
||||
if (diff.inMinutes < 1) {
|
||||
return 'Just now';
|
||||
} else if (diff.inHours < 1) {
|
||||
return '${diff.inMinutes}m ago';
|
||||
} else if (diff.inDays < 1) {
|
||||
return '${diff.inHours}h ago';
|
||||
} else {
|
||||
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,6 +70,20 @@ class _NavigationSidebarState extends State<NavigationSidebar> {
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildMenuItem(
|
||||
icon: Iconsax.messages_3,
|
||||
title: 'Conversations',
|
||||
pageId: 'conversations',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildMenuItem(
|
||||
icon: Iconsax.flash_1,
|
||||
title: 'Executions',
|
||||
pageId: 'executions',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildMenuItem(
|
||||
icon: Iconsax.chart_square,
|
||||
title: 'Analytics',
|
||||
|
||||
@ -6,6 +6,8 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'components/navigation_sidebar.dart';
|
||||
import 'pages/architech_page.dart';
|
||||
import 'pages/agents_page.dart';
|
||||
import 'pages/conversations_page.dart';
|
||||
import 'pages/executions_page.dart';
|
||||
|
||||
class ConsoleLandingPage extends StatefulWidget {
|
||||
const ConsoleLandingPage({super.key});
|
||||
@ -156,6 +158,10 @@ class _ConsoleLandingPageState extends State<ConsoleLandingPage> {
|
||||
return 'The Architech';
|
||||
case 'agents':
|
||||
return 'AI Agents';
|
||||
case 'conversations':
|
||||
return 'Conversations';
|
||||
case 'executions':
|
||||
return 'Executions';
|
||||
case 'analytics':
|
||||
return 'Analytics';
|
||||
case 'tools':
|
||||
@ -174,6 +180,10 @@ class _ConsoleLandingPageState extends State<ConsoleLandingPage> {
|
||||
return const ArchitechPage();
|
||||
case 'agents':
|
||||
return const AgentsPage();
|
||||
case 'conversations':
|
||||
return const ConversationsPage();
|
||||
case 'executions':
|
||||
return const ExecutionsPage();
|
||||
case 'dashboard':
|
||||
default:
|
||||
return _buildDashboardContent(colorScheme);
|
||||
|
||||
@ -1,101 +1,321 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import '../api/api.dart';
|
||||
import '../components/agent_chat_window.dart';
|
||||
|
||||
class ArchitechPage extends StatelessWidget {
|
||||
/// The Architech page - Dual agent chat interface
|
||||
///
|
||||
/// Features:
|
||||
/// - Two independent chat windows stacked vertically
|
||||
/// - Simultaneous conversations with different agents
|
||||
/// - Independent agent selection per chat window
|
||||
/// - Real-time AI responses with token tracking
|
||||
class ArchitechPage extends StatefulWidget {
|
||||
const ArchitechPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
State<ArchitechPage> createState() => _ArchitechPageState();
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
class _ArchitechPageState extends State<ArchitechPage> {
|
||||
final CqrsApiClient _apiClient = CqrsApiClient(
|
||||
config: ApiClientConfig.development,
|
||||
);
|
||||
|
||||
List<AgentDto>? _agents;
|
||||
bool _isLoadingAgents = true;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAgents();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_apiClient.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadAgents() async {
|
||||
setState(() {
|
||||
_isLoadingAgents = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final Result<List<AgentDto>> result = await _apiClient.listAgents();
|
||||
|
||||
result.when(
|
||||
success: (List<AgentDto> agents) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_agents = agents;
|
||||
_isLoadingAgents = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (ApiErrorInfo error) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = error.message;
|
||||
_isLoadingAgents = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Empty State Content
|
||||
FadeInUp(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
padding: const EdgeInsets.all(48),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Iconsax.hierarchy_square,
|
||||
size: 80,
|
||||
color: colorScheme.primary.withValues(alpha:0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Coming Soon',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'The Architech module is currently under development. This powerful tool will allow you to design, visualize, and manage your AI infrastructure with ease.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha:0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha:0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.info_circle,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Stay tuned for updates',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Header
|
||||
_buildHeader(colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Main Content
|
||||
Expanded(
|
||||
child: _buildMainContent(colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return FadeInDown(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.hierarchy_square,
|
||||
color: colorScheme.primary,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'The Architech',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Dual agent conversation workspace',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Iconsax.refresh, color: colorScheme.primary),
|
||||
onPressed: _loadAgents,
|
||||
tooltip: 'Refresh agents',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainContent(ColorScheme colorScheme) {
|
||||
if (_isLoadingAgents) {
|
||||
return _buildLoadingState(colorScheme);
|
||||
}
|
||||
|
||||
if (_errorMessage != null) {
|
||||
return _buildErrorState(colorScheme);
|
||||
}
|
||||
|
||||
if (_agents == null || _agents!.isEmpty) {
|
||||
return _buildEmptyAgentsState(colorScheme);
|
||||
}
|
||||
|
||||
// Simple stacked layout - two chat windows vertically
|
||||
return _buildChatLayout(colorScheme);
|
||||
}
|
||||
|
||||
Widget _buildChatLayout(ColorScheme colorScheme) {
|
||||
return Column(
|
||||
children: [
|
||||
// Chat Window 1 (top half)
|
||||
Expanded(
|
||||
child: FadeInUp(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: AgentChatWindow(
|
||||
title: 'Agent Chat 1',
|
||||
availableAgents: _agents!,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Chat Window 2 (bottom half)
|
||||
Expanded(
|
||||
child: FadeInUp(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: AgentChatWindow(
|
||||
title: 'Agent Chat 2',
|
||||
availableAgents: _agents!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: FadeIn(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(colorScheme.primary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Loading agents and conversations...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: FadeIn(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.danger,
|
||||
size: 64,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Error Loading Data',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_errorMessage ?? 'Unknown error',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadAgents,
|
||||
icon: const Icon(Iconsax.refresh),
|
||||
label: const Text('Retry'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyAgentsState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: FadeIn(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.cpu,
|
||||
size: 80,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No Agents Available',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Create agents in the AI Agents page to start using The Architech',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.info_circle,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Go to AI Agents to create your first agent',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
467
FRONTEND/lib/pages/conversations_page.dart
Normal file
467
FRONTEND/lib/pages/conversations_page.dart
Normal file
@ -0,0 +1,467 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
import '../api/api.dart';
|
||||
|
||||
/// Conversations management page
|
||||
///
|
||||
/// Displays all conversations with details about messages, activity, and status.
|
||||
/// Integrates with backend CQRS API for conversation listing and management.
|
||||
class ConversationsPage extends StatefulWidget {
|
||||
const ConversationsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ConversationsPage> createState() => _ConversationsPageState();
|
||||
}
|
||||
|
||||
class _ConversationsPageState extends State<ConversationsPage> {
|
||||
final CqrsApiClient _apiClient = CqrsApiClient(
|
||||
config: ApiClientConfig.development,
|
||||
);
|
||||
|
||||
List<ConversationListItemDto>? _conversations;
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
String _filterStatus = 'all'; // all, active, inactive
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadConversations();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_apiClient.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadConversations() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final Result<List<ConversationListItemDto>> result =
|
||||
await _apiClient.listConversations();
|
||||
|
||||
result.when(
|
||||
success: (List<ConversationListItemDto> conversations) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_conversations = conversations;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (ApiErrorInfo error) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = error.message;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<ConversationListItemDto> get _filteredConversations {
|
||||
if (_conversations == null) return [];
|
||||
|
||||
switch (_filterStatus) {
|
||||
case 'active':
|
||||
return _conversations!.where((c) => c.isActive).toList();
|
||||
case 'inactive':
|
||||
return _conversations!.where((c) => !c.isActive).toList();
|
||||
default:
|
||||
return _conversations!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with actions
|
||||
_buildHeader(colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Filter chips
|
||||
_buildFilterChips(colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Conversations list
|
||||
Expanded(
|
||||
child: _buildConversationsList(colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.messages_3,
|
||||
color: colorScheme.primary,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Conversations',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_conversations != null
|
||||
? '${_conversations!.length} total conversations'
|
||||
: 'Loading...',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Iconsax.refresh, color: colorScheme.primary),
|
||||
onPressed: _loadConversations,
|
||||
tooltip: 'Refresh conversations',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChips(ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildFilterChip('All', 'all', colorScheme),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterChip('Active', 'active', colorScheme),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterChip('Inactive', 'inactive', colorScheme),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(
|
||||
String label,
|
||||
String value,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final bool isSelected = _filterStatus == value;
|
||||
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (bool selected) {
|
||||
setState(() {
|
||||
_filterStatus = value;
|
||||
});
|
||||
},
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConversationsList(ColorScheme colorScheme) {
|
||||
if (_isLoading) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Loading conversations...',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_errorMessage != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.warning_2,
|
||||
size: 48,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Failed to load conversations',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadConversations,
|
||||
icon: const Icon(Iconsax.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<ConversationListItemDto> filtered = _filteredConversations;
|
||||
|
||||
if (filtered.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.message_text,
|
||||
size: 64,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No conversations found',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_filterStatus == 'all'
|
||||
? 'Start a conversation to see it here'
|
||||
: 'No $_filterStatus conversations',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return FadeInUp(
|
||||
duration: Duration(milliseconds: 300 + (index * 50)),
|
||||
child: _buildConversationCard(filtered[index], colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConversationCard(
|
||||
ConversationListItemDto conversation,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 0,
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
// TODO: Navigate to conversation detail
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('View conversation: ${conversation.title} (not implemented)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: conversation.isActive
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Iconsax.messages_3,
|
||||
color: conversation.isActive
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title and status
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
conversation.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: conversation.isActive
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
conversation.isActive ? 'Active' : 'Inactive',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: conversation.isActive
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Summary
|
||||
if (conversation.summary != null)
|
||||
Text(
|
||||
conversation.summary!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Stats
|
||||
Row(
|
||||
children: [
|
||||
_buildStat(
|
||||
Iconsax.message_text,
|
||||
'${conversation.messageCount}',
|
||||
'messages',
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildStat(
|
||||
Iconsax.flash_1,
|
||||
'${conversation.executionCount}',
|
||||
'executions',
|
||||
colorScheme,
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Iconsax.clock,
|
||||
size: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
timeago.format(conversation.lastMessageAt),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStat(
|
||||
IconData icon,
|
||||
String value,
|
||||
String label,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
664
FRONTEND/lib/pages/executions_page.dart
Normal file
664
FRONTEND/lib/pages/executions_page.dart
Normal file
@ -0,0 +1,664 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:animate_do/animate_do.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
import '../api/api.dart';
|
||||
|
||||
/// Executions dashboard page
|
||||
///
|
||||
/// Displays all agent executions with filtering by status, metrics, and details.
|
||||
/// Integrates with backend CQRS API for execution monitoring and analysis.
|
||||
class ExecutionsPage extends StatefulWidget {
|
||||
const ExecutionsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ExecutionsPage> createState() => _ExecutionsPageState();
|
||||
}
|
||||
|
||||
class _ExecutionsPageState extends State<ExecutionsPage> {
|
||||
final CqrsApiClient _apiClient = CqrsApiClient(
|
||||
config: ApiClientConfig.development,
|
||||
);
|
||||
|
||||
List<ExecutionListItemDto>? _executions;
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
ExecutionStatus? _filterStatus; // null = all, or specific status
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadExecutions();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_apiClient.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadExecutions() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final Result<List<ExecutionListItemDto>> result = _filterStatus == null
|
||||
? await _apiClient.listExecutions()
|
||||
: await _apiClient.listExecutionsByStatus(_filterStatus!);
|
||||
|
||||
result.when(
|
||||
success: (List<ExecutionListItemDto> executions) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_executions = executions;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (ApiErrorInfo error) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = error.message;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, int> get _statusCounts {
|
||||
if (_executions == null) return {};
|
||||
|
||||
final Map<String, int> counts = {};
|
||||
for (final ExecutionListItemDto exec in _executions!) {
|
||||
counts[exec.status.value] = (counts[exec.status.value] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
double get _totalCost {
|
||||
if (_executions == null) return 0.0;
|
||||
|
||||
return _executions!.fold<double>(
|
||||
0.0,
|
||||
(double sum, ExecutionListItemDto exec) =>
|
||||
sum + (exec.estimatedCost ?? 0.0),
|
||||
);
|
||||
}
|
||||
|
||||
int get _totalTokens {
|
||||
if (_executions == null) return 0;
|
||||
|
||||
return _executions!.fold<int>(
|
||||
0,
|
||||
(int sum, ExecutionListItemDto exec) =>
|
||||
sum + (exec.inputTokens ?? 0) + (exec.outputTokens ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with actions
|
||||
_buildHeader(colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Metrics cards
|
||||
_buildMetricsCards(colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Status filter chips
|
||||
_buildStatusFilters(colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Executions list
|
||||
Expanded(
|
||||
child: _buildExecutionsList(colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.flash_1,
|
||||
color: colorScheme.primary,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Agent Executions',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_executions != null
|
||||
? '${_executions!.length} total executions'
|
||||
: 'Loading...',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Iconsax.refresh, color: colorScheme.primary),
|
||||
onPressed: _loadExecutions,
|
||||
tooltip: 'Refresh executions',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricsCards(ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
'Total Cost',
|
||||
'\$${_totalCost.toStringAsFixed(4)}',
|
||||
Iconsax.dollar_circle,
|
||||
colorScheme.primaryContainer,
|
||||
colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
'Total Tokens',
|
||||
_totalTokens.toString(),
|
||||
Iconsax.cpu,
|
||||
colorScheme.secondaryContainer,
|
||||
colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
'Avg Messages',
|
||||
_executions != null && _executions!.isNotEmpty
|
||||
? (_executions!.fold<int>(
|
||||
0,
|
||||
(int sum, ExecutionListItemDto exec) =>
|
||||
sum + exec.messageCount,
|
||||
) /
|
||||
_executions!.length)
|
||||
.toStringAsFixed(1)
|
||||
: '0',
|
||||
Iconsax.message_text,
|
||||
colorScheme.tertiaryContainer,
|
||||
colorScheme.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricCard(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color backgroundColor,
|
||||
Color textColor,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: textColor, size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusFilters(ColorScheme colorScheme) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusChip('All', null, colorScheme),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusChip(
|
||||
'Pending',
|
||||
ExecutionStatus.pending,
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusChip(
|
||||
'Running',
|
||||
ExecutionStatus.running,
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusChip(
|
||||
'Completed',
|
||||
ExecutionStatus.completed,
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusChip(
|
||||
'Failed',
|
||||
ExecutionStatus.failed,
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatusChip(
|
||||
'Cancelled',
|
||||
ExecutionStatus.cancelled,
|
||||
colorScheme,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusChip(
|
||||
String label,
|
||||
ExecutionStatus? status,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final bool isSelected = _filterStatus == status;
|
||||
final int count =
|
||||
status == null ? (_executions?.length ?? 0) : (_statusCounts[status.value] ?? 0);
|
||||
|
||||
return FilterChip(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label),
|
||||
if (count > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer.withValues(alpha: 0.2)
|
||||
: colorScheme.onSurface.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (bool selected) {
|
||||
setState(() {
|
||||
_filterStatus = status;
|
||||
});
|
||||
_loadExecutions();
|
||||
},
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExecutionsList(ColorScheme colorScheme) {
|
||||
if (_isLoading) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Loading executions...',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_errorMessage != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.warning_2,
|
||||
size: 48,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Failed to load executions',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadExecutions,
|
||||
icon: const Icon(Iconsax.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_executions == null || _executions!.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.flash_1,
|
||||
size: 64,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No executions found',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_filterStatus == null
|
||||
? 'Start an agent execution to see it here'
|
||||
: 'No ${_filterStatus!.value.toLowerCase()} executions',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _executions!.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return FadeInUp(
|
||||
duration: Duration(milliseconds: 300 + (index * 50)),
|
||||
child: _buildExecutionCard(_executions![index], colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExecutionCard(
|
||||
ExecutionListItemDto execution,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final Color statusColor = _getStatusColor(execution.status, colorScheme);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
elevation: 0,
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
// TODO: Navigate to execution detail
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('View execution details (not implemented)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header row
|
||||
Row(
|
||||
children: [
|
||||
// Status indicator
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Agent name
|
||||
Expanded(
|
||||
child: Text(
|
||||
execution.agentName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Status badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
execution.status.value,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// User prompt
|
||||
Text(
|
||||
execution.userPrompt,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
// Error message if failed
|
||||
if (execution.errorMessage != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Iconsax.warning_2,
|
||||
size: 16,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
execution.errorMessage!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Stats row
|
||||
Row(
|
||||
children: [
|
||||
_buildExecutionStat(
|
||||
Iconsax.message_text,
|
||||
'${execution.messageCount}',
|
||||
colorScheme,
|
||||
),
|
||||
if (execution.inputTokens != null &&
|
||||
execution.outputTokens != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
_buildExecutionStat(
|
||||
Iconsax.cpu,
|
||||
'${execution.inputTokens! + execution.outputTokens!}',
|
||||
colorScheme,
|
||||
),
|
||||
],
|
||||
if (execution.estimatedCost != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
_buildExecutionStat(
|
||||
Iconsax.dollar_circle,
|
||||
'\$${execution.estimatedCost!.toStringAsFixed(4)}',
|
||||
colorScheme,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Iconsax.clock,
|
||||
size: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
timeago.format(execution.startedAt),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExecutionStat(
|
||||
IconData icon,
|
||||
String value,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(ExecutionStatus status, ColorScheme colorScheme) {
|
||||
switch (status) {
|
||||
case ExecutionStatus.pending:
|
||||
return colorScheme.tertiary;
|
||||
case ExecutionStatus.running:
|
||||
return colorScheme.primary;
|
||||
case ExecutionStatus.completed:
|
||||
return Colors.green;
|
||||
case ExecutionStatus.failed:
|
||||
return colorScheme.error;
|
||||
case ExecutionStatus.cancelled:
|
||||
return colorScheme.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,6 +304,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.8"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -565,6 +573,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
timeago:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: timeago
|
||||
sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -47,6 +47,7 @@ dependencies:
|
||||
http: ^1.2.2 # HTTP client for API requests
|
||||
json_annotation: ^4.9.0 # JSON serialization annotations
|
||||
openapi_generator_annotations: ^5.0.1 # OpenAPI annotations
|
||||
timeago: ^3.7.0 # Human-readable time formatting
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
2
terminal.md
Normal file
2
terminal.md
Normal file
@ -0,0 +1,2 @@
|
||||
BE: --project Codex.Api/Codex.Api.csproj
|
||||
FE: flutter run -d macos
|
||||
Loading…
Reference in New Issue
Block a user