Career Copilot

ألصق سيرة ذاتية ووصف وظيفة. يحصل المُجنِّدون على حكم ملاءمة ورسالة تواصل تستحق الإرسال. يحصل المرشحون على ملف عن الشركة وتحضير مقابلة مخصص.

Python3.12Google ADKv2OpenAIgpt-5.4-miniFastAPIasyncTavilyMCPLangfusetracinguvmanagedNext.js15
الرسم البياني متعدد الوكلاء لـ Career Copilot

01.الهدف

Google ADK v2 أطلق مؤخراً API الـ workflow القائم على الرسم البياني. أردت دفعه end-to-end على حالة استخدام حقيقية، بروح LangGraph.

لماذا رسم بياني

  • طوبولوجيا صريحة: النودات والـ edges والفروع المتوازية والتوجيه الشرطي ونقاط المزامنة كلها مكوّنات first-class.
  • وحدات قابلة للاختبار: كل نود يمكن اختباره وحدة، استبداله، أو تتبّعه بمفرده.
  • State مكتوب: عقود في كل خطوة بدلاً من parsing prompts بصيغة حرة.
  • تكلفة قابلة للتنبؤ: تعرف مسبقاً ما هي الخطوات التي تشتغل، بأي ترتيب، وأي نموذج يخدم كل واحدة.

02.حالة الاستخدام

ارفع سيرة ذاتية ووصف وظيفة. خط أنابيب من الوكلاء يقوم بالتحليل الكامل ويُنتج مخرجات مخصّصة للجانب الذي يطرح السؤال من حلقة التوظيف، مُجنِّد أو مرشح.

Career Copilot high-level flow

ما يفعله

  • معالجة مسبقة مشتركة: السيرة الذاتية ووصف الوظيفة يُحلَّلان بالتوازي إلى كائنات مكتوبة (مهارات، سنوات الخبرة، إنجازات، مطلوب مقابل مفضّل، مستوى). كل التفرّع يحدث بعد التحليل.
  • مسار المُجنِّد: يقيّم مدى ملاءمة المرشح للدور (fit أو borderline أو no fit)، ثم إما يصوغ رسالة LinkedIn تستشهد بإنجاز حقيقي من السيرة الذاتية، أو يشرح الفجوات ويقترح أدواراً مماثلة.
  • مسار المرشح: بحث مباشر عن الشركة عبر Tavily MCP (التمويل، الثقافة، الأخبار الحديثة)، اقتراحات تموضع للسيرة الذاتية مُكيَّفة مع الوظيفة، وحزمة تحضير مقابلة بأسئلة محتملة ونقاط حوار.

لماذا نحتاج رسماً بيانياً

  • عمل مشترك، مخرجات متباعدة: كلا المسارين يعيدان استخدام نفس CV وJD المُحلَّلَين، ثم ينتجان قطعاً مختلفة تماماً. وكيل single big-prompt سيحتاج إلى prompt شرطي متشعّب؛ الرسم البياني يجعل الفصل طبيعياً.
  • التوازي يقسم زمن الاستجابة إلى النصف: تحليل CV وJD بالتوازي، ثم على جانب المرشح تشغيل البحث وتموضع السيرة الذاتية بالتوازي. سلسلة خطية ستفرض هذه الخطوات أن تعمل واحدة تلو الأخرى.
  • التفرّع يجب أن يكون حتمياً: التوجيه على fit verdict (fit أو borderline → outreach، no_fit → gap explainer) لا يمكن أن يعتمد على LLM يعيد parsing مخرجاته. FunctionNode router يقرر في pure Python على verdict مكتوب.
  • لماذا هذا المجال يناسب: صغير بما يكفي لإطلاقه كمشروع جانبي، حقيقي بما يكفي لكشف failure modes، غني بما يكفي لاحتياج كل واحدة من الـ affordances أعلاه.

03.الهيكلية الوكيلية

الرسم البياني متعدد الوكلاء لـ Career Copilot

الرسم البياني يُرمّز فصلاً متعمَّداً بين control flow حتمي وتوليد مدفوع بـ LLM. التوجيه والمزامنة يبقيان في pure Python؛ فقط التوليد يعبر حدود LLM، وكل مخرج مُقيَّد بـ schema من Pydantic قبل أن يغادر النود.

كيف الرسم البياني مُوصَّل

  • Control flow يبقى خارج الـ LLMs
    • اثنان من FunctionNode routers (mode_router، verdict_router) يقرران الفروع في pure Python على state مكتوب مسبقاً.
    • JoinNodes تنتظر اكتمال الفروع المتوازية قبل التشغيل في المراحل التالية.
    • لا يقرر أي LLM أبداً أي فرع يُطلَق تالياً، مما يُزيل فئة كاملة من failure modes من المسار الحرج.
  • ثمانية وكلاء متخصصون، schema لكل واحد
    • كل وكيل يعلن output_schema الخاص به: ParsedCV، ParsedJD، FitVerdict، OutreachDraft، GapReport، CompanyIntelligence، CVOptimizationBundle، InterviewPrepBundle.
  • طوبولوجيتا فروع على نفس المُدخلات المُحلَّلة
    • فرع المُجنِّد: سلسلة خطية مع split شرطي واحد على fit verdict (fit / borderline → outreach، no_fit → gap report).
    • فرع المرشح: البحث وتموضع السيرة الذاتية fan-out بالتوازي، يتزامنان عبر JoinNode، ثم يغذيان interview prep.
    • التوازي على جانب المرشح آمن لأن المسارَين ليس بينهما أي تبعية بيانات.
  • مخرج هروبي لاستخدام الأدوات
    • ADK لا يسمح حالياً بدمج output_schema مع tools على gpt-5.4-mini.
    • Research Agent يستخدم toolset Tavily MCP ويُصدر payload CompanyIntelligence كـ JSON string.
    • الـ handler يتحقق منه بـ model_validate_json عند حدود الـ API، مع الحفاظ على invariant typed-state عند الحافة.
python · app/agent/agent.py
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.

محلّل

CV Parser

ParsedCV

يستخرج المهارات وسنوات الخبرة والإنجازات واللغات في ParsedCV مكتوب.

JD Parser

ParsedJD

يستخرج المسمى والشركة والمهارات المطلوبة والمفضلة والمستوى وإشارات الوكالة في ParsedJD.

مُجنِّد

Fit Analyzer

FitVerdict

يقارن ParsedCV بـ ParsedJD ويُصدر FitVerdict مع ثقة مُعايَرة وأدلة مطابقة وفجوات.

Outreach Writer

OutreachDraft

يكتب رسالة LinkedIn تستشهد بإنجاز محدد من السيرة الذاتية. مستوى reasoning متوسط.

Gap Explainer

GapReport

في حال no_fit، يشرح الفجوات بلغة واضحة ويقترح أدواراً مماثلة جديرة بالمتابعة.

مرشح

Research Agent

CompanyIntelligence

يستدعي Tavily search و extract عبر MCP لجمع معلومات الشركة: التمويل، الثقافة، Glassdoor.

CV Optimizer

CVOptimizationBundle

يقترح تعديلات مستهدفة على السيرة الذاتية لتطابق الوظيفة بشكل أفضل، دون الكذب بشأن خبرة المرشح.

Interview Prep

InterviewPrepBundle

يبني حزمة تحضير مقابلة: أسئلة محتملة، نقاط حوار، أسئلة ذكية للرد.

04.تصميم النظام

الرسم البياني الوكيلي أعلاه لا يُطلَق إلا إذا كان مكشوفاً عبر API بدون state ومتاحاً من واجهة. طبقتان نظيفتان تقومان بذلك دون تسريب التعقيد إلى الرسم البياني.

Backend

  • FastAPI يكشف الرسم البياني: اثنان من endpoints POST بدون state (/v1/analyze، /v1/extract-pdf). Schema OpenAPI مُولَّد تلقائياً وتكامل Pydantic أصلي.
  • Async من البداية للنهاية: FastAPI زائد runner async من ADK، لذا الفروع المتوازية في الرسم البياني تشتغل فعلاً بالتوازي.
  • جاهز للحاويات: Dockerfile واحد، lockfile من uv. يُنشَر بشكل متطابق على Cloud Run، Fly.io، أو HuggingFace Spaces.
python · app/routes/analyze.py
# 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)
    )

Frontend

  • عميل Next.js 15 رفيع: يجمع المُدخَلات، POST إلى /v1/analyze، يعرض الاستجابة المكتوبة. كل business logic تبقى في الرسم البياني الـ backend.
  • TanStack Query لاستدعاء analyze: useMutation يدفع الـ flow؛ toasts Sonner تُبرز الأخطاء المهيكلة 502 / 422 من الـ backend.
  • Forms مُتحقَّق منها بـ Zod: analyzeInputSchema واحد للوضعَين. الصق نصاً أو ارفع PDF (يمر عبر /v1/extract-pdf أولاً).

05.القابلية للمراقبة

كل طلب، متتبَّع من البداية للنهاية عبر Langfuse.

نظام LLM لا يمكن تتبّعه هو نظام لا يمكن إصلاحه. كل استدعاء /v1/analyze يفتح observation أبوي في Langfuse. استدعاءات sub-agents و tool calls تتداخل تحته كـ spans فرعية، مع التقاط المُدخلات والمخرجات وزمن الاستجابة وعدد tokens. الوضع والنموذج والإصدار وأحجام المُدخلات تُنشر كـ trace attributes للفلترة السريعة في واجهة Langfuse.

python · app/routes/analyze.py
@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 response

شجرة spans لكل طلب

handler الـ /v1/analyze يفتح observation أبوي من نوع 'agent'. ADK يشغّل كل sub-agent و tool call كـ span متداخل تحته. كل المُدخلات والمخرجات وزمن الاستجابة وعدد tokens يُلتقط تلقائياً.

Trace attributes للفلترة

propagate_attributes يحقن الوضع والنموذج والإصدار و cv_chars و jd_chars على الـ trace النشط. فلتر بالوضع في نقرتين، قارن توزيعات الثقة عبر إصدارات النماذج، رصد الانحدارات مبكراً.

فشل سريع عند الحدود

أخطاء الوكيل تظهر كـ 502 مع traceback كامل في الـ logs. تناقضات state الـ workflow (FitVerdict مفقود، OutreachDraft مفقود) تظهر كـ 500 مع تفاصيل قابلة للتنفيذ. لا شيء يُبتلع بصمت.

جرّبه على سيرتك الذاتية ووظيفتك

ألصق سيرة ذاتية، ألصق وصف وظيفة، اختر وضعاً. ستحصل على حكم ومسوّدة في ثوانٍ.