mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
add mana core
This commit is contained in:
parent
ce71db2fc0
commit
754e87ebc0
112 changed files with 34765 additions and 548 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue