CV Parser
→ ParsedCV
يستخرج المهارات وسنوات الخبرة والإنجازات واللغات في ParsedCV مكتوب.
Google ADK v2 أطلق مؤخراً API الـ workflow القائم على الرسم البياني. أردت دفعه end-to-end على حالة استخدام حقيقية، بروح LangGraph.
ارفع سيرة ذاتية ووصف وظيفة. خط أنابيب من الوكلاء يقوم بالتحليل الكامل ويُنتج مخرجات مخصّصة للجانب الذي يطرح السؤال من حلقة التوظيف، مُجنِّد أو مرشح.
الرسم البياني يُرمّز فصلاً متعمَّداً بين control flow حتمي وتوليد مدفوع بـ LLM. التوجيه والمزامنة يبقيان في pure Python؛ فقط التوليد يعبر حدود LLM، وكل مخرج مُقيَّد بـ schema من Pydantic قبل أن يغادر النود.
root_agent = Workflow(
name="career_copilot",
edges=[
(
"START",
(cv_parser_agent, jd_parser_agent), # parallel
parse_join,
mode_router,
{
"RECRUITER": fit_analyzer_agent,
"CANDIDATE": (research_agent, cv_optimizer_agent),
},
),
(
fit_analyzer_agent,
verdict_router,
{"OUTREACH": outreach_writer_agent, "GAP": gap_explainer_agent},
),
(
(research_agent, cv_optimizer_agent),
candidate_join,
interview_prep_agent,
),
],
)ثمانية وكلاء LLM، اثنان من routers من نوع FunctionNode، اثنان من JoinNodes. كل وكيل يعمل على OpenAI gpt-5.4-mini عبر LiteLlm بمستوى reasoning منخفض افتراضياً. يرفع Outreach Writer إلى medium للحصول على رسائل أدق وأقل عمومية. مخرجات كل وكيل هي schema Pydantic يطبّقه output_schema في ADK، باستثناء Research Agent الذي يستخدم أدوات Tavily ويتحقق من JSON عند حدود الـ API.
→ ParsedCV
يستخرج المهارات وسنوات الخبرة والإنجازات واللغات في ParsedCV مكتوب.
→ ParsedJD
يستخرج المسمى والشركة والمهارات المطلوبة والمفضلة والمستوى وإشارات الوكالة في ParsedJD.
→ FitVerdict
يقارن ParsedCV بـ ParsedJD ويُصدر FitVerdict مع ثقة مُعايَرة وأدلة مطابقة وفجوات.
→ OutreachDraft
يكتب رسالة LinkedIn تستشهد بإنجاز محدد من السيرة الذاتية. مستوى reasoning متوسط.
→ GapReport
في حال no_fit، يشرح الفجوات بلغة واضحة ويقترح أدواراً مماثلة جديرة بالمتابعة.
→ CompanyIntelligence
يستدعي Tavily search و extract عبر MCP لجمع معلومات الشركة: التمويل، الثقافة، Glassdoor.
→ CVOptimizationBundle
يقترح تعديلات مستهدفة على السيرة الذاتية لتطابق الوظيفة بشكل أفضل، دون الكذب بشأن خبرة المرشح.
→ InterviewPrepBundle
يبني حزمة تحضير مقابلة: أسئلة محتملة، نقاط حوار، أسئلة ذكية للرد.
الرسم البياني الوكيلي أعلاه لا يُطلَق إلا إذا كان مكشوفاً عبر API بدون state ومتاحاً من واجهة. طبقتان نظيفتان تقومان بذلك دون تسريب التعقيد إلى الرسم البياني.
# Discriminated union: clients get one of three typed response
# shapes, picked by the requested mode.
AnalyzeResponse = Union[
RecruiterFitResponse, # recruiter mode, fit / borderline
RecruiterNoFitResponse, # recruiter mode, no_fit
CandidateResponse, # candidate mode
]
@router.post("/v1/analyze", response_model=AnalyzeResponse)
async def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
"""Run the agent graph against a CV + JD pair, return the typed result."""
# ADK Workflow graph runs end-to-end. Parallel branches actually
# execute in parallel thanks to the async runner.
state = await run_agent(root_agent, initial_state)
# Pydantic schemas at every node boundary guarantee state is typed
# where it matters; we just pick the right response builder.
return (
_build_recruiter_response(state)
if request.mode == "recruiter"
else _build_candidate_response(state)
)كل طلب، متتبَّع من البداية للنهاية عبر Langfuse.
نظام LLM لا يمكن تتبّعه هو نظام لا يمكن إصلاحه. كل استدعاء /v1/analyze يفتح observation أبوي في Langfuse. استدعاءات sub-agents و tool calls تتداخل تحته كـ spans فرعية، مع التقاط المُدخلات والمخرجات وزمن الاستجابة وعدد tokens. الوضع والنموذج والإصدار وأحجام المُدخلات تُنشر كـ trace attributes للفلترة السريعة في واجهة Langfuse.
@router.post("/analyze", response_model=AnalyzeResponse)
async def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
langfuse = get_client()
with langfuse.start_as_current_observation(
name=f"analyze.{request.mode}",
as_type="agent",
input={"mode": request.mode, "cv_text": ..., "jd_text": ...},
) as span, propagate_attributes(
trace_name=f"career-copilot.analyze.{request.mode}",
tags=["analyze", request.mode],
metadata={
"mode": request.mode,
"model": PRIMARY_MODEL,
"version": VERSION,
"cv_chars": str(len(request.cv_text)),
"jd_chars": str(len(request.jd_text)),
},
):
state = await run_agent(root_agent, initial_state)
response = _build_response(state)
span.update(output=response.model_dump())
return responsehandler الـ /v1/analyze يفتح observation أبوي من نوع 'agent'. ADK يشغّل كل sub-agent و tool call كـ span متداخل تحته. كل المُدخلات والمخرجات وزمن الاستجابة وعدد tokens يُلتقط تلقائياً.
propagate_attributes يحقن الوضع والنموذج والإصدار و cv_chars و jd_chars على الـ trace النشط. فلتر بالوضع في نقرتين، قارن توزيعات الثقة عبر إصدارات النماذج، رصد الانحدارات مبكراً.
أخطاء الوكيل تظهر كـ 502 مع traceback كامل في الـ logs. تناقضات state الـ workflow (FitVerdict مفقود، OutreachDraft مفقود) تظهر كـ 500 مع تفاصيل قابلة للتنفيذ. لا شيء يُبتلع بصمت.
ألصق سيرة ذاتية، ألصق وصف وظيفة، اختر وضعاً. ستحصل على حكم ومسوّدة في ثوانٍ.