@tanstack/cta-framework-react-cra 0.45.0 → 0.46.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/add-ons/ai/assets/src/routes/demo/ai-chat.tsx +2 -2
- package/add-ons/prisma/assets/prisma/seed.ts.ejs +2 -2
- package/add-ons/prisma/assets/src/db.ts.ejs +2 -2
- package/add-ons/prisma/package.json.ejs +5 -5
- package/add-ons/sentry/package.json +2 -1
- package/add-ons/start/assets/vite.config.ts.ejs +7 -1
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ResumeAssistantButton.tsx +20 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +30 -0
- package/examples/resume/package.json +27 -0
- package/package.json +2 -2
- package/tests/snapshots/react-cra/cr-ts-start-apollo-client-npm.json +1 -1
- package/tests/snapshots/react-cra/cr-ts-start-npm.json +1 -1
- package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
className={cn("leading-none font-semibold", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="card-description"
|
|
45
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="card-action"
|
|
55
|
+
className={cn(
|
|
56
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-content"
|
|
68
|
+
className={cn("px-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
Card,
|
|
86
|
+
CardHeader,
|
|
87
|
+
CardFooter,
|
|
88
|
+
CardTitle,
|
|
89
|
+
CardAction,
|
|
90
|
+
CardDescription,
|
|
91
|
+
CardContent,
|
|
92
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
3
|
+
import { CheckIcon } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Checkbox({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<CheckboxPrimitive.Root
|
|
13
|
+
data-slot="checkbox"
|
|
14
|
+
className={cn(
|
|
15
|
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<CheckboxPrimitive.Indicator
|
|
21
|
+
data-slot="checkbox-indicator"
|
|
22
|
+
className="flex items-center justify-center text-current transition-none"
|
|
23
|
+
>
|
|
24
|
+
<CheckIcon className="size-3.5" />
|
|
25
|
+
</CheckboxPrimitive.Indicator>
|
|
26
|
+
</CheckboxPrimitive.Root>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Checkbox }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
11
|
+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function HoverCardTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
17
|
+
return (
|
|
18
|
+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function HoverCardContent({
|
|
23
|
+
className,
|
|
24
|
+
align = "center",
|
|
25
|
+
sideOffset = 4,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
28
|
+
return (
|
|
29
|
+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
|
30
|
+
<HoverCardPrimitive.Content
|
|
31
|
+
data-slot="hover-card-content"
|
|
32
|
+
align={align}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
className={cn(
|
|
35
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</HoverCardPrimitive.Portal>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Separator({
|
|
7
|
+
className,
|
|
8
|
+
orientation = "horizontal",
|
|
9
|
+
decorative = true,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SeparatorPrimitive.Root
|
|
14
|
+
data-slot="separator-root"
|
|
15
|
+
decorative={decorative}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Separator }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchServerSentEvents,
|
|
3
|
+
useChat,
|
|
4
|
+
createChatClientOptions,
|
|
5
|
+
} from '@tanstack/ai-react'
|
|
6
|
+
import type { InferChatMessages } from '@tanstack/ai-react'
|
|
7
|
+
|
|
8
|
+
// Default chat options for type inference
|
|
9
|
+
const defaultChatOptions = createChatClientOptions({
|
|
10
|
+
connection: fetchServerSentEvents('/api/resume-chat'),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type ResumeChatMessages = InferChatMessages<typeof defaultChatOptions>
|
|
14
|
+
|
|
15
|
+
export const useResumeChat = () => {
|
|
16
|
+
const chatOptions = createChatClientOptions({
|
|
17
|
+
connection: fetchServerSentEvents('/api/resume-chat'),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return useChat(chatOptions)
|
|
21
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { allJobs, allEducations } from 'content-collections'
|
|
5
|
+
|
|
6
|
+
// Tool definition for getting jobs by skill
|
|
7
|
+
export const getJobsBySkillToolDef = toolDefinition({
|
|
8
|
+
name: 'getJobsBySkill',
|
|
9
|
+
description:
|
|
10
|
+
'Find all jobs where the candidate used a specific technology or skill. Use this to check if the candidate has experience with particular technologies.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
skill: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe(
|
|
15
|
+
'The skill or technology to search for (e.g., "React", "TypeScript", "Leadership")',
|
|
16
|
+
),
|
|
17
|
+
}),
|
|
18
|
+
outputSchema: z.array(
|
|
19
|
+
z.object({
|
|
20
|
+
jobTitle: z.string(),
|
|
21
|
+
company: z.string(),
|
|
22
|
+
location: z.string(),
|
|
23
|
+
startDate: z.string(),
|
|
24
|
+
endDate: z.string().optional(),
|
|
25
|
+
summary: z.string(),
|
|
26
|
+
tags: z.array(z.string()),
|
|
27
|
+
content: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Server implementation
|
|
33
|
+
export const getJobsBySkill = getJobsBySkillToolDef.server(({ skill }) => {
|
|
34
|
+
return allJobs.filter((job) =>
|
|
35
|
+
job.tags.some((tag) => tag.toLowerCase().includes(skill.toLowerCase())),
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Tool definition for getting all jobs
|
|
40
|
+
export const getAllJobsToolDef = toolDefinition({
|
|
41
|
+
name: 'getAllJobs',
|
|
42
|
+
description:
|
|
43
|
+
'Get a complete list of all work experience with full details including job titles, companies, dates, summaries, and skills. Use this to get an overview of the candidate\'s entire work history.',
|
|
44
|
+
inputSchema: z.object({}),
|
|
45
|
+
outputSchema: z.array(
|
|
46
|
+
z.object({
|
|
47
|
+
jobTitle: z.string(),
|
|
48
|
+
company: z.string(),
|
|
49
|
+
location: z.string(),
|
|
50
|
+
startDate: z.string(),
|
|
51
|
+
endDate: z.string().optional(),
|
|
52
|
+
summary: z.string(),
|
|
53
|
+
tags: z.array(z.string()),
|
|
54
|
+
content: z.string(),
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Server implementation
|
|
60
|
+
export const getAllJobs = getAllJobsToolDef.server(() => {
|
|
61
|
+
return allJobs.map((job) => ({
|
|
62
|
+
jobTitle: job.jobTitle,
|
|
63
|
+
company: job.company,
|
|
64
|
+
location: job.location,
|
|
65
|
+
startDate: job.startDate,
|
|
66
|
+
endDate: job.endDate,
|
|
67
|
+
summary: job.summary,
|
|
68
|
+
tags: job.tags,
|
|
69
|
+
content: job.content,
|
|
70
|
+
}))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Tool definition for getting all education
|
|
74
|
+
export const getAllEducationToolDef = toolDefinition({
|
|
75
|
+
name: 'getAllEducation',
|
|
76
|
+
description:
|
|
77
|
+
'Get a complete list of all education history including schools, programs, dates, and skills learned. Use this to understand the candidate\'s educational background.',
|
|
78
|
+
inputSchema: z.object({}),
|
|
79
|
+
outputSchema: z.array(
|
|
80
|
+
z.object({
|
|
81
|
+
school: z.string(),
|
|
82
|
+
summary: z.string(),
|
|
83
|
+
startDate: z.string(),
|
|
84
|
+
endDate: z.string().optional(),
|
|
85
|
+
tags: z.array(z.string()),
|
|
86
|
+
content: z.string(),
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Server implementation
|
|
92
|
+
export const getAllEducation = getAllEducationToolDef.server(() => {
|
|
93
|
+
return allEducations.map((education) => ({
|
|
94
|
+
school: education.school,
|
|
95
|
+
summary: education.summary,
|
|
96
|
+
startDate: education.startDate,
|
|
97
|
+
endDate: education.endDate,
|
|
98
|
+
tags: education.tags,
|
|
99
|
+
content: education.content,
|
|
100
|
+
}))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Tool definition for searching experience
|
|
104
|
+
export const searchExperienceToolDef = toolDefinition({
|
|
105
|
+
name: 'searchExperience',
|
|
106
|
+
description:
|
|
107
|
+
'Search for jobs by keywords in the job title, company name, summary, or content. Use this to find specific types of experience or roles.',
|
|
108
|
+
inputSchema: z.object({
|
|
109
|
+
query: z
|
|
110
|
+
.string()
|
|
111
|
+
.describe(
|
|
112
|
+
'The search query (e.g., "senior", "lead", "frontend", "startup")',
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
115
|
+
outputSchema: z.array(
|
|
116
|
+
z.object({
|
|
117
|
+
jobTitle: z.string(),
|
|
118
|
+
company: z.string(),
|
|
119
|
+
location: z.string(),
|
|
120
|
+
startDate: z.string(),
|
|
121
|
+
endDate: z.string().optional(),
|
|
122
|
+
summary: z.string(),
|
|
123
|
+
tags: z.array(z.string()),
|
|
124
|
+
matchedIn: z
|
|
125
|
+
.array(z.string())
|
|
126
|
+
.describe('Which fields matched the search'),
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Server implementation
|
|
132
|
+
export const searchExperience = searchExperienceToolDef.server(({ query }) => {
|
|
133
|
+
const lowerQuery = query.toLowerCase()
|
|
134
|
+
|
|
135
|
+
return allJobs
|
|
136
|
+
.map((job) => {
|
|
137
|
+
const matchedIn: string[] = []
|
|
138
|
+
|
|
139
|
+
if (job.jobTitle.toLowerCase().includes(lowerQuery)) {
|
|
140
|
+
matchedIn.push('job title')
|
|
141
|
+
}
|
|
142
|
+
if (job.company.toLowerCase().includes(lowerQuery)) {
|
|
143
|
+
matchedIn.push('company')
|
|
144
|
+
}
|
|
145
|
+
if (job.summary.toLowerCase().includes(lowerQuery)) {
|
|
146
|
+
matchedIn.push('summary')
|
|
147
|
+
}
|
|
148
|
+
if (job.content.toLowerCase().includes(lowerQuery)) {
|
|
149
|
+
matchedIn.push('description')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { job, matchedIn }
|
|
153
|
+
})
|
|
154
|
+
.filter(({ matchedIn }) => matchedIn.length > 0)
|
|
155
|
+
.map(({ job, matchedIn }) => ({
|
|
156
|
+
jobTitle: job.jobTitle,
|
|
157
|
+
company: job.company,
|
|
158
|
+
location: job.location,
|
|
159
|
+
startDate: job.startDate,
|
|
160
|
+
endDate: job.endDate,
|
|
161
|
+
summary: job.summary,
|
|
162
|
+
tags: job.tags,
|
|
163
|
+
matchedIn,
|
|
164
|
+
}))
|
|
165
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { chat, maxIterations, toServerSentEventsResponse } from '@tanstack/ai'
|
|
3
|
+
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
4
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
5
|
+
import { geminiText } from '@tanstack/ai-gemini'
|
|
6
|
+
import { ollamaText } from '@tanstack/ai-ollama'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getJobsBySkill,
|
|
10
|
+
getAllJobs,
|
|
11
|
+
getAllEducation,
|
|
12
|
+
searchExperience,
|
|
13
|
+
} from '@/lib/resume-tools'
|
|
14
|
+
|
|
15
|
+
export const Route = createFileRoute('/api/resume-chat')({
|
|
16
|
+
server: {
|
|
17
|
+
handlers: {
|
|
18
|
+
POST: async ({ request }) => {
|
|
19
|
+
const requestSignal = request.signal
|
|
20
|
+
|
|
21
|
+
if (requestSignal.aborted) {
|
|
22
|
+
return new Response(null, { status: 499 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const abortController = new AbortController()
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const body = await request.json()
|
|
29
|
+
const { messages } = body
|
|
30
|
+
const data = body.data || {}
|
|
31
|
+
|
|
32
|
+
const SYSTEM_PROMPT = `You are a helpful resume assistant helping recruiters and hiring managers evaluate if this candidate is a good fit for their job requirements.
|
|
33
|
+
|
|
34
|
+
CAPABILITIES:
|
|
35
|
+
1. Use getJobsBySkill to find jobs where the candidate used specific technologies or skills
|
|
36
|
+
2. Use getAllJobs to get the candidate's complete work history with all details
|
|
37
|
+
3. Use getAllEducation to get the candidate's educational background
|
|
38
|
+
4. Use searchExperience to search for specific types of roles or experience by keywords
|
|
39
|
+
|
|
40
|
+
INSTRUCTIONS:
|
|
41
|
+
- When asked about specific technologies or skills, use getJobsBySkill to find relevant experience
|
|
42
|
+
- When asked about overall experience or career progression, use getAllJobs
|
|
43
|
+
- When asked about education or training, use getAllEducation
|
|
44
|
+
- When asked about specific types of roles (e.g., "senior", "lead"), use searchExperience
|
|
45
|
+
- Be professional, concise, and helpful in your responses
|
|
46
|
+
- Provide specific details from the resume when available
|
|
47
|
+
- When calculating years of experience, consider the date ranges provided
|
|
48
|
+
- If the candidate has experience with something, highlight specific roles and time periods
|
|
49
|
+
- If the candidate lacks certain experience, be honest but constructive
|
|
50
|
+
|
|
51
|
+
CONTEXT: You are helping evaluate this candidate's qualifications for potential job opportunities.`
|
|
52
|
+
|
|
53
|
+
// Determine the best available provider
|
|
54
|
+
let provider: 'anthropic' | 'openai' | 'gemini' | 'ollama' = data.provider || 'ollama'
|
|
55
|
+
let model: string = data.model || 'mistral:7b'
|
|
56
|
+
|
|
57
|
+
// Use the first available provider with an API key, fallback to ollama
|
|
58
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
59
|
+
provider = 'anthropic'
|
|
60
|
+
model = 'claude-haiku-4-5'
|
|
61
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
62
|
+
provider = 'openai'
|
|
63
|
+
model = 'gpt-4o'
|
|
64
|
+
} else if (process.env.GEMINI_API_KEY) {
|
|
65
|
+
provider = 'gemini'
|
|
66
|
+
model = 'gemini-2.0-flash-exp'
|
|
67
|
+
}
|
|
68
|
+
// else keep ollama as default
|
|
69
|
+
|
|
70
|
+
// Adapter factory pattern for multi-vendor support
|
|
71
|
+
const adapterConfig = {
|
|
72
|
+
anthropic: () =>
|
|
73
|
+
anthropicText((model || 'claude-haiku-4-5') as any),
|
|
74
|
+
openai: () => openaiText((model || 'gpt-4o') as any),
|
|
75
|
+
gemini: () => geminiText((model || 'gemini-2.0-flash-exp') as any),
|
|
76
|
+
ollama: () => ollamaText((model || 'mistral:7b') as any),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const adapter = adapterConfig[provider]()
|
|
80
|
+
|
|
81
|
+
const stream = chat({
|
|
82
|
+
adapter,
|
|
83
|
+
tools: [getJobsBySkill, getAllJobs, getAllEducation, searchExperience],
|
|
84
|
+
systemPrompts: [SYSTEM_PROMPT],
|
|
85
|
+
agentLoopStrategy: maxIterations(5),
|
|
86
|
+
messages,
|
|
87
|
+
abortController,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
console.error('Resume chat error:', error)
|
|
93
|
+
if (error.name === 'AbortError' || abortController.signal.aborted) {
|
|
94
|
+
return new Response(null, { status: 499 })
|
|
95
|
+
}
|
|
96
|
+
return new Response(
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
error: 'Failed to process chat request',
|
|
99
|
+
message: error.message,
|
|
100
|
+
}),
|
|
101
|
+
{
|
|
102
|
+
status: 500,
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
})
|