1- ' use server' ;
1+ " use server" ;
22
3- import { createStreamableValue } from 'ai/rsc' ;
4- import { streamText } from 'ai' ;
5- import { createOpenAI , openai } from '@ai-sdk/openai' ;
6- import { MODELS } from '../helpers/Models' ;
7- import { headers } from 'next/headers' ;
8- import { ResumeGeneratorLimiter } from '@/helpers/rate-limiter' ;
3+ import { createStreamableValue } from "ai/rsc" ;
4+ import { streamText } from "ai" ;
5+ import { createOpenAI , openai } from "@ai-sdk/openai" ;
6+ import { MODELS } from "../helpers/Models" ;
7+ import { headers } from "next/headers" ;
8+ import { ResumeGeneratorLimiter } from "@/helpers/rate-limiter" ;
9+ import { getIp } from "@/helpers/commons/server" ;
10+ import path from "path" ;
11+ import { promises as fs } from "fs" ;
12+
13+ export type Education = {
14+ degree : string ;
15+ school : string ;
16+ startYear ?: string ;
17+ endYear ?: string ;
18+ description ?: string ;
19+ } ;
20+
21+ export type Experience = {
22+ company : string ;
23+ position : string ;
24+ startDate : string ;
25+ endDate : string ;
26+ description : string ;
27+ } ;
28+
29+ export type Social = {
30+ platform : string ;
31+ url : string ;
32+ } ;
933
1034export type BasicResumeInfo = {
11- name : string
12- email : string
13- role : string
14- description ?: string
15- dob ?: string
16- social ?: {
17- linkedin ?: string
18- github ?: string
19- [ key : string ] : string | undefined
20- }
21- education ?: {
22- degree : string
23- school : string
24- startYear ?: string
25- endYear ?: string
26- description ?: string
27- } [ ]
28- [ key : string ] : string | object | undefined
29- }
35+ name : string ;
36+ email : string ;
37+ role : string ;
38+ description ?: string ;
39+ dob ?: string ;
40+ social ?: Social [ ] ;
41+ education ?: Education [ ] ;
42+ experience ?: Experience [ ] ;
43+ [ key : string ] : string | object | undefined ;
44+ } ;
3045
3146export type CustomResponse = {
32- status : "error" | "success"
33- message : any
34- }
47+ status : "error" | "success" ;
48+ message : any ;
49+ } ;
3550
3651const groq = createOpenAI ( {
3752 baseURL : "https://api.groq.com/openai/v1" ,
3853 apiKey : process . env . GROQ_API_KEY ,
3954} ) ;
4055
41-
42- function getIp ( ) {
43- let forwardFor = headers ( ) . get ( 'x-forwarded-for' )
44- let ip = headers ( ) . get ( 'x-real-ip' )
45-
46- if ( forwardFor ) {
47- return forwardFor . split ( ',' ) [ 0 ] . trim ( )
56+ export async function generateResume (
57+ resumeInfo : BasicResumeInfo
58+ ) : Promise < CustomResponse > {
59+ try {
60+ const ip = getIp ( ) ?? "localhost" ;
61+ const rl = await ResumeGeneratorLimiter . limit ( ip ) ;
62+ if ( ! rl . success ) {
63+ throw new Error (
64+ "You have exceeded the rate limit for this action. Please try again later."
65+ ) ;
4866 }
49- if ( ip ) {
50- return ip . trim ( )
67+
68+ const message = generateResumeInfoMessage ( resumeInfo ) ;
69+ const modelDefinition = MODELS . find ( ( model ) => model . provider === "groq" ) ;
70+ if ( ! modelDefinition ) {
71+ throw new Error ( "Model not found for groq" ) ;
5172 }
52- return null
73+ const modelProvider = modelDefinition . provider === "groq" ? groq : openai ;
74+ const model = modelProvider ( modelDefinition . id ) ;
75+
76+ console . log ( message ) ;
77+ const promptFilePath = path . join (
78+ process . cwd ( ) ,
79+ "prompts" ,
80+ process . env . RESUME_SYSTEM_PROMPT ?? 'resume.txt'
81+ ) ;
82+ const systemPrompt = await fs . readFile ( promptFilePath , "utf-8" ) ;
83+ const result = await streamText ( {
84+ model,
85+ prompt : message ,
86+ system : systemPrompt ,
87+ } ) ;
88+
89+ const stream = createStreamableValue ( result . textStream ) ;
90+ return {
91+ status : "success" ,
92+ message : stream . value ,
93+ } ;
94+ } catch ( error ) {
95+ console . error ( error ) ;
96+ return {
97+ status : "error" ,
98+ message : ( error as Error ) . message ,
99+ } ;
100+ }
53101}
54102
103+ function generateResumeInfoMessage ( resumeInfo : BasicResumeInfo ) : string {
104+ let message = "Today`s Date is: " + new Date ( ) . toDateString ( ) + ". " ;
105+
106+ // Use switch case to handle different keys
107+ for ( const key in resumeInfo ) {
108+ const value = resumeInfo [ key ] ;
109+ if ( ! value || value . toString ( ) . trim ( ) === "" ) {
110+ continue ;
111+ }
112+
113+ switch ( key ) {
114+ case "name" :
115+ message += `I am ${ value } . ` ;
116+ break ;
117+
118+ case "email" :
119+ message += `My email is ${ value } . ` ;
120+ break ;
121+
122+ case "dob" :
123+ message += `I was born on ${ value } . ` ;
124+ break ;
125+
126+ case "location" :
127+ message += `I am located in ${ value } . ` ;
128+ break ;
129+
130+ case "social" :
131+ message += `You can find my social profiles platforms url(or username): ${ JSON . stringify (
132+ value
133+ ) } . `;
134+ break ;
135+
136+ case "description" :
137+ message += `Here is a brief description about me: ${ value } . ` ;
138+ break ;
55139
56- export async function generateResume ( resumeInfo : BasicResumeInfo ) : Promise < CustomResponse > {
57- try {
58- const ip = getIp ( ) ?? 'localhost'
59- const rl = await ResumeGeneratorLimiter . limit ( ip )
60- if ( ! rl . success ) {
61- throw new Error ( "You have exceeded the rate limit for this action. Please try again later." )
62- }
63- const message = generateResumeInfoMessage ( resumeInfo )
64- const modelDefinition = MODELS . find ( model => model . provider === 'groq' ) ;
65- if ( ! modelDefinition ) {
66- throw new Error ( "Model not found for groq" )
67- }
68- const modelProvider = modelDefinition . provider === "groq" ? groq : openai ;
69- const model = modelProvider ( modelDefinition . id ) ;
70- console . log ( message )
71- const result = await streamText ( {
72- model,
73- prompt : message ,
74- system : `You are an AI named quickcv that generates resumes in markdown format with a high ATS score. Only return the resume content in markdown format using the provided data. Do not include any greetings, confirmations, errors, or pleasantries. The output should only be the resume itself. Also try quantifying the resume with numbers and percentages where possible and if enough data is provided. Use the best verbs and adjectives to describe the person's skills and achievements. The resume should be targeted at the role and company provided in the data. Also try to make use of bullet points and lists where possible. Avoid using any personal pronouns and too much repetition.`
140+ case "education" :
141+ message += `Here is my educational background: ` ;
142+ ( value as Education [ ] ) . forEach ( ( edu ) => {
143+ message += `I have a ${ edu . degree } from ${ edu . school } . ` ;
144+ if ( edu . startYear ) message += `I started in ${ edu . startYear } . ` ;
145+ if ( edu . endYear ) message += `and finished in ${ edu . endYear } . ` ;
146+ if ( edu . description ) message += `${ edu . description } . ` ;
75147 } ) ;
148+ break ;
76149
77- const stream = createStreamableValue ( result . textStream ) ;
78- return {
79- status : "success" ,
80- message : stream . value
81- }
82- } catch ( error ) {
83- console . error ( error )
84- return {
85- status : "error" ,
86- message : ( error as Error ) . message
87- }
88- }
89- }
150+ case "experience" :
151+ message += `I have experience working at the following companies: ` ;
152+ ( value as Experience [ ] ) . forEach ( ( exp ) => {
153+ message += `I worked at ${ exp . company } as a ${ exp . position } ` ;
154+ if ( exp . startDate ) message += ` from ${ exp . startDate } ` ;
155+ if ( exp . endDate ) message += ` to ${ exp . endDate } ` ;
156+ if ( exp . description )
157+ message += `Here is a brief description of my role: ${ exp . description } . ` ;
158+ } ) ;
159+ break ;
90160
161+ case "role" :
162+ message += `This resume is for the role of ${ value } . ` ;
163+ break ;
91164
92- function generateResumeInfoMessage ( resumeInfo : BasicResumeInfo ) : string {
93- let message = ``
94- for ( const key in resumeInfo ) {
95-
96- if ( ! resumeInfo [ key ] || resumeInfo [ key ] ?. toString ( ) . trim ( ) === '' ) {
97- continue ;
98- }
99- // if name start with i am or i'm {name}
100- if ( key === 'name' ) {
101- message += `I am ${ resumeInfo [ key ] } . `
102- }
103- // if email and my email is {email}
104- if ( key === 'email' ) {
105- message += `My email is ${ resumeInfo [ key ] } . `
106- }
107- // for date of birth
108- if ( key === 'dob' ) {
109- message += `I was born on ${ resumeInfo [ key ] } . `
110- }
111- // if socials are present add them by stringifying the object
112- if ( key === 'social' ) {
113- message += `I can be found on ${ JSON . stringify ( resumeInfo [ key ] ) } . `
114- }
115- // for role
116- if ( key === 'role' ) {
117- message += `I am a ${ resumeInfo [ key ] } . `
118- }
119- // for description
120- if ( key === 'description' ) {
121- message += `${ resumeInfo [ key ] } . `
122- }
123- // for education
124- if ( key === 'education' ) {
125- message += `Here is my educational background. `
126- for ( const edu of ( resumeInfo [ 'education' ] ?? [ ] ) ) {
127- message += `I have a ${ edu . degree } from ${ edu . school } . `
128- if ( edu . startYear ) {
129- message += `I started in ${ edu . startYear } . `
130- }
131- if ( edu . endYear ) {
132- message += `and finished in ${ edu . endYear } . `
133- }
134- if ( edu . description ) {
135- message += `${ edu . description } . `
136- }
137- }
138- }
139-
140- if ( key === 'targetCompany' ) {
141- message += `I want this resume to be targeted at me getting a job at ${ resumeInfo [ key ] } . `
142- }
165+ case "targetCompany" :
166+ message += `I want this resume to be tailored for a position at ${ value } . ` ;
167+ break ;
168+
169+ default :
170+ // Stringify and add any other unhandled keys
171+ message += `For this key "${ key } ", here is my info: ${ JSON . stringify (
172+ value
173+ ) } . `;
174+ break ;
143175 }
144- return message ;
145- }
176+ }
177+
178+ return message ;
179+ }
0 commit comments