add mana core

This commit is contained in:
Wuesteon 2025-11-25 18:56:35 +01:00
parent ce71db2fc0
commit 754e87ebc0
112 changed files with 34765 additions and 548 deletions

View file

@ -23,6 +23,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@manacore/shared-errors": "workspace:*",
"@google-cloud/aiplatform": "^3.34.0",
"@google-cloud/storage": "^7.15.0",
"@google/genai": "^1.14.0",

View file

@ -1,8 +1,11 @@
import { Injectable } from '@nestjs/common';
import {
Injectable,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
type AsyncResult,
ok,
err,
NotFoundError,
DatabaseError,
} from '@manacore/shared-errors';
// Define interfaces for our character data
export interface CharacterCreateDto {
@ -23,6 +26,21 @@ export interface CharacterUpdateDto {
images_data?: any[];
}
// Character type for return values
export interface Character {
id: string;
name: string;
original_description?: string;
character_description_prompt?: string;
character_description?: string;
image_url?: string;
animal_type?: string;
images_data?: any[];
user_id: string;
created_at: string;
updated_at: string;
}
@Injectable()
export class CharacterService {
constructor() {}
@ -32,13 +50,13 @@ export class CharacterService {
* @param execute The execute function from SupabaseAuthService
* @param userId The authenticated user ID
* @param characterData The character data to create
* @returns The created character
* @returns Result containing the created character or error
*/
async createCharacter(
execute: <T>(operation: string, params?: any) => Promise<T>,
_userId: string,
characterData: CharacterCreateDto,
) {
): AsyncResult<Character> {
try {
// Ensure animalType has a default value if undefined (based on memory)
if (characterData.animalType === undefined) {
@ -46,7 +64,7 @@ export class CharacterService {
}
// Use the execute function to create a character as the authenticated user
const character = await execute('create_character', {
const character = await execute<Character>('create_character', {
name: characterData.name,
description: characterData.original_description,
prompt: characterData.character_description_prompt,
@ -54,11 +72,16 @@ export class CharacterService {
images_data: characterData.images_data || [],
});
return character;
return ok(character);
} catch (error) {
console.error('Error creating character:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to create character: ${message}`);
return err(
DatabaseError.queryFailed(
'create_character',
error instanceof Error ? error.message : 'Unknown error',
error instanceof Error ? error : undefined,
),
);
}
}
@ -66,26 +89,29 @@ export class CharacterService {
* Get a character by ID
* @param execute The execute function from SupabaseAuthService
* @param characterId The character ID to get
* @returns The character
* @returns Result containing the character or error
*/
async getCharacter(
execute: <T>(operation: string, params?: any) => Promise<T>,
characterId: string,
) {
): AsyncResult<Character> {
try {
const character = await execute('get_character', { id: characterId });
const character = await execute<Character | null>('get_character', { id: characterId });
if (!character) {
throw new NotFoundException(
`Character with ID ${characterId} not found`,
);
return err(NotFoundError.resource('Character', characterId));
}
return character;
return ok(character);
} catch (error) {
if (error instanceof NotFoundException) throw error;
const message = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to get character: ${message}`);
console.error('Error getting character:', error);
return err(
DatabaseError.queryFailed(
'get_character',
error instanceof Error ? error.message : 'Unknown error',
error instanceof Error ? error : undefined,
),
);
}
}
@ -94,13 +120,13 @@ export class CharacterService {
* @param execute The execute function from SupabaseAuthService
* @param characterId The character ID to update
* @param updateData The character data to update
* @returns The updated character
* @returns Result containing the updated character or error
*/
async updateCharacter(
execute: <T>(operation: string, params?: any) => Promise<T>,
characterId: string,
updateData: CharacterUpdateDto,
) {
): AsyncResult<Character> {
try {
// Check if this is Finnia and ensure she's described as a magical fox (based on memory)
if (updateData.name === 'Finnia' && updateData.original_description) {
@ -109,7 +135,7 @@ export class CharacterService {
}
}
const character = await execute('update_character', {
const character = await execute<Character | null>('update_character', {
id: characterId,
name: updateData.name,
description: updateData.original_description,
@ -119,16 +145,19 @@ export class CharacterService {
});
if (!character) {
throw new NotFoundException(
`Character with ID ${characterId} not found`,
);
return err(NotFoundError.resource('Character', characterId));
}
return character;
return ok(character);
} catch (error) {
if (error instanceof NotFoundException) throw error;
const message = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to update character: ${message}`);
console.error('Error updating character:', error);
return err(
DatabaseError.queryFailed(
'update_character',
error instanceof Error ? error.message : 'Unknown error',
error instanceof Error ? error : undefined,
),
);
}
}
@ -136,43 +165,52 @@ export class CharacterService {
* Delete a character
* @param execute The execute function from SupabaseAuthService
* @param characterId The character ID to delete
* @returns The deleted character
* @returns Result containing the deleted character or error
*/
async deleteCharacter(
execute: <T>(operation: string, params?: any) => Promise<T>,
characterId: string,
) {
): AsyncResult<Character> {
try {
const character = await execute('delete_character', { id: characterId });
const character = await execute<Character | null>('delete_character', { id: characterId });
if (!character) {
throw new NotFoundException(
`Character with ID ${characterId} not found`,
);
return err(NotFoundError.resource('Character', characterId));
}
return character;
return ok(character);
} catch (error) {
if (error instanceof NotFoundException) throw error;
const message = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to delete character: ${message}`);
console.error('Error deleting character:', error);
return err(
DatabaseError.queryFailed(
'delete_character',
error instanceof Error ? error.message : 'Unknown error',
error instanceof Error ? error : undefined,
),
);
}
}
/**
* List all characters for the authenticated user
* @param execute The execute function from SupabaseAuthService
* @returns An array of characters
* @returns Result containing an array of characters or error
*/
async listCharacters(
execute: <T>(operation: string, params?: any) => Promise<T>,
) {
): AsyncResult<Character[]> {
try {
const characters = await execute('list_characters', {});
return characters || [];
const characters = await execute<Character[] | null>('list_characters', {});
return ok(characters || []);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
throw new BadRequestException(`Failed to list characters: ${message}`);
console.error('Error listing characters:', error);
return err(
DatabaseError.queryFailed(
'list_characters',
error instanceof Error ? error.message : 'Unknown error',
error instanceof Error ? error : undefined,
),
);
}
}
}

View file

@ -3,8 +3,7 @@ import { AppModule } from './app.module';
import { Logger, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppConfig } from './config/app.config';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { ErrorLoggingService } from './core/services/error-logging.service';
import { AppExceptionFilter } from '@manacore/shared-errors/nestjs';
async function bootstrap() {
const logger = new Logger('Bootstrap');
@ -45,9 +44,8 @@ async function bootstrap() {
}),
);
// Get ErrorLoggingService from DI container and pass to filter
const errorLoggingService = app.get(ErrorLoggingService);
app.useGlobalFilters(new HttpExceptionFilter(errorLoggingService));
// Global exception filter for standardized error responses
app.useGlobalFilters(new AppExceptionFilter());
// Use PORT env variable (required by Cloud Run) or fallback to config
const port = process.env.PORT || config?.port || 3000;

View file

@ -14,6 +14,7 @@ import {
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { isOk } from '@manacore/shared-errors';
import {
CreateStoryDto,
CreateStoryWithAnimalCharacterDto,
@ -323,19 +324,17 @@ export class StoryController {
updateDto.storyTextGerman,
);
if (updateResult.error || !updateResult.data) {
if (!isOk(updateResult)) {
this.logger.error(
`[StoryController] Error updating page: ${updateResult.error?.message}`,
);
throw new BadRequestException(
updateResult.error?.message || 'Failed to update page',
`[StoryController] Error updating page: ${updateResult.error.message}`,
);
throw updateResult.error; // Caught by AppExceptionFilter
}
// 6. Update the story in the database with new pages data
const updatedStory = await this.supabaseService.updateStory(
storyId,
{ pages_data: updateResult.data },
{ pages_data: updateResult.value },
token,
);

View file

@ -2,7 +2,15 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { Character } from '../core/models/character';
import { Result } from '../core/models/error';
import {
type AsyncResult,
type Result,
ok,
err,
ServiceError,
ValidationError,
NotFoundError,
} from '@manacore/shared-errors';
import {
STORY_RESPONSE_FORMAT,
STORY_TITLE_FORMAT_GERMAN,
@ -51,7 +59,7 @@ export class StoryService {
storyDescription: string,
character: Character,
authorSystemPrompt: string,
): Promise<Result<StoryResponse>> {
): AsyncResult<StoryResponse> {
// Log character data for debugging
this.logger.log(`Creating storyline for character: ${character.name}`);
this.logger.log(
@ -125,30 +133,29 @@ export class StoryService {
}
}
return {
data: {
pages: parsedResponse.pages,
},
error: null,
};
return ok({ pages: parsedResponse.pages });
} catch (error) {
if (axios.isAxiosError(error)) {
console.error('API Error:', {
this.logger.error('API Error:', {
status: error.response?.status,
data: error.response?.data,
});
return {
data: null,
error: new Error(`Failed to create story: ${error.message}`),
};
return err(
ServiceError.generationFailed(
'Azure OpenAI',
`Failed to create story: ${error.message}`,
error,
),
);
}
console.error('Error creating story:', error);
return {
data: null,
error: new Error(
this.logger.error('Error creating story:', error);
return err(
ServiceError.generationFailed(
'Azure OpenAI',
error instanceof Error ? error.message : String(error),
error instanceof Error ? error : undefined,
),
};
);
}
}
@ -163,7 +170,7 @@ export class StoryService {
storyDescription: string,
character: Character,
authorSystemPrompt: string,
): Promise<Result<StoryResponse>> {
): AsyncResult<StoryResponse> {
try {
// Log character data for debugging
this.logger.log(`Creating animal story for character: ${character.name}`);
@ -239,49 +246,45 @@ export class StoryService {
}
}
return {
data: {
pages: parsedResponse.pages,
},
error: null,
};
return ok({ pages: parsedResponse.pages });
} catch (error) {
if (axios.isAxiosError(error)) {
console.error('API Error:', {
this.logger.error('API Error:', {
status: error.response?.status,
data: error.response?.data,
});
// Try with Gemini as fallback for axios errors too
try {
console.log('Falling back to Gemini after axios error...');
this.logger.log('Falling back to Gemini after axios error...');
const geminiResult = await this.createAnimalStoryWithGemini(
storyDescription,
character.animal_type,
authorSystemPrompt,
);
if (geminiResult.pages) {
return {
data: { pages: geminiResult.pages },
error: null,
};
return ok({ pages: geminiResult.pages });
}
} catch (geminiError) {
console.error('Gemini fallback also failed:', geminiError);
this.logger.error('Gemini fallback also failed:', geminiError);
}
return {
data: null,
error: new Error(`Failed to create animal story: ${error.message}`),
};
return err(
ServiceError.generationFailed(
'Azure OpenAI',
`Failed to create animal story: ${error.message}`,
error,
),
);
}
console.error('Error creating animal story:', error);
return {
data: null,
error: new Error(
this.logger.error('Error creating animal story:', error);
return err(
ServiceError.generationFailed(
'Azure OpenAI',
error instanceof Error ? error.message : String(error),
error instanceof Error ? error : undefined,
),
};
);
}
}
@ -362,7 +365,7 @@ export class StoryService {
public async generateStoryTitle(
story: StoryResponse['pages'],
): Promise<Result<string>> {
): AsyncResult<string> {
const combinedStory = story.map((page) => page.text).join(' ');
const messages = [
{
@ -391,25 +394,23 @@ export class StoryService {
},
);
return {
error: null,
data: JSON.parse(response.data.choices[0].message.content)?.title,
};
const title = JSON.parse(response.data.choices[0].message.content)?.title;
return ok(title);
} catch (error) {
console.error('Error generating story title:', error);
return {
data: null,
error: new Error(
this.logger.error('Error generating story title:', error);
return err(
ServiceError.generationFailed(
'Azure OpenAI',
error instanceof Error ? error.message : String(error),
error instanceof Error ? error : undefined,
),
};
);
}
}
/**
* Update story page text
* @param storyId The ID of the story
* @param pagesData The pages data array
* @param pageNumber The page number to update
* @param storyText The new story text (optional)
* @param storyTextGerman The new German story text (optional)
@ -421,59 +422,40 @@ export class StoryService {
storyText?: string,
storyTextGerman?: string,
): Result<any[]> {
try {
this.logger.log(`[StoryService] Updating page ${pageNumber}`);
this.logger.log(`[StoryService] Updating page ${pageNumber}`);
if (!pagesData || !Array.isArray(pagesData)) {
return {
data: null,
error: new Error('Invalid pages data'),
};
}
// Find the page to update
const pageIndex = pagesData.findIndex(
(page) => page.page_number === pageNumber,
);
if (pageIndex === -1) {
return {
data: null,
error: new Error(`Page ${pageNumber} not found`),
};
}
// Create updated pages array
const updatedPages = [...pagesData];
const updatedPage = { ...updatedPages[pageIndex] };
// Update the text fields if provided
if (storyText !== undefined) {
updatedPage.story_text = storyText;
}
// If German text is provided, update it
// Otherwise keep the existing German text
if (storyTextGerman !== undefined) {
updatedPage.story_text = storyTextGerman;
}
updatedPages[pageIndex] = updatedPage;
this.logger.log(`[StoryService] Successfully updated page ${pageNumber}`);
return {
data: updatedPages,
error: null,
};
} catch (error) {
this.logger.error('[StoryService] Error updating page text:', error);
return {
data: null,
error: new Error(
error instanceof Error ? error.message : String(error),
),
};
if (!pagesData || !Array.isArray(pagesData)) {
return err(ValidationError.invalidInput('pagesData', 'Invalid pages data'));
}
// Find the page to update
const pageIndex = pagesData.findIndex(
(page) => page.page_number === pageNumber,
);
if (pageIndex === -1) {
return err(NotFoundError.resource('Page', String(pageNumber)));
}
// Create updated pages array
const updatedPages = [...pagesData];
const updatedPage = { ...updatedPages[pageIndex] };
// Update the text fields if provided
if (storyText !== undefined) {
updatedPage.story_text = storyText;
}
// If German text is provided, update it
// Otherwise keep the existing German text
if (storyTextGerman !== undefined) {
updatedPage.story_text = storyTextGerman;
}
updatedPages[pageIndex] = updatedPage;
this.logger.log(`[StoryService] Successfully updated page ${pageNumber}`);
return ok(updatedPages);
}
}