-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from Tutortoise/feat/recommendation
feat(recommendation): integration with recommender service
- Loading branch information
Showing
10 changed files
with
418 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { RECOMMENDATION_ENABLED, RECOMMENDATION_URL } from "@/config"; | ||
import { | ||
RemoteRecommendationService, | ||
InterestRecommendationService, | ||
} from "./recommendation.service"; | ||
import { logger } from "@middleware/logging.middleware"; | ||
import axios from "axios"; | ||
import { LearnerRepository } from "../learner/learner.repository"; | ||
import { TutoriesRepository } from "../tutories/tutories.repository"; | ||
|
||
export const createRecommendationService = ( | ||
learnerRepository: LearnerRepository, | ||
tutoriesRepository: TutoriesRepository, | ||
) => { | ||
if (process.env.NODE_ENV === "test" || !RECOMMENDATION_ENABLED) { | ||
logger.info( | ||
"Recommendation service is disabled, using simple interest-matching service", | ||
); | ||
return new InterestRecommendationService( | ||
learnerRepository, | ||
tutoriesRepository, | ||
); | ||
} | ||
|
||
const service = new RemoteRecommendationService(RECOMMENDATION_URL!); | ||
|
||
try { | ||
axios | ||
.get(`${RECOMMENDATION_URL}/health`, { | ||
timeout: 5000, | ||
}) | ||
.catch(() => { | ||
throw new Error("Health check failed"); | ||
}); | ||
|
||
logger.info("Recommendation service is healthy and ready"); | ||
return service; | ||
} catch (error) { | ||
logger.warn( | ||
`Recommendation service is unavailable: ${error}, falling back to simple interest-matching service`, | ||
); | ||
return new InterestRecommendationService( | ||
learnerRepository, | ||
tutoriesRepository, | ||
); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { TutorAvailability } from "@/db/schema"; | ||
import { LearningStyle } from "../learner/learner.types"; | ||
|
||
export interface Recommendation { | ||
tutor_id: string; | ||
tutories_id: string; | ||
name: string; | ||
email: string; | ||
city: string | null; | ||
district: string | null; | ||
category: string; | ||
tutory_name: string; | ||
about: string; | ||
methodology: string; | ||
hourly_rate: number; | ||
type_lesson: string; | ||
completed_orders: number; | ||
total_orders: number; | ||
match_reasons?: string[]; | ||
location_match: boolean; | ||
availability: TutorAvailability | null; | ||
} | ||
|
||
export interface RecommendationServiceResponse { | ||
learner: { | ||
id: string; | ||
name: string; | ||
email: string; | ||
learning_style: LearningStyle | null; | ||
city: string | null; | ||
district: string | null; | ||
interests: string[]; | ||
}; | ||
recommendations: Recommendation[]; | ||
total_found: number; | ||
requested: number; | ||
} | ||
|
||
export interface RecommendationService { | ||
getRecommendations(learnerId: string): Promise<RecommendationServiceResponse>; | ||
trackInteraction(learnerId: string, tutoriesId: string): Promise<void>; | ||
checkHealth(): Promise<boolean>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import axios from "axios"; | ||
import { | ||
Recommendation, | ||
RecommendationService, | ||
RecommendationServiceResponse, | ||
} from "./recommendation.interface"; | ||
import { logger } from "@middleware/logging.middleware"; | ||
import { TutoriesRepository } from "../tutories/tutories.repository"; | ||
import { LearnerRepository } from "../learner/learner.repository"; | ||
|
||
export class RemoteRecommendationService implements RecommendationService { | ||
constructor(private readonly baseUrl: string) {} | ||
|
||
async trackInteraction(learnerId: string, tutoriesId: string): Promise<void> { | ||
try { | ||
axios.get(`${this.baseUrl}/interaction/${learnerId}/${tutoriesId}`); | ||
} catch (error) { | ||
logger.error("Recommendation service error:", error); | ||
throw new Error("Recommendation service unavailable"); | ||
} | ||
} | ||
|
||
async getRecommendations( | ||
learnerId: string, | ||
): Promise<RecommendationServiceResponse> { | ||
try { | ||
const response = await axios.get<RecommendationServiceResponse>( | ||
`${this.baseUrl}/recommendations/${learnerId}`, | ||
{ timeout: 5000 }, | ||
); | ||
|
||
return response.data; | ||
} catch (error) { | ||
logger.error("Recommendation service error:", error); | ||
throw new Error("Recommendation service unavailable"); | ||
} | ||
} | ||
|
||
checkHealthSync(): boolean { | ||
try { | ||
const xhr = new XMLHttpRequest(); | ||
xhr.open("GET", `${this.baseUrl}/health`, false); // Make the request synchronous | ||
xhr.timeout = 5000; | ||
xhr.send(null); | ||
|
||
return xhr.status === 200; | ||
} catch (error) { | ||
logger.warn("Health check failed:", error); | ||
return false; | ||
} | ||
} | ||
|
||
async checkHealth(): Promise<boolean> { | ||
try { | ||
const response = await axios.get(`${this.baseUrl}/health`, { | ||
timeout: 5000, | ||
}); | ||
return response.data.status === "healthy"; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
export class InterestRecommendationService implements RecommendationService { | ||
constructor( | ||
private readonly learnerRepository: LearnerRepository, | ||
private readonly tutoriesRepository: TutoriesRepository, | ||
) {} | ||
|
||
async trackInteraction(learnerId: string, tutoriesId: string): Promise<void> { | ||
// No-op | ||
} | ||
|
||
async getRecommendations( | ||
learnerId: string, | ||
): Promise<RecommendationServiceResponse> { | ||
const learner = await this.learnerRepository.getLearnerById(learnerId); | ||
if (!learner) { | ||
throw new Error("Learner not found"); | ||
} | ||
|
||
const recommendations: Recommendation[] = | ||
await this.tutoriesRepository.getTutoriesByLearnerInterests(learnerId); | ||
|
||
const result: RecommendationServiceResponse = { | ||
learner: { | ||
id: learner.id, | ||
name: learner.name, | ||
email: learner.email, | ||
learning_style: learner.learningStyle, | ||
city: learner.city, | ||
district: learner.district, | ||
interests: learner.interests, | ||
}, | ||
recommendations, | ||
total_found: 0, | ||
requested: 0, | ||
}; | ||
|
||
return result; | ||
} | ||
|
||
checkHealthSync(): boolean { | ||
return true; | ||
} | ||
|
||
async checkHealth(): Promise<boolean> { | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.