Clearance Check Agent
Inspiration
Before a production can film or release content, someone has to manually comb through the script, flagging anything that might need rights clearance — real brands, copyrighted songs, real people, specific locations — and then research each one. This is slow, easy to get wrong under deadline pressure, and usually falls to whoever has the least time to do it thoroughly. I wanted to build something that automates the first pass of that process: not replacing a lawyer's judgment, but turning a task that could take hours into a report generated in minutes, while still producing genuinely specific, useful legal reasoning instead of a generic checklist.
What it does
Clearance Check Agent takes a film or TV script and:
- Identifies clearance-relevant mentions — real brands, copyrighted songs, real people, and specific real locations — while ignoring generic or fictional details that don't matter.
- Autonomously decides what's actually worth researching. This is the core agentic behavior: instead of researching every mention it finds unconditionally, the agent uses judgment to skip mentions that are low-risk, incidental, or redundant with something else already being checked — and explains its reasoning for every skip.
- Researches each flagged mention in real time using Parallel's Search API, pulling current, real sources (news, legal precedent, trademark guidelines) rather than relying on the model's training data alone.
- Synthesizes findings into a clear risk note for each researched mention: whether it likely needs formal clearance, relevant precedent found, and a recommended next step.
- Supports manual override — if a user disagrees with a skip decision, they can request that the mention be researched anyway, without re-running the agent over the whole script.
- Outputs structured JSON, displayed through a Streamlit web interface with color-coded status (flagged vs. clear) for each mention.
How I built it
Tech stack:
- Google Cloud (Gemini Enterprise Agent Platform, formerly Vertex AI) — hosts the Gemini model calls, authenticated via Application Default Credentials tied to a Google Cloud project.
- Google Agent Development Kit (ADK) (
google-adk) — defines the agent itself: its instructions, its tool, and its decision-making loop. This is what makes the project a genuine agent rather than a fixed pipeline — the model decides what to do, not hardcoded control flow. - Gemini (
gemini-3.5-flash) — the underlying model powering both the agent's reasoning and the risk-note synthesis. - Parallel Search API (
parallel-webSDK) — real-time web research, called at runtime as a tool the agent invokes on its own judgment. - Streamlit, deployed on Cloud Run — the web interface and hosting.
- Python —
asynciofor the agent runner.
Process: I built this in two stages, deliberately. First, a working fixed pipeline (extract → research every mention → synthesize) to prove the core Parallel + Gemini mechanic produced genuinely useful output. Once that was validated — including against a scene from a real produced screenplay pulled from IMSDb, to stress-test extraction against realistic formatting, rather than only clean synthetic input — I rebuilt the same idea using ADK, giving the model a real tool and a decision loop instead of hardcoded control flow. From there: a Streamlit UI wrapping the agent, containerized with Docker, and deployed to Cloud Run for a public URL.
Data sources: all clearance research comes from Parallel's live web search at query time — no static dataset — because clearance risk is inherently current; a lawsuit or licensing dispute from last month can change the right answer. For testing, I used a real screenplay scene from IMSDb (not included in the repo, for copyright reasons) plus two original scenes I wrote myself, one of which ships in the repo as sample_script.txt so anyone can test the agent immediately.
What I learned
Comparing the fixed pipeline against the agent made the value of "agentic" concrete, not just conceptual. On a 10-mention test script, the fixed pipeline researched all 10; the agent researched 6 and skipped 4 with specific reasoning — roughly 40% fewer research calls, without losing coverage of anything that mattered.
The efficiency gain was really about judgment, not just filtering. The agent distinguished real MLB players by actual prominence — flagging a Hall-of-Fame-level player while skipping three less-prominent players mentioned only incidentally. More interestingly, it researched two sports broadcasters, even though one was arguably no more famous than a skipped player, because their actual commentary was reproduced in the script, implicating a right of publicity over a specific performance, not just a name being mentioned. The agent wasn't applying a single rule ("famous people get researched"); it reasoned differently about prominence versus how a mention was used, depending on which consideration actually mattered. It also caught that a band's clearance risk was already covered by researching the specific song mentioned alongside it — a relationship between two mentions that a fixed pipeline structurally can't see, since it treats every mention independently.
*No model training was involved * The agent uses a pre-built Gemini model as-is; ADK provides an orchestration layer (exposing our Parallel-search function as a tool with a schema the model can read, managing the reasoning loop, letting the model's output drive control flow) rather than any model customization. The judgment seen above comes entirely from a detailed system instruction plus an optional tool, not from fine-tuning.
Learning ADK itself was its own real learning curve, separate from the AI logic. Coming from more general cloud/API experience, Google's agent framework has its own conventions that took deliberate ramp-up time to get right:
Configuration and model selection weren't obvious. ADK auto-detects Google Cloud credentials through specific environment variables (
GOOGLE_GENAI_USE_VERTEXAI,GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION), which is a different pattern than the rawgoogle-genaiclient's explicitenterprise=Trueparameter I'd already been using elsewhere in the project. Picking a valid model string was also its own hurdle; a wrong model name doesn't fail at the ADK layer, it fails deep inside a Google Cloud API call, so tracing it back to "the model string is wrong" wasn't immediately obvious the first time.Tools aren't just Python functions, they're contracts with the model. ADK reads a function's docstring and type hints to generate the schema the LLM actually sees. Writing a tool that "worked" in the sense of running correctly wasn't enough; the docstring had to be sufficiently descriptive that the model could judge when to call it and when to skip it. Early, thinner docstrings produced an agent that either over-called or under-called the tool. The real fix was to treat the docstring as a specification for the model's judgment, not just as internal code documentation.
The execution model is async and event-based, not a simple function call. Getting a response back means creating a session through a runner, sending a message, and iterating over a stream of events until one is flagged as the final response. Quite different from the single synchronous call used in the earlier pipeline version. That structure exists because ADK is built to support multi-step tool-calling loops (the model can call a tool, see the result, and decide to call another tool or respond) rather than one-shot completions, which only became clear once I saw the runner's event loop rather than just reading the "quickstart" example.
Getting the agent to actually exercise judgment took real prompt iteration, not just wiring up the tool. Simply giving the agent an optional tool didn't automatically produce good skip/research decisions. Early instruction drafts either researched everything (defeating the purpose) or skipped too aggressively. Getting the balance shown in the results above took explicit instruction language pushing back on lazy reasoning ("don't just say low risk, explain why") and required structured JSON output before the decisions were even legible enough to evaluate.
Challenges I ran into
Antivirus software turned out to be a bigger bug source than any of my code. Gemini API calls started failing intermittently with WinError 10054. A mid-connection TLS renegotiation. Which is a known signature of software doing SSL inspection. To diagnose it outside Python, I ran curl -v directly against the same endpoint, which pointed to two antivirus programs (Windows Defender and McAfee) in conflict over real-time network interception. Rather than requiring antivirus to be disabled to run the project, I built retry logic with exponential backoff directly into the client, which fixed the practical symptom regardless of the exact machine it runs on.
Deploying to Cloud Run was its own extended debugging chain. In sequence: the default Cloud Build service account was missing storage.objectViewer and logging.logWriter permissions (both needed explicit IAM grants on a fresh project); the container then failed to start because streamlit itself had been left out of a regenerated requirements.txt; after fixing that, a Python version mismatch surfaced, pip freeze had captured package versions resolved for my local Python 3.14, including a numpy version that required Python ≥3.12, while the Dockerfile was still pinned to 3.11; and finally, passing environment variables (including the Parallel API key) via --set-env-vars kept silently merging into a single malformed variable due to comma-parsing behavior in the shell, which I only solved by switching to gcloud's custom-delimiter syntax (^:^). Each of these was a one-line fix once correctly diagnosed, but finding the real error behind Cloud Run's generic "container failed to start" message required methodical log hunting at every step.
Built With
- asyncio
- docker
- github
- google-adk
- google-cloud
- google-cloud-run
- google-gemini
- parallel-api
- python
- streamlit
- vs-code
Log in or sign up for Devpost to join the conversation.