Corinna is an agentic Digital Services Act self-assessment tool built with LangGraph. It is being built by Simon Coessens in collaboration with Vittorio Franzese (legal lead; PhD in AI & Law at the University of Tübingen), under PI Prof. Antonio Davola, as part of the PRIN PNRR 2022 – SNIP “Self-Assessment Network Impact Program” during an External Expert engagement (Università degli Studi di Bari, Jun–Nov 2025). Project context: SNIP Project.
TL;DR: EU digital regulation is dense. This tool determines whether a company is in-scope for the Digital Services Act, assigns the correct service category, and explains which obligations matter for that company.
Research review – scope confirmation:

Research review – size confirmation:

Compliance dashboard – obligations list:

Compliance dashboard – obligation detail:

Compliance dashboard – action items export:

- Minimal input surface: the user provides a company name; all other context is AI-harvested and user-validated.
- Backend: FastAPI (
backend/api/main.py) exposes streaming and blocking endpoints per agent graph. Streaming is SSE-based and forwards LangGraph event traces (LLM tokens, tool starts/ends, node transitions). - Models/tools:
deepseek-chat(LangChain), Tavily search, Qdrant retriever (knowledge_base) with OpenAI embeddings. - Frontend: Next.js app orchestrates the multi-phase flow and passes
frontend_contextinto the main agent so replies are aware of the active company/phase.
- Company Matcher (
backend/agents/company_matcher) – ReAct loop with toolsweb_search(Tavily) andfinish_matching. Iterates a capped number of times to resolve the canonical company (name + URL) and returns structured JSON. - Company Researcher (
backend/agents/company_researcher) – Loads sub-questions from CSV, runs parallel research agents per question, then summarises with a separate model call. Batching is governed bymax_concurrent_research; output isSubQuestionAnswer[]plus raw research traces. - Service Categorizer (
backend/agents/service_categorizer) – Ingests the confirmed profile JSON, classifies territorial scope and service class, derives obligations from YAML specs, and runs per-obligation analyses (batched) before emitting a consolidated compliance report. - Main Agent (
backend/agents/main_agent) – Lightweight ReAct wrapper with toolsretrieve_dsa_knowledge(Qdrant-backed RAG) andweb_search. Acceptsfrontend_contextto condition the system prompt on UI state.
/agents/company_matcher[/stream]– entity resolution./agents/company_researcher[/stream]– parallel research + summarisation./agents/service_categorizer[/stream]– service classification and obligation analysis./agents/main_agent[/stream]– chat entry point with optional frontend context./health– agent availability.
- Phase 1:
CompanyMatcherstreams candidate entities from the matcher graph. - Phase 2:
DeepResearchruns the researcher graph;ResearchReviewlets users accept/override per-section findings (scope, size, service type). - Phase 3:
ServiceClassificationposts the curated profile to the categorizer;ComplianceDashboardrenders applicability and action items for each obligation. - Chat sidecar (
Chatbot) feedsfrontend_contextto the main agent so answers stay aligned with the active company and phase.
Prompts live in .jinja files alongside each agent and are rendered with Jinja2 to inject runtime variables (company name, context, classification summaries). The code loads templates via a shared helper and renders per call; logic stays in Python, text in templates.
backend/agents/
├── prompts/ # Shared prompt utilities
│ └── __init__.py # load_prompt() helper
│
├── company_matcher/
│ └── src/company_matcher/
│ ├── prompts/
│ │ └── prompt.jinja # Single-turn prompt (system + task)
│ └── graph.py
│
├── company_researcher/
│ └── src/company_researcher/
│ ├── prompts/
│ │ ├── researcher.jinja # Single-turn prompt (system + task)
│ │ └── summarize.jinja # Summarization prompt
│ └── graph.py
│
├── service_categorizer/
│ └── src/service_categorizer/
│ ├── prompts/
│ │ ├── classify.jinja # Service class + territorial scope
│ │ ├── obligation.jinja # Per-obligation analysis
│ │ └── summarize.jinja # Final report synthesis
│ └── graph.py
└── main_agent/
└── src/main_agent/
├── prompts/
│ └── system.jinja # Multi-turn: system prompt only
└── graph.py # User messages come from state
- Single-turn agents embed system + task in one template; main agent uses a system-only template and appends user messages from state.
- Templates take only the runtime variables they need (e.g.,
company_name,frontend_context,classification_summary).
Example single-turn template:
{# Company Matcher - Complete Prompt #}
{#
Variables:
- company_name: The target company
- max_iterations: Maximum search attempts (default: 5)
#}
You are a company matching agent.
## Guidelines
- Maximum {{ max_iterations | default(5) }} iterations allowed
## Task
Find a match for: "{{ company_name }}"Example multi-turn template (system prompt only):
{# Main Agent - System Prompt #}
{#
Variables:
- context: Optional frontend context
#}
You are the Corinna assistant.
## Guidelines
- Cite specific DSA articles
- Provide actionable guidance
{% if context %}
## Current Context
{{ context }}
{% endif %}





