关注「索引目录」公众号,获取更多干货。
LangChain 最近推出了:一种构建结构化多智能体系统的新方法,该系统可以跨多个步骤进行规划、委派和推理。
它内置了规划功能、上下文文件系统和子代理生成功能。但令人惊讶的是,将该代理连接到真正的前端仍然非常困难。
今天,我们将构建一个由 Deep Agents 驱动的求职助手,并将其连接到使用的实时 Next.js UI ,以便前端与代理实时保持同步。
您将找到架构、关键模式、UI ↔ 代理之间的状态流动方式,以及从头开始构建此功能的逐步指南。
我们来建造它。
1. 什么是深度代理?
如今大多数智能体只是“循环中的 LLM + 工具”。这种方法虽然可行,但往往不够深入:没有明确的计划,长期执行能力弱,而且随着运行时间的延长,状态会变得混乱。
、和这样的流行智能体通过遵循一种常见的模式来解决这个问题:它们首先进行规划,将工作环境外部化(通常通过文件或 shell),并将孤立的工作片段委派给子智能体。
Deep Agents 将这些基本元素打包成一个可重用的代理运行时。
与其从头开始设计自己的代理循环,不如调用create_deep_agent(...)并获取一个预先配置好的执行图,该执行图已经知道如何在多个步骤中进行规划、委派和管理状态。
实际上,通过该方法创建的深度代理create_deep_agent本质上就是一个 LangGraph 图。它没有单独的运行时或隐藏的编排层。
这意味着标准的 LangGraph 功能可以直接使用:
流媒体
检查点和中断
人机回路控制
心智模型(其运行方式)
从概念上讲,执行流程如下所示:
User goal
↓
Deep Agent (LangGraph StateGraph)
├─ Plan: write_todos → updates "todos" in state
├─ Delegate: task(...) → runs a subagent with its own tool loop
├─ Context: ls/read_file/write_file/edit_file → persists working notes/artifacts
↓
Final answer
这样就为“计划→执行工作→存储中间工件→继续”提供了一个可用的结构,而无需发明自己的计划格式、内存层或委托协议。
您可以访问阅读更多内容并查看。
CopilotKit 的适用范围
深度代理会将关键部分(例如文件和消息)推送到显式状态todos,这使得运行情况的检查更加容易。这种显式状态也使得与集成成为可能。
CopilotKit 是一个前端运行时,它通过实时流式传输代理事件和状态更新(底层使用
这个中间件(CopilotKitMiddleware)使得前端能够在运行过程中与代理保持同步。您可以在阅读相关文档。
agent = create_deep_agent(
model="openai:gpt-4o",
tools=[get_weather],
middleware=[CopilotKitMiddleware()],
for frontend tools and context
system_prompt="You are a helpful research assistant."
)
下图显示了 UI 中的用户操作如何通过 AG-UI 发送到任何代理后端,以及响应如何作为标准化事件流回。
2. 核心组件
以下是我们稍后将用到的核心组件:
1) 规划工具(通过深度代理内置) - 内置规划/待办行为,以便代理可以将工作流程分解为步骤,而无需您编写单独的规划工具。
Conceptual example (not required in codebase)
@tool
def todo_write(tasks: List[str]) -> str:
formatted = "\n".join([f"- {task}" for task in tasks])
return f"Todo list created:\n{formatted}"
2) 子代理——让主代理将特定任务委派到独立的执行循环中。每个子代理都有自己的提示、工具和上下文。
subagents = [
{
"name": "job-search-agent",
"description": "Finds relevant jobs and outputs structured job candidates.",
"system_prompt": JOB_SEARCH_PROMPT,
"tools": [internet_search],
}
]
3) 工具——这是代理实际执行操作的方式。这里,finalize()表示完成信号。
@tool
def finalize() -> dict:
"""Signal that the agent is done."""
return {"status": "done"}
深度代理的实现方式(中间件)
如果您想知道create_deep_agent()LangGraph 代理是如何实际注入规划、文件和子代理的,答案是中间件。
每个功能都以单独的中间件形式实现。默认情况下,附加了三个中间件:
- 添加write_todos工具和指令,推动代理在多步骤任务期间明确地规划和更新实时待办事项列表。
- 添加文件工具(ls,,,),以便代理可以将笔记和工件外部化,而不是将所有内容都塞进聊天记录中read_file。write_fileedit_file
- 添加该task工具,允许主代理将工作委托给具有隔离上下文和自己提示/工具的子代理。
这使得 Deep Agents 无需引入新的运行时环境即可实现“预配置”的功能。如果您想深入了解,可以查看相关其中展示了具体的实现细节。
我们正在建造什么?
让我们创建一个代理,该代理可以:
接受简历(PDF格式),并提取技能和背景信息
使用深度代理来规划和协调子代理
使用工具(Tavily)在网络上搜索相关工作
通过 CopilotKit (AG-UI) 将工具结果流式传输回用户界面
在构建代理的过程中,我们将看到这些概念中的一些实际应用。
3. 前端:将代理连接到用户界面
我们先来构建前端部分。这就是我们的目录结构。
该src目录托管 Next.js 前端,包括 UI、共享组件和/api/copilotkit用于代理通信的 CopilotKit API 路由()。
.
├── src/ ← Next.js frontend
│ ├── app/
│ │ ├── page.tsx
│ │ ├── layout.tsx ← CopilotKit provider
│ │ └── api/
│ │ ├── upload-resume/route.ts ← upload endpoint
│ │ └── copilotkit/route.ts ← CopilotKit AG-UI runtime
│ ├── components/
│ │ ├── ChatPanel.tsx ← Chat + tool capture
│ │ ├── ResumeUpload.tsx ← PDF upload UI
│ │ ├── JobsResults.tsx ← Jobs table renderer
│ │ └── LivePreviewPanel.tsx
│ └── lib/
│ └── types.ts
├── package.json
├── next.config.ts
└── README.md
步骤 1:CopilotKit 提供程序和布局
安装必要的 CopilotKit 软件包。
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime
@copilotkit/react-core
提供核心 React hooks 和上下文,将您的 UI 连接到与 AG-UI 兼容的代理后端。
@copilotkit/react-ui
提供现成的 UI 组件,可快速构建 AI 聊天或助手界面。
@copilotkit/runtime
是服务器端运行时,它公开 API 端点,并使用 HTTP 和 SSE 将前端与外部 AG-UI 兼容的代理后端连接起来。
该组件必须包裹应用程序中与 Copilot 相关的部分。在大多数情况下,最好将其放置在整个应用程序周围,例如在layout.tsx.
import type { Metadata } from "next";
import { CopilotKit } from "@copilotkit/react-core";
import "./globals.css";
import "@copilotkit/react-ui/styles.css";
export const metadata: Metadata = {
title: "Job Finder | Deep Agents with CopilotKit",
description: "A job search assistant powered by Deep Agents and CopilotKit",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
{children}
);
}
这里runtimeUrl="/api/copilotkit"指向 CopilotKit 用于与代理后端通信的 Next.js API 路由。
每个页面都包含在这个上下文中,以便 UI 组件知道要调用哪个代理以及将请求发送到哪里。
步骤 2:Next.js API 路由:代理到 FastAPI
这个 Next.js API 路由充当浏览器和深度代理之间的轻量级代理。它:
接受来自用户界面的 CopilotKit 请求。
通过 AG-UI 将它们转发给代理。
将代理状态和事件流式传输回前端
所有请求都通过一个单一的端点,而不是让前端直接与 FastAPI 代理通信/api/copilotkit。
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest } from "next/server";
const serviceAdapter = new ExperimentalEmptyAdapter();
const runtime = new CopilotRuntime({
agents: {
job_application_assistant: new LangGraphHttpAgent({
url: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
}),
},
});
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
以下是对上述代码的简单解释:
上面的代码注册了job_application_assistant代理。
LangGraphHttpAgent
:定义了一个远程 LangGraph 代理端点。它指向运行在 FastAPI 上的 Deep Agents 后端。
ExperimentalEmptyAdapter
:当代理后端处理自己的 LLM 调用和编排时,使用简单的空操作适配器
copilotRuntimeNextJSAppRouterEndpoint
一个小型辅助函数,用于将 Copilot 运行时适配到 Next.js 应用路由 API 路由,并返回一个handleRequest函数。
步骤 3:恢复上传 API 端点
此 API 路由(src\app\api\upload-resume\route.ts)处理来自前端的简历上传请求,并将其转发到 FastAPI 后端。它:
接受来自浏览器的多部分文件上传
将文件代理到后端简历解析器
将提取的文本和技能返回给用户界面。
将简历解析放在后端可以让代理重用相同的逻辑,并保持前端轻量级。
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const backendFormData = new FormData();
backendFormData.append("file", file);
const backendUrl = process.env.BACKEND_URL || "http://localhost:8123";
const response = await fetch(`${backendUrl}/api/upload-resume`, {
method: "POST",
body: backendFormData,
});
if (!response.ok) {
throw new Error("Backend upload failed");
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Upload failed" },
{ status: 500 }
);
}
}
步骤 4:构建关键组件
由于整体代码量巨大,我只介绍每个组件背后的核心逻辑。您可以在代码仓库中找到所有src\components。
这些组件使用(如 useCopilotReadable)将所有内容连接在一起。
✅ 简历上传组件
该客户端组件处理简历选择并将文件转发到后端进行解析。
它接受一个 PDF/TXT 文件,将其 POST 到/api/upload-resume父组件,并将提取的文本和技能提升回父组件。
"use client";
import { useRef, useState } from "react";
type ResumeUploadResponse = { success: boolean; text: string; skills: string[]; filename: string };
export function ResumeUpload({ onUploadSuccess }: { onUploadSuccess(d: ResumeUploadResponse): void }) {
const [selectedFile, setSelectedFile] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const inputRef = useRef(null);
const onSelect = (e: React.ChangeEvent) => {
setError(null);
const f = e.target.files?.[0] ?? null;
if (f && !["application/pdf", "text/plain"].includes(f.type)) {
setSelectedFile(null);
setError("Please upload a PDF or TXT file");
e.target.value = ""; // allow re-selecting same file
return;
}
setSelectedFile(f);
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedFile) return;
setIsLoading(true);
setError(null);
try {
const fd = new FormData();
fd.append("file", selectedFile);
const res = await fetch("/api/upload-resume", { method: "POST", body: fd });
if (!res.ok) throw new Error("Upload failed");
onUploadSuccess((await res.json()) as ResumeUploadResponse);
setSelectedFile(null);
if (inputRef.current) inputRef.current.value = "";
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to upload resume");
} finally {
setIsLoading(false);
}
};
return (
{isLoading ? "Uploading..." : "Upload Resume"}
{error && {error}}
{/* ... UI/styling omitted ... */}
);
}
以下是简要说明:
接受用户输入的 PDF/TXT 文件
/api/upload-resume
使用以下方式发送文件FormData
从后端接收提取的文本和技能
通过某种方式提取数据onUploadSuccess,以便稍后将其注入代理程序。
中的完整代码。
✅ 聊天面板组件
这是连接用户、客服人员和工具输出的核心用户界面。聊天面板:
嵌入 CopilotChat 以处理对话输入和流式代理响应
用于useCopilotReadable将简历文本、检测到的技能和用户偏好持续同步到代理的上下文中
拦截工具调用(例如update_jobs_list),以更新本地 UI 状态,而无需将作业 JSON 输出到聊天窗口。
我们还使用该组件构建了对话式用户界面。
"use client";
import { useState, useRef } from "react";
import { useDefaultTool, useCopilotReadable } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
import { ResumeUpload } from "./ResumeUpload";
import { JobsResults } from "./JobsResults";
export function ChatPanel() {
// form + resume state (title, location, skills, resume text…)
const [jobs, setJobs] = useState([]);
const processedKeyRef = useRef(null); // dedupe repeated tool calls
// Capture tool output
useDefaultTool({
render: ({ name, status, args, result }) => {
if (name === "update_jobs_list" && status === "complete" && result?.jobs_list) {
const key = JSON.stringify({
len: result.jobs_list.length,
first: result.jobs_list[0]?.url,
});
if (processedKeyRef.current !== key) {
processedKeyRef.current = key;
// Avoid setState during render
queueMicrotask(() => {
setJobs(result.jobs_list);
});
}
}
// Render tool calls inline
return (
...
);
},
});
// Send UI state + resume data into agent context
useCopilotReadable({
description: "Job search preferences",
value: {
targetTitle,
targetLocation,
skillsHint,
resumeText,
detectedSkills,
},
});
return (
{/* Resume upload + extracted skills UI */}
{!resumeUploaded && }
{/* Job search inputs (title / location / skills) */}
{/* CopilotKit chat UI */}
{/* Structured output rendered outside chat */}
);
}
中的完整代码。
✅ 工作成果组成部分
这是一个纯粹的展示组件。它接收jobs数组(update_jobs_list完成后填充数据),并将其渲染成表格,从而保持聊天输出的简洁。
"use client";
import { JobPosting } from "@/lib/types";
export function JobsResults({ jobs }: { jobs: JobPosting[] }) {
if (!jobs.length) return null;
return (
Jobs
{/* Company | Title | Location | Link | Good match */}
{jobs.map((j, idx) => (
{j.company}
{j.title}
{j.location}
Open
{j.goodMatch || "Yes"}
))}
);
}
中的完整代码。
步骤 5:将聊天用户界面连接到代理
至此,所有组件均已就位。此页面仅用于渲染ChatPanel,它已通过 CopilotKit 与 Deep Agents 后端完全连接。
LivePreviewPanel旁边还安装了辅助面板。由于工具调用已经在面板内部内联渲染CopilotChat,因此该面板目前是可选的,它作为一个正在开发中的空间,用于实现更丰富的调试和可视化功能。
"use client";
import { ChatPanel } from "@/components/ChatPanel";
import { LivePreviewPanel } from "@/components/LivePreviewPanel";
export default function Page() {
return (
{/* App header (branding + description) */}
Job Application Assistant
Find personalized jobs with AI.
{/* ... badges / styling omitted ... */}
{/* Tool calls are already rendered inside CopilotChat */}
{/* This panel is optional and currently used for experimentation */}
{/* Footer */}
{/* ... footer content omitted ... */}
);
}
4. 后端:构建代理服务(FastAPI + Deep Agents + AG-UI)
接下来我们将构建托管我们深度代理的 FastAPI 后端。
该/agent目录下运行着一个 FastAPI 服务器,用于运行作业应用程序代理。以下是后端项目的结构。
.
├── agent/ ← Deep Agents backend
│ ├── main.py ← FastAPI + AG-UI endpoint
│ ├── agent.py ← Deep Agents graph & tools
│ ├── pyproject.toml ← Python deps (uv)
│ └── uv.lock
...
从宏观层面来看,后端负责:
公开一个与 CopilotKit 兼容的代理端点(用于流式传输代理状态和工具调用)
提供/api/upload-resume用于解析简历的接口
构建一个深度代理图,该代理图可以进行规划、将任务委派给子代理,并在网络上搜索匹配的工作。
后端使用进行依赖管理。如果您的系统中没有安装 uv,请安装它。
pip install uv
使用以下命令初始化一个新的 UV 项目。这将生成一个新的 UV 项目pyproject.toml。
cd agent
uv init
该后端使用的大多数 AI 工具(尤其是 AG-UI Strands)目前需要 Python 3.12 或更高版本,因此请确保使用以下命令告知 uv 使用兼容的 Python 版本:
uv python pin 3.12
然后安装依赖项。这也会创建项目的虚拟环境。
uv add copilotkit deepagents fastapi langchain langchain-openai pypdf python-dotenv python-multipart tavily-python "uvicorn[standard]"
copilotkit
:通过流式传输、工具和共享状态将代理连接到前端。
deepagents
:用于多步骤执行的规划优先代理框架。
fastapi
:一个公开代理 API 的 Web 框架。
langchain
代理和工具编排层。
langchain-openai
:将 OpenAI 模型集成到 LangChain 中。
pypdf
:从 PDF 文件中提取文本。
python-dotenv
:从以下位置加载环境变量.env
python-multipart
:启用 FastAPI 中的文件上传功能。
tavily-python
:网络搜索实时经纪人研究。
uvicorn[standard]
:用于运行 FastAPI 的 ASGI 服务器。
现在运行以下命令,生成一个uv.lock包含确切版本的文件。
uv sync
添加必要的 API 密钥
.env在这两个目录下分别创建一个文件agent,并将你的和添加到该文件中。我已经附上了文档链接,方便你参考。
OPENAI_API_KEY=sk-proj-...
TAVILY_API_KEY=tvly-dev-...
OPENAI_MODEL=gpt-4-turbo
OpenAI API密钥
Tavily API 密钥
步骤 1:定义智能体的行为
我们首先使用一个严格的系统提示来定义代理的行为agent.py。
在深度代理中,系统提示充当工作流的控制层,结合规划和委派,将复杂的任务分解为有序的步骤。
通过强制执行固定的执行顺序来协调MAIN_SYSTEM_PROMPT工具和子代理。此提示确保:
外部行动总是通过工具发生的。
UI状态以受控方式更新。
执行以确定性的方式结束finalize()
MAIN_SYSTEM_PROMPT = """
You are a tool-using agent.
Hard rules:
- Never include job details, URLs, or JSON in assistant messages.
- Only output jobs via update_jobs_list(jobs_json).
- A valid job must be a single job detail page on an ATS or company careers page.
- Do NOT use job boards or listing/search pages.
- company MUST be the hiring company (never Lever/Greenhouse/Ashby/Workday/Talent.com/etc).
Schema (exact keys):
- company, title, location, url, goodMatch
Steps:
1) Call internet_search(query) exactly once.
2) From the returned results, select up to 5 valid individual job postings.
3) Call update_jobs_list(jobs_json) once.
4) Call finalize().
5) Output: Found N jobs.
If you cannot find 5 valid jobs, return as many valid ones as possible.
"""
JOB_SEARCH_PROMPT定义了特定子代理的行为。其职责仅限于寻找相关工作并以受控格式返回结构化结果。
JOB_SEARCH_PROMPT = (
"Search and select 5 real postings that match the user's title, locations, and skills. "
"Output ONLY this block format (no extra text before/after the wrapper):\n"
"\n"
'[{"company":"...","title":"...","location":"...","link":"https://...","Good Match":"one sentence"},'
' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"one sentence"},'
' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"one sentence"},'
' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"one sentence"},'
' {"company":"...","title":"...","location":"...","link":"https://...","Good Match":"one sentence"}]'
"\n"
"Each job MUST:"
"- Be a single opening (not a job board, filter page or company jobs index)"
"- Belong to a specific company with a dedicated job description page"
"You must:"
"- Use internet_search to find relevant jobs."
"- Do NOT output job listings, JSON, or URLs in messages."
"- Return everything ONLY by calling the parent tool `update_jobs_list` with a JSON string."
)
步骤 2:添加简历解析和技能提取工具
我们使用以下函数从上传的 PDF 文件中提取原始文本pypdf。FastAPI 上传端点使用此函数将简历转换为纯文本。
def parse_pdf_resume(file_path: str) -> str:
with open(file_path, "rb") as file:
reader = PdfReader(file)
return "".join(page.extract_text() for page in reader.pages)
接下来,我们从简历中提取轻量级的结构化信号(语言、框架、工具)。这会影响求职查询和匹配质量。
def extract_skills_from_resume(resume_text: str) -> List[str]:
skills_db = {
"languages": ["Python", "JavaScript", "Go"],
"frameworks": ["React", "FastAPI", "Django"],
"cloud": ["AWS", "Docker", "Kubernetes"],
}
found = set()
text = resume_text.lower()
for skills in skills_db.values():
for skill in skills:
if skill.lower() in text:
found.add(skill)
return list(found)
步骤 3:定义搜索、UI 更新和终止工具
工具是代理与外部世界/用户界面之间的集成界面。
该internet_search工具负责发现真实的招聘信息。它会特意获取额外的搜索结果,过滤掉任何包含“不良”子字符串的URL(招聘网站/搜索页面),BAD_URL_SUBSTRINGS并只返回第一个max_results有效的搜索结果。
BAD_URL_SUBSTRINGS = [
"linkedin.com/jobs/search",
"linkedin.com/jobs/",
"builtin.com/jobs",
"naukri.com",
"glassdoor.",
"/jobs/search",
"/search?",
]
def _is_bad(url: str) -> bool:
u = (url or "").lower()
return any(p in u for p in BAD_URL_SUBSTRINGS)
@tool
def internet_search(query: str, max_results: int = 10) -> List[Dict[str, Any]]:
"""
Search for jobs using Tavily API. Always returns up to 5 results.
"""
tavily_key = os.environ.get("TAVILY_API_KEY")
if not tavily_key:
raise RuntimeError("TAVILY_API_KEY not set")
client = TavilyClient(api_key=tavily_key)
res = client.search(
query=query,
max_results=max_results * 3,
get more, then filter
include_raw_content=False,
topic="general",
)
trimmed = []
for r in res.get("results", []):
url = r.get("url") or ""
if _is_bad(url):
continue
trimmed.append(
{
"title": r.get("title"),
"url": url,
"content": (r.get("content") or "")[:400],
}
)
if len(trimmed) == max_results:
break
print(f"[SEARCH] Returning {len(trimmed)} filtered results")
print(trimmed)
return trimmed
该update_jobs_list工具是结构化工作数据到达前端的唯一途径,它能确保 UI 更新清晰明确,并将 JSON 数据排除在聊天消息之外。
@tool
def update_jobs_list(jobs_json: str) -> Dict[str, Any]:
"""Send jobs list to UI state."""
jobs = json.loads(jobs_json)
print(f"[TOOL] update_jobs_list: {len(jobs)} jobs")
return {"jobs_list": jobs}
finalize该工具发出信号,表明代理已完成其工作流程。
@tool
def finalize() -> dict:
"""Signal completion."""
print("[TOOL] finalize: Job search complete")
return {"status": "done"}
步骤 4:构建包含子代理的深度代理图
现在我们将所有内容连接起来,并构建深度代理图build_agent()。
def build_agent():
"""Build Deep Agents graph with proper recursion limit"""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("Missing OPENAI_API_KEY")
llm = ChatOpenAI(
model=os.environ.get("OPENAI_MODEL", "gpt-4-turbo"),
temperature=0.7,
api_key=api_key,
)
tools = [
internet_search,
update_jobs_list,
finalize,
]
subagents = [
{
"name": "job-search-agent",
"description": "Finds relevant jobs and outputs JSON.",
"system_prompt": JOB_SEARCH_PROMPT,
"tools": [internet_search],
},
]
agent_graph = create_deep_agent(
model=llm,
system_prompt=MAIN_SYSTEM_PROMPT,
tools=tools,
subagents=subagents,
middleware=[CopilotKitMiddleware()],
checkpointer=MemorySaver(),
)
print("[AGENT] Deep Agents graph created")
print(agent_graph)
return agent_graph
步骤 5:FastAPI 设置
最后一步是初始化后端并将其作为 FastAPI 应用公开。它还负责处理简历上传和 PDF 解析,在将原始文件发送给招聘人员之前,将其转换为清晰的文本和技能信息。
import os
from fastapi import FastAPI, HTTPException, File, UploadFile
import uvicorn
from dotenv import load_dotenv
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import LangGraphAGUIAgent
from agent import build_agent, parse_pdf_resume, extract_skills_from_resume
import tempfile
load_dotenv()
app = FastAPI(
title="Job Application Assistant",
description="Find personalized job openings based on skills and preferences",
version="1.0.0",
)
try:
agent_graph = build_agent()
print(agent_graph)
add_langgraph_fastapi_endpoint(
app=app,
agent=LangGraphAGUIAgent(
name="job_application_assistant",
description="Job finder",
graph=agent_graph,
),
path="/",
)
print("[MAIN] Agent registered")
except Exception as e:
print(f"[ERROR] Failed to build agent: {str(e)}")
raise
@app.get("/healthz")
async def health_check():
"""Health check"""
return {
"status": "healthy",
"service": "job-application-assistant",
"version": "1.0.0",
}
@app.post("/api/upload-resume")
async def upload_resume(file: UploadFile = File(...)):
"""
Upload and parse resume (PDF, DOCX, TXT).
Returns extracted text and skills.
"""
if not file:
raise HTTPException(status_code=400, detail="No file provided")
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
content = await file.read()
tmp.write(content)
tmp_path = tmp.name
if file.filename.endswith(".pdf"):
resume_text = parse_pdf_resume(tmp_path)
else:
for other formats, just read as text
resume_text = content.decode("utf-8", errors="ignore")
skills = extract_skills_from_resume(resume_text)
os.unlink(tmp_path)
return {
"success": True,
"text": resume_text[:1000],
"skills": skills,
"filename": file.filename,
}
except Exception as e:
print(f"[ERROR] Resume upload failed: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
def main():
"""Run server"""
host = os.getenv("SERVER_HOST", "0.0.0.0")
port = int(os.getenv("SERVER_PORT", 8123))
uvicorn.run(
"main:app",
host=host,
port=port,
reload=True,
log_level="info",
)
if __name__ == "__main__":
main()
5. 运行应用程序
代码所有部分完成后,就可以在本地运行了。请确保已将凭据添加到agent/.env.
从项目根目录导航到该agent目录并启动 FastAPI 服务器:
cd agent
uv run python main.py
后端将于http://localhost:8123.
在新终端窗口中,使用以下命令启动前端开发服务器:
npm run dev
两个服务器都运行起来后,在浏览器中打开即可在本地查看前端。
然后您上传简历并搜索职位。
根据作业查询的不同,它可以返回不同数量的结果。以下是另一个输出结果!
CopilotKit 还提供了,这是一个实时 AG-UI 运行时视图,可让您检查从后端实时传输的代理运行、状态快照、消息和工具调用。您可以通过叠加在应用程序上的 CopilotKit 按钮访问它。
6. 数据流
现在我们已经构建好了前端和代理服务,接下来我们将展示它们之间的数据实际流动方式。如果您一直跟着我们一起构建,应该很容易理解。
[User uploads resume & submits job query]
↓
Next.js UI (ResumeUpload + CopilotChat)
↓
useCopilotReadable syncs resume + preferences
↓
POST /api/copilotkit (AG-UI protocol)
↓
FastAPI + Deep Agents (/copilotkit endpoint)
↓
Resume context + skills injected into the agent
↓
Deep Agents orchestration
├─ internet_search (Tavily)
├─ job filtering & normalization
└─ update_jobs_list (tool call)
↓
AG-UI streaming (SSE)
↓
CopilotKit runtime receives the tool result
↓
Frontend captures the tool output
↓
Jobs rendered in table + chat stay clean
就是这样!🎉
您现在拥有一个由 Deep Agents 提供支持的求职申请助手,其前端层为 CopilotKit。
关注「索引目录」公众号,获取更多干货。

