Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,957 changes: 10,957 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@
"swagger-ui-express": "^5.0.1",
"typeorm": "^1.1.0"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": ["**/*.(t|j)s", "!**/*.module.ts"],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
}
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
Expand Down
97 changes: 97 additions & 0 deletions src/agent/agent.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AgentController } from './agent.controller';
import { AgentService } from './agent.service';
import { HttpService } from '@nestjs/axios';
import { of } from 'rxjs';
import { AxiosResponse } from 'axios';
import {
AnalyzeMarketDto,
CreatePredictionDto,
CoachAdviceRequestDto,
} from './dto';

describe('AgentController', () => {
let controller: AgentController;
let service: AgentService;

const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AgentController],
providers: [
AgentService,
{
provide: HttpService,
useValue: mockHttpService,
},
],
}).compile();

controller = module.get<AgentController>(AgentController);
service = module.get<AgentService>(AgentService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('getStatus', () => {
it('should return agent status', async () => {
const result = await controller.getStatus();
expect(result).toBeDefined();
expect(result.status).toBe('healthy');
expect(result.mode).toBe('active');
expect(result.capabilities).toBeInstanceOf(Array);
expect(result.capabilities.length).toBeGreaterThan(0);
});

it('should include uptime as a positive number', async () => {
const result = await controller.getStatus();
expect(result.uptime).toBeGreaterThanOrEqual(0);
});
});

describe('analyze', () => {
it('should throw NotImplementedException', async () => {
const dto: AnalyzeMarketDto = {
marketId: '550e8400-e29b-41d4-a716-446655440000',
};
await expect(controller.analyze(dto)).rejects.toThrow();
});
});

describe('predict', () => {
it('should throw NotImplementedException', async () => {
const dto: CreatePredictionDto = {
marketId: '550e8400-e29b-41d4-a716-446655440000',
outcome: 'team_a_win',
};
await expect(controller.predict(dto)).rejects.toThrow();
});
});

describe('coach', () => {
it('should throw NotImplementedException', async () => {
const dto: CoachAdviceRequestDto = {
userId: '550e8400-e29b-41d4-a716-446655440000',
};
await expect(controller.coach(dto)).rejects.toThrow();
});
});

describe('getLeaderboardInsights', () => {
it('should throw NotImplementedException', async () => {
await expect(
controller.getLeaderboardInsights(
'550e8400-e29b-41d4-a716-446655440000',
'global',
10,
),
).rejects.toThrow();
});
});
});
199 changes: 196 additions & 3 deletions src/agent/agent.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,204 @@
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
Controller,
Get,
Post,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
ApiQuery,
ApiBody,
} from '@nestjs/swagger';
import { AgentService } from './agent.service';
import {
AnalyzeMarketDto,
AnalysisResultDto,
CreatePredictionDto,
PredictionResultDto,
AgentStatusDto,
CoachAdviceRequestDto,
CoachAdviceResponseDto,
LeaderboardInsightDto,
ApiErrorDto,
} from './dto';

@ApiTags('agent')
@Controller('agent')
export class AgentController {
constructor(private readonly agentService: AgentService) {}

// Agent endpoints will be implemented as issues
@Post('analyze')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({
summary: 'Analyze a prediction market',
description:
'Performs a comprehensive AI-driven analysis of a prediction market, considering multiple factors such as team form, historical data, and market conditions to generate a confidence score and recommendation.',
})
@ApiBody({ type: AnalyzeMarketDto })
@ApiResponse({
status: 200,
description: 'Market analysis completed successfully',
type: AnalysisResultDto,
})
@ApiResponse({
status: 400,
description: 'Invalid request parameters',
type: ApiErrorDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid or missing API key',
})
@ApiResponse({
status: 503,
description: 'AI service unavailable',
type: ApiErrorDto,
})
async analyze(@Body() dto: AnalyzeMarketDto): Promise<AnalysisResultDto> {
return this.agentService.analyzeMarket(dto);
}

@Post('predict')
@HttpCode(HttpStatus.CREATED)
@ApiBearerAuth()
@ApiOperation({
summary: 'Create an AI-powered prediction',
description:
'Submits an AI-generated prediction for a specified market, optionally linked to a pre-computed analysis. The agent will stake on behalf of the system if a stake amount is provided.',
})
@ApiBody({ type: CreatePredictionDto })
@ApiResponse({
status: 201,
description: 'Prediction created successfully',
type: PredictionResultDto,
})
@ApiResponse({
status: 400,
description: 'Invalid request parameters',
type: ApiErrorDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid or missing API key',
})
@ApiResponse({
status: 404,
description: 'Market not found',
type: ApiErrorDto,
})
async predict(@Body() dto: CreatePredictionDto): Promise<PredictionResultDto> {
return this.agentService.createPrediction(dto);
}

@Get('status')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({
summary: 'Get agent operational status',
description:
'Returns the current operational status of the AI agent, including connectivity to AI models, blockchain, oracles, and database. Provides capability-level health checks.',
})
@ApiResponse({
status: 200,
description: 'Agent status retrieved successfully',
type: AgentStatusDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid or missing API key',
})
async getStatus(): Promise<AgentStatusDto> {
return this.agentService.getStatus();
}

@Post('coach')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({
summary: 'Get personalized coaching advice',
description:
'Generates personalized performance insights and strategic advice for a user based on their prediction history, accuracy trends, and market participation patterns.',
})
@ApiBody({ type: CoachAdviceRequestDto })
@ApiResponse({
status: 200,
description: 'Coaching advice generated successfully',
type: CoachAdviceResponseDto,
})
@ApiResponse({
status: 400,
description: 'Invalid request parameters',
type: ApiErrorDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid or missing API key',
})
@ApiResponse({
status: 404,
description: 'User not found',
type: ApiErrorDto,
})
async coach(@Body() dto: CoachAdviceRequestDto): Promise<CoachAdviceResponseDto> {
return this.agentService.getCoachAdvice(dto);
}

@Get('leaderboard/:userId')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({
summary: 'Get leaderboard insights for a user',
description:
'Retrieves leaderboard ranking and performance insights for a specific user, including their current rank, rank trend, and comparison with top participants.',
})
@ApiParam({
name: 'userId',
description: 'The unique identifier of the user',
example: '550e8400-e29b-41d4-a716-446655440000',
type: String,
})
@ApiQuery({
name: 'type',
description: 'Leaderboard type to query',
required: false,
example: 'global',
enum: ['global', 'weekly', 'monthly'],
})
@ApiQuery({
name: 'limit',
description: 'Number of top entries to include',
required: false,
example: 10,
type: Number,
})
@ApiResponse({
status: 200,
description: 'Leaderboard insights retrieved successfully',
type: LeaderboardInsightDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - invalid or missing API key',
})
@ApiResponse({
status: 404,
description: 'User not found on leaderboard',
type: ApiErrorDto,
})
async getLeaderboardInsights(
@Param('userId') userId: string,
@Query('type') type?: string,
@Query('limit') limit?: number,
): Promise<LeaderboardInsightDto> {
return this.agentService.getLeaderboardInsights(userId, type, limit);
}
}
57 changes: 57 additions & 0 deletions src/agent/agent.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AgentService } from './agent.service';
import { HttpService } from '@nestjs/axios';
import { of } from 'rxjs';
import { AxiosResponse } from 'axios';

describe('AgentService', () => {
let service: AgentService;

const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AgentService,
{
provide: HttpService,
useValue: mockHttpService,
},
],
}).compile();

service = module.get<AgentService>(AgentService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('getStatus', () => {
it('should return healthy status', async () => {
const result = await service.getStatus();
expect(result.status).toBe('healthy');
expect(result.mode).toBe('active');
});

it('should return all capability names', async () => {
const result = await service.getStatus();
const capabilityNames = result.capabilities.map((c) => c.name);
expect(capabilityNames).toContain('prediction_analyst');
expect(capabilityNames).toContain('market_creator');
expect(capabilityNames).toContain('oracle_validator');
expect(capabilityNames).toContain('leaderboard_coach');
expect(capabilityNames).toContain('creator_assistant');
});

it('should have a valid timestamp', async () => {
const result = await service.getStatus();
const timestamp = new Date(result.timestamp);
expect(timestamp instanceof Date).toBe(true);
expect(isNaN(timestamp.getTime())).toBe(false);
});
});
});
Loading