chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:30:30 +08:00
commit 914fea506e
2793 changed files with 802106 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
[*.sh]
indent_style = space
indent_size = 2
+2
View File
@@ -0,0 +1,2 @@
code_review:
comment_severity_threshold: LOW
+153
View File
@@ -0,0 +1,153 @@
# GoogleCloudPlatform/generative-ai Style Guide
The current year is 2026.
IMPORTANT: Agent Platform (full name Gemini Enterprise Agent Platform) was previously named "Vertex AI" and many web resources use the legacy branding. Make sure that all new content uses the new name.
## Markdown Style (For `.md` files and `.ipynb` Markdown Cells)
- Use single backticks ( ` ` ) to format inline code elements, such as variable names, function names, enum names, and brief code snippets.
- Add documentation links to the appropriate Agent Platform pages when describing product features. e.g. https://docs.cloud.google.com/gemini-enterprise-agent-platform/
- Do not reference documentation from the Gemini Developer API, e.g. https://ai.google.dev/ unless there is not a suitable page in the Agent Platform documentation.
The Author block in Notebooks and Markdown should be in a format like this:
For one author:
| Author |
| --- |
| [Firstname Lastname](https://github.com/username) |
For multiple authors
| Authors |
| --- |
| [Firstname Lastname](https://github.com/username) |
| [Firstname Lastname](https://github.com/username) |
## Code Requirements
- Don't include hard-coded Google Cloud project IDs, always use a placeholder like `[your-project-id]`
**Correct**
```py
PROJECT_ID = "[your-project-id]"
```
**Incorrect**
```py
PROJECT_ID = "actual-projectid-1234"
```
- Use the `global` endpoint for all requests, unless a regional endpoint is specifically required for a model or feature.
**Correct**
```py
LOCATION = "global"
```
**Incorrect**
```py
LOCATION = "us-central1"
```
- Don't restart the kernel or use `!pip`, use `%pip` when installing
**Correct**
```sh
%pip install
```
**Incorrect**
```sh
!pip install
```
```sh
!pip3 install
```
```py
app = IPython.Application.instance()
app.kernel.do_shutdown(True)
```
## Golden Rule: Use the Correct and Current SDK
Always use the **Google Gen AI SDK** (`google-genai`), which is the unified
standard library for all Gemini API requests (AI Studio/Gemini Developer API
and Vertex AI/Agent Platform) as of 2026. Do not use legacy libraries and SDKs.
- **Library Name:** Google Gen AI SDK
- **Python Package:** `google-genai`
- **Legacy Library**: (`google-generativeai`) is deprecated.
**Installation:**
- **Incorrect:** `pip install google-generativeai`
- **Incorrect:** `pip install google-ai-generativelanguage`
- **Correct:** `pip install google-genai`
**APIs and Usage:**
- **Incorrect:** `import google.generativeai as genai`-> **Correct:** `from
google import genai`
- **Incorrect:** `from google.ai import generativelanguage_v1` ->
**Correct:** `from google import genai`
- **Incorrect:** `from google.generativeai` -> **Correct:** `from google
import genai`
- **Incorrect:** `from google.generativeai import types` -> **Correct:** `from
google.genai import types`
- **Incorrect:** `import google.generativeai as genai` -> **Correct:** `from
google import genai`
- **Incorrect:** `genai.configure(api_key=...)` -> **Correct:** `client =
genai.Client(api_key='...')`
- **Incorrect:** `model = genai.GenerativeModel(...)`
- **Incorrect:** `model.generate_content(...)` -> **Correct:**
`client.models.generate_content(...)`
- **Incorrect:** `response = model.generate_content(..., stream=True)` ->
**Correct:** `client.models.generate_content_stream(...)`
- **Incorrect:** `genai.GenerationConfig(...)` -> **Correct:**
`types.GenerateContentConfig(...)`
- **Incorrect:** `safety_settings={...}` -> **Correct:** Use `safety_settings`
inside a `GenerateContentConfig` object.
- **Incorrect:** `from google.api_core.exceptions import GoogleAPIError` ->
**Correct:** `from google.genai.errors import APIError`
- **Incorrect:** `types.ResponseModality.TEXT`
## Models
- By default, use the following models when using `google-genai`:
- **General Text & Multimodal Tasks:** `gemini-3.5-flash`
- **Coding and Complex Reasoning Tasks:** `gemini-3.1-pro-preview`
- **Low Latency & High Volume Tasks:** `gemini-3.1-flash-lite`
- **Fast Image Generation and Editing:** `gemini-3.1-flash-image` (aka Nano Banana 2)
- **High-Quality Image Generation and Editing:** `gemini-3-pro-image` (aka Nano Banana Pro)
- **High-Fidelity Video Generation:** or `veo-3.1-generate-001`
- **Advanced Video Editing Tasks:** `veo-3.1-generate-001`
- **Video Generation, Balance of speed and quality:** `veo-3.1-fast-generate-001`
- **Fast Video Generation:** `veo-3.1-lite-generate-001`
- It is also acceptable to use following models if explicitly requested by the
user:
- **Gemini 2.5 Series**: `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite`
- **Nano Banana:** `gemini-2.5-flash-image`
- Do not use the following deprecated models (or their variants like
`gemini-1.5-flash-latest`):
- **Gemini 3 Pro Preview**: `gemini-3-pro-preview` (Replaced by `gemini-3.1-pro-preview`)
- **Gemini 3 Flash Preview**: `gemini-3-flash-preview`
- **Gemini 3.1 Flash Image Preview**: `gemini-3.1-flash-image-preview`
- **Gemini 3 Pro Image Preview**: `gemini-3-pro-image-preview`
- **Gemini 2.0 Series**: `gemini-2.0-flash`, `gemini-2.0-flash-lite`
- **Gemini 1.5 Series**: `gemini-1.5-flash`, `gemini-1.5-pro`
- **Prohibited:** `gemini-pro`
- **Veo 3:** `veo-3.0-generate-001`, `veo-3.0-fast-generate-001`
- **Veo 2:** `veo-2.0-generate-001`
- **Imagen:** `imagegeneration@00*`, `imagetext@001` `imagen-3.0-*`, `imagen-4.0-*`
+93
View File
@@ -0,0 +1,93 @@
# Template taken from https://github.com/v8/v8/blob/master/.git-blame-ignore-revs.
#
# This file contains a list of git hashes of revisions to be ignored by git blame. These
# revisions are considered "unimportant" in that they are unlikely to be what you are
# interested in when blaming. Most of these will probably be commits related to linting
# and code formatting.
#
# Instructions:
# - Only large (generally automated) reformatting or renaming CLs should be
# added to this list. Do not put things here just because you feel they are
# trivial or unimportant. If in doubt, do not put it on this list.
# - Precede each revision with a comment containing the PR title and number.
# For bulk work over many commits, place all commits in a block with a single
# comment at the top describing the work done in those commits.
# - Only put full 40-character hashes on this list (not short hashes or any
# other revision reference).
# - Append to the bottom of the file (revisions should be in chronological order
# from oldest to newest).
# - Because you must use a hash, you need to append to this list in a follow-up
# PR to the actual reformatting PR that you are trying to ignore.
3495535bb8a42cd96bb65058396c464611bf6eb9
dbca58d5b3fe8bcb3a18b0708692545d4e13c70f
6862d06d1bed3bfb73e4a15d6d22db70980fa345
9ee2011ee29828b417b7ac9ac31c4a29643a16e7
43626f81127ddba7a181df451455e103cebee3a5
e32ff08d8f83a614f2b4206a15a5c8aa00adaa48
d699c642fa8312e1a0bfc1ad2673bc17648a3c1a
cac56b435fc03002ce6896eec78dff6876bc9dc1
a27d6c3304d0c8fc5978da93e7c4e7bfebd2eba3
401df3220d24516eac92f5b4539a76cd7d757814
33c6bcca6009219efbf05746b9e634c7d4d7dcba
08734ab993a4f5ac1a10561919b54c39ec3af04e
3f89a238eb0b0077ec82544c4fdc1a3fa781416c
a265634234d6c03c83dda1dd29484bdafbf98489
dc699bc4cf3272a79a07ab93b0576d79afbe77de
9c40a06d35cdb95ddbbf78018d0d87a2a945a35e
418725cc5d10af3952179afa1ba9f9477c767ac2
5a653d1d93f15995a306bfeb0c0c5d8f0b909242
5742823bffccefce65087e6530649b4c9beddd67
5742823bffccefce65087e6530649b4c9beddd67
fc5d62431a71187ee6f92a8ccc492ca10e5dc2c3
f35a605a4eaef3a8f0c65f598fe978294cf0e900
5aec8dc940fa015601f1a9c727331291daffdc3d
ff92e99df5faf575245b635a1accd87e56245149
c867107284b92cc641e6d274f8088b764b9c2fa2
d6572fcc7d5d602a96f87fe1754fae9c15b058e8
f5e47957685ffdc4fd4c01385bb11152d8829232
bccaf22a73e3965cb02ed7ca3022ca859c53f2b8
eeced39d16e4638ba6df67a6401e15ca376cd73b
7a5d55d1087fbf1617294281c0eebd212bf1e52c
6ddf4cdd146b928452950408a8c4c1324351681b
0c961fcb3c9cb7bfb5e1ab73acbb8fe7caebcbd4
647a33fcaeb34ee456e4518b4ca443f82a92c501
812b47dfb4c12a5621417a6da8cf5796b44e70d3
a043e43746eea8aeca5dbfb17f2d496a2747b6a3
aa073fe0b74e3813593476ff9a13496d24e2adab
030647d76d06d3a69a0f4b716ad1035de6808653
2b29d1d9a7418fa941709179a1f284be91f3195f
94cace18604ef473e77e54a516b76ac263fae42b
cb2d315aab7438ec45db9928536e7c4c3802fc49
824b32b10f7c3701de214784307b4abb22e5fb0f
b5c2d85557d877bc99bf18fdf549423dc54bb108
8db09b75bdf1e2955f06fc77decfd06bfde13957
f3fd6359f6424af47ba9d5f63145a69989e5c920
221c0b03b29e4abf5a8d07daf2c47d7752a053a4
12bea7f71729db4d19fa2eac5715d326cae39a22
67f18b9c0b1beb1174bd55daa2069ab0f9b3e99d
344921c7c585c5e4c54863a58f59efabdbfe06f6
84ab588c313ddfd08aa6288ed2eef5bf784b1a1a
8e28e0cfa5031892beaebecbe6308696964cc2e5
6664dd984baa77f8358fa0ed8b57bde2b897e116
7adb28cb68e34608e193bb47d123df7051714a46
3e27e38b7eb154763d4c20d11d9c74a6dc08655a
abf108d802b8f0230b38553d0413153a25a01b97
cf0063a41156c362f0f9823a016343daf46ab46b
b086ad4760a3437f0bf21a144b343e48bd0a015d
96f901d9b1f52a2c2dc467679354cfa78b9cec4a
cbc49cc90875655dcd26647835edfcb41c6b2caf
800d05b4a3a28fe23b971e702e76df0c41e6d308
3f0974fed57315c6123864fa2334609f3332029a
afdcb95a39e574515a080cbe544fe35e7bf6d28b
21d4430e8d4b1e2cc2d65b643f88465c81cb76bd
b5dd69e1fc58a01c0c9bf07c4ed30aa132b77553
a41f948147f9c16c9b1255038929ab1d1ac07def
4d099b05aafd58ab6f3bfb816fd312d8d2c7ffcd
6d74fa5d6d8a9a694951a8dac296c1cf314c9d5f
38a81c39a9f2afa674eb89d76bfe5e7671eacf6f
9b71a07da1a2824d108954d5155521ba465df1ef
076464885ddf63d57d91cac830ff83f774d5c877
ca7ceb9f8358a5d79c31d2511f705bcc55c24232
0538e91f7c69c34174c722ae1ed1552261c8b805
6ba714a7cf1d8d460f5518e5bf220b76088aec09
e8cdc7edfeb79312ebe8dcdcf4662ab64ee5bbc7
+79
View File
@@ -0,0 +1,79 @@
# Code owners file.
# This file controls who is tagged for review for any given pull request.
#
# For syntax help see:
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
# @GoogleCloudPlatform/generative-ai-devrel are the default owners for changes in this repo
* @GoogleCloudPlatform/generative-ai-devrel
/generative-ai/embeddings @GoogleCloudPlatform/generative-ai-devrel @kazunori279
/generative-ai/embeddings/vector-search-2-quickstart.ipynb @GoogleCloudPlatform/generative-ai-devrel @ericgribkoff
/generative-ai/gemini @GoogleCloudPlatform/generative-ai-devrel @polong-lin
/generative-ai/gemini/agent-engine @GoogleCloudPlatform/generative-ai-devrel @koverholt
/generative-ai/gemini/agent-engine/tutorial_ag2_on_agent_engine.ipynb @GoogleCloudPlatform/generative-ai-devrel @awaemmanuel
/generative-ai/gemini/agent-engine/tutorial_alloydb_rag_agent.ipynb @GoogleCloudPlatform/generative-ai-devrel @averikitsch
/generative-ai/gemini/agent-engine/tutorial_cloud_sql_pg_rag_agent.ipynb @GoogleCloudPlatform/generative-ai-devrel @averikitsch
/generative-ai/gemini/agents/genai-experience-concierge @GoogleCloudPlatform/generative-ai-devrel @pablofgaeta
/generative-ai/gemini/code-execution @GoogleCloudPlatform/generative-ai-devrel @koverholt
/generative-ai/gemini/evaluation @GoogleCloudPlatform/generative-ai-devrel @inardini @jsondai
/generative-ai/gemini/evaluation/evaluate_langchain_chains.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig
/generative-ai/gemini/function-calling @GoogleCloudPlatform/generative-ai-devrel @koverholt
/generative-ai/gemini/getting-started @GoogleCloudPlatform/generative-ai-devrel @gericdong @polong-lin
/generative-ai/gemini/grounding @GoogleCloudPlatform/generative-ai-devrel @holtskinner
/generative-ai/gemini/mcp/intro_to_mcp.ipynb @GoogleCloudPlatform/generative-ai-devrel @wadave
/generative-ai/gemini/model-optimizer/intro_model_optimizer.ipynb @GoogleCloudPlatform/generative-ai-devrel @nardosalemu
/generative-ai/gemini/multimodal-dataset @GoogleCloudPlatform/generative-ai-devrel @fthoele @cleop-google @diskontinuum
/generative-ai/gemini/multimodal-live-api/websocket-demo-app/ @GoogleCloudPlatform/generative-ai-devrel @ZackAkil
/generative-ai/gemini/rag-engine @GoogleCloudPlatform/generative-ai-devrel @edtsoi430
/generative-ai/gemini/rag-engine/rag_engine_cross_corpus_retrieval.ipynb @GoogleCloudPlatform/generative-ai-devrel @tiger-w-jin
/generative-ai/gemini/responsible-ai @GoogleCloudPlatform/generative-ai-devrel @iamthuya
/generative-ai/gemini/responsible-ai/gemini_safety_ratings.ipynb @GoogleCloudPlatform/generative-ai-devrel @ghchinoy
/generative-ai/gemini/sample-apps/finance-advisor-spanner/ @GoogleCloudPlatform/generative-ai-devrel @anirbanbagchi1979
/generative-ai/gemini/sample-apps/genai-mlops-tune-and-eval @GoogleCloudPlatform/generative-ai-devrel @willisc7
/generative-ai/gemini/sample-apps/genwealth/ @GoogleCloudPlatform/generative-ai-devrel @paulramsey
/generative-ai/gemini/sample-apps/photo-discovery @GoogleCloudPlatform/generative-ai-devrel @KhanhNwin @kazunori279
/generative-ai/gemini/tuning/gemini_supervised_finetuning_text_classification.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig @gabrielahrlr
/generative-ai/gemini/tuning/supervised_finetuning_using_gemini.ipynb @GoogleCloudPlatform/generative-ai-devrel @dmoonat
/generative-ai/gemini/use-cases/applying-llms-to-data/analyze-poster-images-in-bigquery/poster_image_analysis.ipynb @GoogleCloudPlatform/generative-ai-devrel @aliciawilliams
/generative-ai/gemini/use-cases/applying-llms-to-data/semantic-search-in-bigquery/stackoverflow_questions_semantic_search.ipynb @GoogleCloudPlatform/generative-ai-devrel @sethijaideep
/generative-ai/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions @GoogleCloudPlatform/generative-ai-devrel @shanecglass
/generative-ai/gemini/use-cases/education @GoogleCloudPlatform/generative-ai-devrel @PicardParis
/generative-ai/gemini/use-cases/intro_multimodal_use_cases.ipynb @GoogleCloudPlatform/generative-ai-devrel @saeedaghabozorgi
/generative-ai/gemini/use-cases/multimodal-sentiment-analysis/intro_to_multimodal_sentiment_analysis.ipynb @GoogleCloudPlatform/generative-ai-devrel @Mansari
/generative-ai/gemini/use-cases/productivity/productivity_coaching_with_google_calendar_and_gmail.ipynb @GoogleCloudPlatform/generative-ai-devrel @Mansari
/generative-ai/gemini/use-cases/retail @GoogleCloudPlatform/generative-ai-devrel @iamthuya
/generative-ai/gemini/use-cases/retail/product_attributes_extraction.ipynb @GoogleCloudPlatform/generative-ai-devrel @tianli
/generative-ai/gemini/use-cases/retrieval-augmented-generation/rag_pipeline_terabyte_scale_with_bigframes.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig @lspatarog
/generative-ai/gemini/use-cases/retrieval-augmented-generation/rag_qna_with_bq_and_featurestore.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig @lspatarog
/generative-ai/gemini/use-cases/retrieval-augmented-generation/raw_with_bigquery.ipynb @GoogleCloudPlatform/generative-ai-devrel @jeffonelson
/generative-ai/gemini/use-cases/retrieval-augmented-generation/retail_warranty_claim_chatbot.ipynb @GoogleCloudPlatform/generative-ai-devrel @zthor5
/generative-ai/genkit/generate-synthetic-database @GoogleCloudPlatform/generative-ai-devrel @aikozhaoz
/generative-ai/genkit/postcard-generator @GoogleCloudPlatform/generative-ai-devrel @mattsday
/generative-ai/open-models/fine-tuning @GoogleCloudPlatform/generative-ai-devrel @goudtho
/generative-ai/open-models/serving @GoogleCloudPlatform/generative-ai-devrel @polong-lin
/generative-ai/open-models/serving/cloud_run_ollama_gemma3_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol
/generative-ai/open-models/serving/cloud_run_vllm_gemma3_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol
/generative-ai/open-models/serving/vertex_ai_pytorch_inference_pllum_with_custom_handler.ipynb @GoogleCloudPlatform/generative-ai-devrel @rsamborski
/generative-ai/open-models/use-cases/cloud_run_ollama_gemma2_rag_qa.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig
/generative-ai/search @GoogleCloudPlatform/generative-ai-devrel @holtskinner
/generative-ai/search/gemini-enterprise/group-licensing @GoogleCloudPlatform/generative-ai-devrel @williamsmt @googlecharles
/generative-ai/search/retrieval-augmented-generation/examples/contract_analysis.ipynb @GoogleCloudPlatform/generative-ai-devrel @guruvittal
/generative-ai/speech/getting-started @GoogleCloudPlatform/generative-ai-devrel @inardini
/generative-ai/speech/use-cases/storytelling @GoogleCloudPlatform/generative-ai-devrel @holtskinner
/generative-ai/vision/getting-started @GoogleCloudPlatform/generative-ai-devrel @iamthuya
/generative-ai/vision/gradio/gradio_image_generation_sdk.ipynb @GoogleCloudPlatform/generative-ai-devrel @jbrache
/generative-ai/vision/use-cases @GoogleCloudPlatform/generative-ai-devrel @iamthuya
/generative-ai/vision/use-cases/hey_llm @GoogleCloudPlatform/generative-ai-devrel @tushuhei
/generative-ai/vision/sample-apps/V-Start @GoogleCloudPlatform/generative-ai-devrel @WafaeBakkali
/generative-ai/open-models/serving/cloud_run_ollama_qwen3_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol
/generative-ai/open-models/get_started_with_model_garden_sdk.ipynb @GoogleCloudPlatform/generative-ai-devrel @inardini @lizzij
/generative-ai/open-models/use-cases/model_garden_litellm_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @lizzij
/generative-ai/llmevalkit @GoogleCloudPlatform/generative-ai-devrel @santoromike @lkatherine
/generative-ai/gemini/evaluation/evaluate_gemini_structured_output.ipynb @GoogleCloudPlatform/generative-ai-devrel @stevie-p
/generative-ai/agents/gke/agents_with_memory/get_started_with_memory_for_adk_in_gke.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol
/generative-ai/agents/cloud_run/agents_with_memory/get_started_with_memory_for_adk_in_cloud_run.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol
/generative-ai/agents/agent_engine/tutorial_bidi_stream_v2.ipynb @GoogleCloudPlatform/generative-ai-devrel @ericgribkoff
/generative-ai/gemini/sample-apps/gemini-live-telephony-app @GoogleCloudPlatform/generative-ai-devrel @KVishnuVardhanR
/generative-ai/search/auto-rag-eval @tanyagoogle
+48
View File
@@ -0,0 +1,48 @@
name: 🐞 Bug Report
description: File a bug report
title: "[Bug]: "
assignees:
- GoogleCloudPlatform/generative-ai-devrel
body:
- type: markdown
attributes:
value: |
Thanks for stopping by to let us know something could be better!
**PLEASE READ**:
- This repository is for [Generative AI with Vertex AI on Google Cloud](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/overview), not the [Google AI Gemini/PaLM APIs](https://ai.google.dev/)
- For issues with the Google AI Gemini API, refer to the [google-gemini](https://github.com/google-gemini) organization or the [Google Developers Community Discourse](https://discuss.ai.google.dev/).
- For issues with the Vertex AI Python SDK, please report in the [googleapis/python-aiplatform](https://github.com/googleapis/python-aiplatform/) repository.
- If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
- For issues with the Vertex AI API, please report them in [Google issue tracker](https://issuetracker.google.com/issues/new?component=1130925&template=1637248&pli=1).
- type: input
id: file-name
attributes:
label: File Name
description: Which notebook/code sample is the issue occurring in?
placeholder: ex. `gemini/getting-started/intro_gemini_python.ipynb`
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us what you expected to happen and how to reproduce the issue.
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GoogleCloudPlatform/generative-ai?tab=coc-ov-file)
options:
- label: I agree to follow this project's Code of Conduct
required: true
+23
View File
@@ -0,0 +1,23 @@
blank_issues_enabled: false
contact_links:
- name: Google Gen AI Python SDK
url: https://github.com/googleapis/python-genai
about: For issues with the Google Gen AI Python SDK, please report in the googleapis/python-genai repository.
- name: Vertex AI Python SDK
url: https://github.com/googleapis/python-aiplatform
about: For issues with the Vertex AI Python SDK, please report in the googleapis/python-aiplatform repository.
- name: Google AI Gemini API
url: https://github.com/google-gemini
about: This repository is for Agent Platform on Google Cloud, not the Google AI Gemini/PaLM APIs. For issues with the Google AI Gemini API, report them in the appropriate repository in the google-gemini GitHub organization.
- name: Agent Platform API issue
url: https://issuetracker.google.com/issues/new?component=1130925&template=1637248&pli=1
about: For issues with the Agent Platform API, please report them in Google issue tracker.
- name: Google Cloud Support
url: https://cloud.google.com/support
about: If you have a support contract with Google, please create a case in the support console instead of filing on GitHub. This will ensure a timely response.
- name: Stack Overflow - Google Cloud Collective
url: https://stackoverflow.com/collectives/google-cloud
about: A collective for developers who utilize Google Cloud's infrastructure and platform capabilities. This collective is organized and managed by the Stack Overflow community.
- name: Google Cloud Skills Boost (Qwiklabs) Support
url: https://support.google.com/qwiklabs/answer/10702448
about: For issues with Qwiklabs, such as progress not updating, please contact Qwiklabs support.
@@ -0,0 +1,45 @@
name: 💡 Feature Request
description: Suggest an idea for this repository
title: "[Feat]: "
assignees:
- GoogleCloudPlatform/generative-ai-devrel
body:
- type: markdown
attributes:
value: |
Thanks for stopping by to let us know something could be better!
**PLEASE READ**:
- This repository is for [Agent Platform on Google Cloud](https://docs.cloud.google.com/gemini-enterprise-agent-platform), not the [Google AI Gemini/PaLM APIs](https://ai.google.dev/)
- For issues with the Google AI Gemini API, refer to one of [these repositories](https://github.com/orgs/google/repositories?q=generative-ai) or the [Google Developers Community Discourse](https://discuss.ai.google.dev/).
- If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
placeholder: Ex. I'm always frustrated when [...]
- type: textarea
id: describe
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GoogleCloudPlatform/generative-ai?tab=coc-ov-file)
options:
- label: I agree to follow this project's Code of Conduct
required: true
+12
View File
@@ -0,0 +1,12 @@
# Description
Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Follow the [`CONTRIBUTING` Guide](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/CONTRIBUTING.md).
- [ ] Ensure the tests and linter pass (Run `./scripts/format.sh` from the repository root to format).
- If you are creating a new notebook/sample:
- [ ] You are listed as the author in your notebook or `README` file.
- [ ] Your account is listed in [`CODEOWNERS`](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/.github/CODEOWNERS) for the file(s).
Fixes #<issue_number_goes_here> 🦕
+17
View File
@@ -0,0 +1,17 @@
# check-spelling/check-spelling configuration
| File | Purpose | Format | Info |
| -------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| [dictionary.txt](dictionary.txt) | Replacement dictionary (creating this file will override the default dictionary) | one word per line | [dictionary](https://github.com/check-spelling/check-spelling/wiki/Configuration#dictionary) |
| [allow.txt](allow.txt) | Add words to the dictionary | one word per line (only letters and `'`s allowed) | [allow](https://github.com/check-spelling/check-spelling/wiki/Configuration#allow) |
| [reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject) |
| [excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes) |
| [only.txt](only.txt) | Only check matching files (applied after excludes) | perl regular expression | [only](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-only) |
| [patterns.txt](patterns.txt) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) |
| [candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns) |
| [line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) |
| [advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice) |
| [block-delimiters.list](block-delimiters.list) | Define block begin/end markers to ignore lines of text | line with _literal string_ for **start** followed by line with _literal string_ for **end** | [block ignore](https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Block-Ignore#status) |
Note: you can replace any of these files with a directory by the same name (minus the suffix)
and then include multiple files inside that directory (with that suffix) to merge multiple files together.
+28
View File
@@ -0,0 +1,28 @@
<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>
If items relate to a ...
- binary file (or some other file you wouldn't want to check at all).
Please add a file path to the `excludes.txt` file matching the containing file.
File paths are Perl 5 Regular Expressions - you can [test](https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.
`^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude `README.md` (on whichever branch you're using).
- well-formed pattern.
If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
try adding it to the `patterns.txt` file.
Patterns are Perl 5 Regular Expressions - you can [test](https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.
Note that patterns can't match multiline strings.
</details>
<!-- adoption information-->
:steam_locomotive: If you're seeing this message and your PR is from a branch that doesn't have check-spelling,
please merge to your PR's base branch to get the version configured for your repository.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
# Public Keys
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
# RSA Private Key
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
# GPG Public Key
-----BEGIN PGP PUBLIC KEY BLOCK-----
-----END PGP PUBLIC KEY BLOCK-----
# Certificates
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
+675
View File
@@ -0,0 +1,675 @@
# marker to ignore all code on line
^.*/\* #no-spell-check-line \*/.*$
# marker to ignore all code on line
^.*\bno-spell-check(?:-line|)(?:\s.*|)$
# https://cspell.org/configuration/document-settings/
# cspell inline
^.*\b[Cc][Ss][Pp][Ee][Ll]{2}:\s*[Dd][Ii][Ss][Aa][Bb][Ll][Ee]-[Ll][Ii][Nn][Ee]\b
# patch hunk comments
^@@ -\d+(?:,\d+|) \+\d+(?:,\d+|) @@ .*
# git index header
index (?:[0-9a-z]{7,40},|)[0-9a-z]{7,40}\.\.[0-9a-z]{7,40}
# file permissions
['"`\s][-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\+?['"`\s]
# css url wrappings
\burl\([^)]+\)
# cid urls
(['"])cid:.*?\g{-1}
# data url in parens
\(data:(?:[^) ][^)]*?|)(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})[^)]*\)
# data url in quotes
([`'"])data:(?:[^ `'"].*?|)(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,}).*\g{-1}
# data url
\bdata:[-a-zA-Z=;:/0-9+]*,\S*
# https/http/file urls
(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]
# mailto urls
mailto:[-a-zA-Z=;:/?%&0-9+@._]{3,}
# magnet urls
magnet:[?=:\w]+
# magnet urls
"magnet:[^"]+"
# obs:
"obs:[^"]*"
# The `\b` here means a break, it's the fancy way to handle urls, but it makes things harder to read
# In this examples content, I'm using a number of different ways to match things to show various approaches
# asciinema
\basciinema\.org/a/[0-9a-zA-Z]+
# asciinema v2
^\[\d+\.\d+, "[io]", ".*"\]$
# apple
\bdeveloper\.apple\.com/[-\w?=/]+
# Apple music
\bembed\.music\.apple\.com/fr/playlist/usr-share/[-\w.]+
# appveyor api
\bci\.appveyor\.com/api/projects/status/[0-9a-z]+
# appveyor project
\bci\.appveyor\.com/project/(?:[^/\s"]*/){2}builds?/\d+/job/[0-9a-z]+
# Amazon
# Amazon
\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)
# AWS S3
\b\w*\.s3[^.]*\.amazonaws\.com/[-\w/&#%_?:=]*
# AWS execute-api
\b[0-9a-z]{10}\.execute-api\.[-0-9a-z]+\.amazonaws\.com\b
# AWS ELB
\b\w+\.[-0-9a-z]+\.elb\.amazonaws\.com\b
# AWS SNS
\bsns\.[-0-9a-z]+.amazonaws\.com/[-\w/&#%_?:=]*
# AWS VPC
vpc-\w+
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
# YouTube url
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
# YouTube music
\bmusic\.youtube\.com/youtubei/v1/browse(?:[?&]\w+=[-a-zA-Z0-9?&=_]*)
# YouTube tag
<\s*youtube\s+id=['"][-a-zA-Z0-9?_]*['"]
# YouTube image
\bimg\.youtube\.com/vi/[-a-zA-Z0-9?&=_]*
# Google Accounts
\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]*
# Google Analytics
\bgoogle-analytics\.com/collect.[-0-9a-zA-Z?%=&_.~]*
# Google APIs
\bgoogleapis\.(?:com|dev)/[a-z]+/(?:v\d+/|)[a-z]+/[-@:./?=\w+|&]+
# Google Storage
\b[-a-zA-Z0-9.]*\bstorage\d*\.googleapis\.com(?:/\S*|)
# Google Calendar
\bcalendar\.google\.com/calendar(?:/u/\d+|)/embed\?src=[@./?=\w&%]+
\w+\@group\.calendar\.google\.com\b
# Google DataStudio
\bdatastudio\.google\.com/(?:(?:c/|)u/\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|)
# The leading `/` here is as opposed to the `\b` above
# ... a short way to match `https://` or `http://` since most urls have one of those prefixes
# Google Docs
/docs\.google\.com/[a-z]+/(?:ccc\?key=\w+|(?:u/\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\?[-\w=#.]*|/\?[\w=&]*|))
# Google Drive
\bdrive\.google\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]*
# Google Groups
\bgroups\.google\.com(?:/[a-z]+/(?:#!|)[^/\s"]+)*
# Google Maps
\bmaps\.google\.com/maps\?[\w&;=]*
# Google themes
themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+.
# Google CDN
\bclients2\.google(?:usercontent|)\.com[-0-9a-zA-Z/.]*
# Goo.gl
/goo\.gl/[a-zA-Z0-9]+
# Google Chrome Store
\bchrome\.google\.com/webstore/detail/[-\w]*(?:/\w*|)
# Google Books
\bgoogle\.(?:\w{2,4})/books(?:/\w+)*\?[-\w\d=&#.]*
# Google Fonts
\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]*
# Google Forms
\bforms\.gle/\w+
# Google Scholar
\bscholar\.google\.com/citations\?user=[A-Za-z0-9_]+
# Google Colab Research Drive
\bcolab\.research\.google\.com/drive/[-0-9a-zA-Z_?=]*
# GitHub SHAs (api)
\bapi.github\.com/repos(?:/[^/\s"]+){3}/[0-9a-f]+\b
# GitHub SHAs (markdown)
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
# GitHub SHAs
\bgithub\.com(?:/[^/\s"]+){2}[@#][0-9a-f]+\b
# GitHub SHA refs
\[([0-9a-f]+)\]\(https://(?:www\.|)github.com/[-\w]+/[-\w]+/commit/\g{-1}[0-9a-f]*
# GitHub wiki
\bgithub\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\b
# githubusercontent
/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]*
# githubassets
\bgithubassets.com/[0-9a-f]+(?:[-/\w.]+)
# gist github
\bgist\.github\.com/[^/\s"]+/[0-9a-f]+
# git.io
\bgit\.io/[0-9a-zA-Z]+
# GitHub JSON
"node_id": "[-a-zA-Z=;:/0-9+_]*"
# Contributor
\[[^\]]+\]\(https://github\.com/[^/\s"]+/?\)
# GHSA
GHSA(?:-[0-9a-z]{4}){3}
# GitHub actions
\buses:\s+[-\w.]+/[-\w./]+@[-\w.]+
# GitLab commit
\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b
# GitLab merge requests
\bgitlab\.[^/\s"]*/\S+/\S+/-/merge_requests/\d+/diffs#[0-9a-f]{40}\b
# GitLab uploads
\bgitlab\.[^/\s"]*/uploads/[-a-zA-Z=;:/0-9+]*
# GitLab commits
\bgitlab\.[^/\s"]*/(?:[^/\s"]+/){2}commits?/[0-9a-f]+\b
# binance
accounts\.binance\.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]*
# bitbucket diff
\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}diff(?:stat|)(?:/[^/\s"]+){2}:[0-9a-f]+
# bitbucket repositories commits
\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}commits?/[0-9a-f]+
# bitbucket commits
\bbitbucket\.org/(?:[^/\s"]+/){2}commits?/[0-9a-f]+
# bit.ly
\bbit\.ly/\w+
# bitrise
\bapp\.bitrise\.io/app/[0-9a-f]*/[\w.?=&]*
# bootstrapcdn.com
\bbootstrapcdn\.com/[-./\w]+
# cdn.cloudflare.com
\bcdnjs\.cloudflare\.com/[./\w]+
# circleci
\bcircleci\.com/gh(?:/[^/\s"]+){1,5}.[a-z]+\?[-0-9a-zA-Z=&]+
# gitter
\bgitter\.im(?:/[^/\s"]+){2}\?at=[0-9a-f]+
# gravatar
\bgravatar\.com/avatar/[0-9a-f]+
# ibm
[a-z.]*ibm\.com/[-_#=:%!?~.\\/\d\w]*
# imgur
\bimgur\.com/[^.]+
# Internet Archive
\barchive\.org/web/\d+/(?:[-\w.?,'/\\+&%$#_:]*)
# discord
/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}
# Disqus
\bdisqus\.com/[-\w/%.()!?&=_]*
# medium link
\blink\.medium\.com/[a-zA-Z0-9]+
# medium
\bmedium\.com/@?[^/\s"]+/[-\w]+
# microsoft
\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]*
# powerbi
\bapp\.powerbi\.com/reportEmbed/[^"' ]*
# vs devops
\bvisualstudio.com(?::443|)/[-\w/?=%&.]*
# microsoft store
\bmicrosoft\.com/store/apps/\w+
# mvnrepository.com
\bmvnrepository\.com/[-0-9a-z./]+
# now.sh
/[0-9a-z-.]+\.now\.sh\b
# oracle
\bdocs\.oracle\.com/[-0-9a-zA-Z./_?#&=]*
# chromatic.com
/\S+.chromatic.com\S*[")]
# codacy
\bapi\.codacy\.com/project/badge/Grade/[0-9a-f]+
# compai
\bcompai\.pub/v1/png/[0-9a-f]+
# mailgun api
\.api\.mailgun\.net/v3/domains/[0-9a-z]+\.mailgun.org/messages/[0-9a-zA-Z=@]*
# mailgun
\b[0-9a-z]+.mailgun.org
# /message-id/
/message-id/[-\w@./%]+
# Reddit
\breddit\.com/r/[/\w_]*
# requestb.in
\brequestb\.in/[0-9a-z]+
# sched
\b[a-z0-9]+\.sched\.com\b
# Slack url
slack://[a-zA-Z0-9?&=]+
# Slack
\bslack\.com/[-0-9a-zA-Z/_~?&=.]*
# Slack edge
\bslack-edge\.com/[-a-zA-Z0-9?&=%./]+
# Slack images
\bslack-imgs\.com/[-a-zA-Z0-9?&=%.]+
# shields.io
\bshields\.io/[-\w/%?=&.:+;,]*
# stackexchange -- https://stackexchange.com/feeds/sites
\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/)
# Sentry
[0-9a-f]{32}\@o\d+\.ingest\.sentry\.io\b
# Twitter markdown
\[@[^[/\]:]*?\]\(https://twitter.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)\)
# Twitter hashtag
\btwitter\.com/hashtag/[\w?_=&]*
# Twitter status
\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)
# Twitter profile images
\btwimg\.com/profile_images/[_\w./]*
# Twitter media
\btwimg\.com/media/[-_\w./?=]*
# Twitter link shortened
\bt\.co/\w+
# facebook
\bfburl\.com/[0-9a-z_]+
# facebook CDN
\bfbcdn\.net/[\w/.,]*
# facebook watch
\bfb\.watch/[0-9A-Za-z]+
# dropbox
\bdropbox\.com/sh?/[^/\s"]+/[-0-9A-Za-z_.%?=&;]+
# ipfs protocol
ipfs://[0-9a-zA-Z]{3,}
# ipfs url
/ipfs/[0-9a-zA-Z]{3,}
# w3
\bw3\.org/[-0-9a-zA-Z/#.]+
# loom
\bloom\.com/embed/[0-9a-f]+
# regex101
\bregex101\.com/r/[^/\s"]+/\d+
# figma
\bfigma\.com/file(?:/[0-9a-zA-Z]+/)+
# freecodecamp.org
\bfreecodecamp\.org/[-\w/.]+
# image.tmdb.org
\bimage\.tmdb\.org/[/\w.]+
# mermaid
\bmermaid\.ink/img/[-\w]+|\bmermaid-js\.github\.io/mermaid-live-editor/#/edit/[-\w]+
# Wikipedia
\ben\.wikipedia\.org/wiki/[-\w%.#]+
# gitweb
[^"\s]+/gitweb/\S+;h=[0-9a-f]+
# HyperKitty lists
/archives/list/[^@/]+@[^/\s"]*/message/[^/\s"]*/
# lists
/thread\.html/[^"\s]+
# list-management
\blist-manage\.com/subscribe(?:[?&](?:u|id)=[0-9a-f]+)+
# kubectl.kubernetes.io/last-applied-configuration
"kubectl.kubernetes.io/last-applied-configuration": ".*"
# pgp
\bgnupg\.net/pks/lookup[?&=0-9a-zA-Z]*
# Spotify
\bopen\.spotify\.com/embed/playlist/\w+
# Mastodon
\bmastodon\.[-a-z.]*/(?:media/|@)[?&=0-9a-zA-Z_]*
# scastie
\bscastie\.scala-lang\.org/[^/]+/\w+
# images.unsplash.com
\bimages\.unsplash\.com/(?:(?:flagged|reserve)/|)[-\w./%?=%&.;]+
# pastebin
\bpastebin\.com/[\w/]+
# heroku
\b\w+\.heroku\.com/source/archive/\w+
# quip
\b\w+\.quip\.com/\w+(?:(?:#|/issues/)\w+)?
# badgen.net
\bbadgen\.net/badge/[^")\]'\s]+
# statuspage.io
\w+\.statuspage\.io\b
# media.giphy.com
\bmedia\.giphy\.com/media/[^/]+/[\w.?&=]+
# tinyurl
\btinyurl\.com/\w+
# codepen
\bcodepen\.io/[\w/]+
# registry.npmjs.org
\bregistry\.npmjs\.org/(?:@[^/"']+/|)[^/"']+/-/[-\w@.]+
# getopts
\bgetopts\s+(?:"[^"]+"|'[^']+')
# ANSI color codes
(?:\\(?:u00|x)1[Bb]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+|)m
# URL escaped characters
%[0-9A-F][A-F](?=[A-Za-z])
# lower URL escaped characters
%[0-9a-f][a-f](?=[a-z]{2,})
# IPv6
\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\b
# c99 hex digits (not the full format, just one I've seen)
0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP]
# Punycode
\bxn--[-0-9a-z]+
# sha
sha\d+:[0-9]*[a-f]{3,}[0-9a-f]*
# sha-... -- uses a fancy capture
(\\?['"]|&quot;)[0-9a-f]{40,}\g{-1}
# hex runs
\b[0-9a-fA-F]{16,}\b
# hex in url queries
=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?&
# ssh
#(?:ssh-\S+|-nistp256) [-a-zA-Z=;:/0-9+]{12,}
# PGP
\b(?:[0-9A-F]{4} ){9}[0-9A-F]{4}\b
# GPG keys
\b(?:[0-9A-F]{4} ){5}(?: [0-9A-F]{4}){5}\b
# Well known gpg keys
.well-known/openpgpkey/[\w./]+
# pki
-----BEGIN.*-----END
# uuid:
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
# hex digits including css/html color classes:
(?:[\\0][xX]|\\u|[uU]\+|#x?|%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\d+)\b
# integrity
integrity=(['"])(?:\s*sha\d+-[-a-zA-Z=;:/0-9+]{40,})+\g{-1}
# https://www.gnu.org/software/groff/manual/groff.html
# man troff content
\\f[BCIPR]
# '/"
\\\([ad]q
# .desktop mime types
^MimeTypes?=.*$
# .desktop localized entries
^[A-Z][a-z]+\[[a-z]+\]=.*$
# Localized .desktop content
Name\[[^\]]+\]=.*
# IServiceProvider / isAThing
(?:\b|_)(?:I|isA)(?=(?:[A-Z][a-z]{2,})+(?:[A-Z]|\b))
# crypt
(['"])\$2[ayb]\$.{56}\g{-1}
# scrypt / argon
\$(?:scrypt|argon\d+[di]*)\$\S+
# go.sum
\bh1:\S+
# scala imports
^import (?:[\w.]|\{\w*?(?:,\s*(?:\w*|\*))+\})+
# scala modules
("[^"]+"\s*%%?\s*){2,3}"[^"]+"
# Intel intrinsics
_mm_\w+
# Input to GitHub JSON
content: (['"])[-a-zA-Z=;:/0-9+]*=\g{-1}
# This does not cover multiline strings, if your repository has them,
# you'll want to remove the `(?=.*?")` suffix.
# The `(?=.*?")` suffix should limit the false positives rate
# printf
#%(?:(?:(?:hh?|ll?|[jzt])?[diuoxn]|l?[cs]|L?[fega]|p)(?=[a-z]{2,})|(?:X|L?[FEGA]|p)(?=[a-zA-Z]{2,}))(?!%)(?=[_a-zA-Z]+(?!%)\b)(?=.*?['"])
# Alternative printf
# %s
#%(?:s(?=[a-z]{2,}))(?!%)(?=[_a-zA-Z]+(?!%)\b)(?=.*?['"])
# Python string prefix / binary prefix
# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings
(?<!['"])\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)['"](?=[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})
# Regular expressions for (P|p)assword
\([A-Z]\|[a-z]\)[a-z]+
# JavaScript regular expressions
# javascript test regex
/.{3,}/[gim]*\.test\(
# javascript match regex
\.match\(/[^/\s"]{3,}/[gim]*\s*
# javascript match regex
\.match\(/\\[b].{3,}?/[gim]*\s*\)(?:;|$)
# javascript replace regex
\.replace\(/[^/\s"]{3,}/[gim]*\s*,
# assign regex
= /[^*].*?(?:[a-z]{3,}|[A-Z]{3,}|[A-Z][a-z]{2,}).*/[gi]?(?=\W|$)
# perl regex test
[!=]~ (?:/.*/|m\{.*?\}|m<.*?>|m([|!/@#,;']).*?\g{-1})
# perl qr regex
(?<!\$)\bqr(?:\{.*?\}|<.*?>|\(.*?\)|([|!/@#,;']).*?\g{-1})
# perl run
perl(?:\s+-[a-zA-Z]\w*)+
# C network byte conversions
#(?:\d|\bh)to(?!ken)(?=[a-z])|to(?=[adhiklpun]\()
# Go regular expressions
regexp?\.MustCompile\(`[^`]*`\)
# regex choice
\(\?:[^)]+\|[^)]+\)
# proto
^\s*(\w+)\s\g{-1} =
# sed regular expressions
sed 's/(?:[^/]*?[a-zA-Z]{3,}[^/]*?/){2}
# node packages
(["'])@[^/'" ]+/[^/'" ]+\g{-1}
# go install
go install(?:\s+[a-z]+\.[-@\w/.]+)+
# jetbrains schema https://youtrack.jetbrains.com/issue/RSRP-489571
urn:shemas-jetbrains-com
# kubernetes pod status lists
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+
# kubectl - pods in CrashLoopBackOff
\w+-[0-9a-f]+-\w+\s+\d+/\d+\s+CrashLoopBackOff\s+
# kubernetes object suffix
-[0-9a-f]{10}-\w{5}\s
# posthog secrets
([`'"])phc_[^"',]+\g{-1}
# xcode
# xcodeproject scenes
(?:Controller|destination|ID|id)="\w{3}-\w{2}-\w{3}"
# xcode api botches
customObjectInstantitationMethod
# msvc api botches
PrependWithABINamepsace
# configure flags
.* \| --\w{2,}.*?(?=\w+\s\w+)
# font awesome classes
\.fa-[-a-z0-9]+
# bearer auth
#(['"])[Bb]ear[e][r] .*?\g{-1}
# bearer auth
\b[Bb]ear[e][r]:? [-a-zA-Z=;:/0-9+.]+
# basic auth
(['"])[Bb]asic [-a-zA-Z=;:/0-9+]{3,}\g{-1}
# encoded-word
=\?[-a-zA-Z0-9"*%]+\?[BQ]\?[^?]{0,75}\?=
# Time Zones
\b(?:Africa|Atlantic|America|Antarctica|Asia|Australia|Europe|Indian|Pacific)(?:/\w+)+
# linux kernel info
^(?:bugs|flags|Features)\s+:.*
# systemd mode
systemd.*?running in system mode \([-+].*\)$
# Lorem
# Update Lorem based on your content (requires `ge` and `w` from https://github.com/jsoref/spelling; and `review` from https://github.com/check-spelling/check-spelling/wiki/Looking-for-items-locally )
# grep '^[^#].*lorem' .github/actions/spelling/patterns.txt|perl -pne 's/.*i..\?://;s/\).*//' |tr '|' "\n"|sort -f |xargs -n1 ge|perl -pne 's/^[^:]*://'|sort -u|w|sed -e 's/ .*//'|w|review -
# Warning, while `(?i)` is very neat and fancy, if you have some binary files that aren't proper unicode, you might run into:
# ... Operation "substitution (s///)" returns its argument for non-Unicode code point 0x1C19AE (the code point will vary).
# ... You could manually change `(?i)X...` to use `[Xx]...`
# ... or you could add the files to your `excludes` file (a version after 0.0.19 should identify the file path)
(?:(?:\w|\s|[,.])*\b(?i)(?:amet|consectetur|cursus|dolor|eros|ipsum|lacus|libero|ligula|lorem|magna|neque|nulla|suscipit|tempus)\b(?:\w|\s|[,.])*)
# Non-English
[a-zA-Z]*[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*|[a-zA-Z]{3,}[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]|[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3,}
# highlighted letters
\[[A-Z]\][a-z]+
# French
# This corpus only had capital letters, but you probably want lowercase ones as well.
\b[LN]'+[a-z]{2,}\b
# latex (check-spelling <= 0.0.21)
\\(?:n(?:ew|ormal|osub)|r(?:enew)|t(?:able(?:of|)|he|itle))(?=[a-z]+)
# latex (check-spelling >= 0.0.22)
\\\w{2,}\{
# American Mathematical Society (AMS) / Doxygen
TeX/AMS
# File extensions
\*\.[+\w]+,
# eslint
"varsIgnorePattern": ".+"
# Windows short paths
[/\\][^/\\]{5,6}~\d{1,2}[/\\]
# cygwin paths
/cygdrive/[a-zA-Z]/(?:Program Files(?: \(.*?\)| ?)(?:/[-+.~\\/()\w ]+)*|[-+.~\\/()\w])+
# in check-spelling@v0.0.22+, printf markers aren't automatically consumed
# printf markers
(?<!\\)\\[nrt](?=[a-z]{2,})
# alternate printf markers if you run into latex and friends
(?<!\\)\\[nrt](?=[a-z]{2,})(?=.*['"`])
# apache
a2(?:en|dis)
# weak e-tag
W/"[^"]+"
# the negative lookahead here is to allow catching 'templatesz' as a misspelling
# but to otherwise recognize a Windows path with \templates\foo.template or similar:
\\(?:necessary|r(?:eport|esolve[dr]?|esult)|t(?:arget|emplates?))(?![a-z])
# ignore long runs of a single character:
\b([A-Za-z])\g{-1}{3,}\b
# version suffix <word>v#
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
# Compiler flags (Unix, Java/Scala)
# Use if you have things like `-Pdocker` and want to treat them as `docker`
#(?:^|[\t ,>"'`=(])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
# Compiler flags (Windows / PowerShell)
# This is a subset of the more general compiler flags pattern.
# It avoids matching `-Path` to prevent it from being treated as `ath`
(?:^|[\t ,"'`=(])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))
# Compiler flags (linker)
,-B
# libraries
\blib(?!rar(?:ies|y))(?=[a-z])
# WWNN/WWPN (NAA identifiers)
\b(?:0x)?10[0-9a-f]{14}\b|\b(?:0x|3)?[25][0-9a-f]{15}\b|\b(?:0x|3)?6[0-9a-f]{31}\b
# iSCSI iqn (approximate regex)
\biqn\.[0-9]{4}-[0-9]{2}(?:[\.-][a-z][a-z0-9]*)*\b
# curl arguments
\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
# set arguments
\b(?:bash|sh|set)(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)*
# tar arguments
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long...
\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b
# macOS temp folders
/var/folders/\w\w/[+\w]+/(?:T|-Caches-)/
# github runner temp folders
/home/runner/work/_temp/[-_/a-z0-9]+
+118
View File
@@ -0,0 +1,118 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes
(?:^|/)(?i)COPYRIGHT
(?:^|/)(?i)LICEN[CS]E
(?:^|/)3rdparty/
(?:^|/)go\.sum$
(?:^|/)package(?:-lock|)\.json$
(?:^|/)Pipfile$
(?:^|/)pyproject.toml
(?:^|/)requirements(?:-dev|-doc|-test|)\.txt$
(?:^|/)vendor/
/CODEOWNERS$
\.a$
\.ai$
\.all-contributorsrc$
\.avi$
\.bmp$
\.bz2$
\.cer$
\.class$
\.coveragerc$
\.crl$
\.crt$
\.csr$
\.dll$
\.docx?$
\.drawio$
\.DS_Store$
\.eot$
\.eps$
\.exe$
\.gif$
\.git-blame-ignore-revs$
\.gitattributes$
\.gitkeep$
\.graffle$
\.gz$
\.icns$
\.ico$
\.jar$
\.jks$
\.jpe?g$
\.key$
\.lib$
\.lock$
\.map$
\.min\..
\.mo$
\.mod$
\.mp[34]$
\.o$
\.ocf$
\.otf$
\.p12$
\.parquet$
\.pdf$
\.pem$
\.pfx$
\.png$
\.psd$
\.pyc$
\.pylintrc$
\.qm$
\.s$
\.sig$
\.so$
\.svgz?$
\.sys$
\.tar$
\.tgz$
\.tiff?$
\.ttf$
\.wav$
\.webm$
\.webp$
\.woff2?$
\.xcf$
\.xlsx?$
\.xpm$
\.xz$
\.zip$
^\.github/actions/spelling/
^\Q.github/workflows/spelling.yaml\E$
^\Q.github/workflows/notebook_linter/run_linter.sh\E$
^\Q.github/workflows/linter.yaml\E$
^\Qgemini/use-cases/education/use_cases_for_education.ipynb\E$
^\Qgemini/sample-apps/image-bash-jam/gemini-explain-image-italian.sh\E$
^\Qgemini/sample-apps/image-bash-jam/tts.sh\E$
^\Qgemini/use-cases/document-processing/sheet_music.ipynb\E$
^\Qgemini/function-calling/use_case_company_news_and_insights.ipynb\E$
^\Qgemini/getting-started/intro_gemini_1_5_pro.ipynb\E$
^\Qgemini/getting-started/intro_gemini_pro_vision_python.ipynb\E$
^\Qgemini/getting-started/intro_gemini_python.ipynb\E$
^\Qgemini/agent-engine/tutorial_google_maps_agent.ipynb\E$
^\Qgemini/sample-apps/image-bash-jam/images/.keep\E$
^\Qgemini/sample-apps/image-bash-jam/README.md\E$
^\Qgemini/sample-apps/photo-discovery/app/linux/CMakeLists.txt\E$
^\Qgemini/sample-apps/photo-discovery/app/windows/CMakeLists.txt\E$
^\Qgemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/provision_text_sample_table.sql\E$
^\Qgemini/use-cases/healthcare/react_gemini_healthcare_api.ipynb\E$
^\Qgemini/use-cases/intro_multimodal_use_cases.ipynb\E$
^\Qgemini/use-cases/retrieval-augmented-generation/intro_multimodal_rag.ipynb\E$
^\Qgemini/use-cases/retail/product_attributes_extraction.ipynb\E$
^\Qlanguage/use-cases/marketing-image-overlay/marketing_image_overlay.ipynb\E$
^\Qsearch/bulk-question-answering/bulk_question_answering_output.tsv\E$
^\Qvision/getting-started/image_editing_maskmode.ipynb\E$
^\Qvision/getting-started/image_generation.ipynb\E$
^\Qvision/getting-started/imagen4_image_generation.ipynb\E$
^\Qvision/getting-started/visual_captioning.ipynb\E$
^\Qvision/use-cases/creating_high_quality_visual_assets_with_gemini_and_imagen.ipynb\E$
^\Qgemini/orchestration/llamaindex_workflows.ipynb\E$
ignore$
^\Qworkshops/ai-agents/ai_agents_for_engineers.ipynb\E$
^\Q.github/workflows/issue_assigner/assign_issue.py\E$
^\Qnoxfile.py\E$
py\.typed$
^\Qaudio/speech/use-cases/storytelling/macbeth_the_sitcom.json\E$
.ruff.toml
^\Q.gemini/styleguide.md\E$
@@ -0,0 +1,323 @@
# reject `m_data` as VxWorks defined it and that breaks things if it's used elsewhere
# see [fprime](https://github.com/nasa/fprime/commit/d589f0a25c59ea9a800d851ea84c2f5df02fb529)
# and [Qt](https://github.com/qtproject/qt-solutions/blame/fb7bc42bfcc578ff3fa3b9ca21a41e96eb37c1c7/qtscriptclassic/src/qscriptbuffer_p.h#L46)
#\bm_data\b
# Were you debugging using a framework with `fit()`?
# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test,
# you might not want to check in code where you skip all the other tests.
#\bfit\(
# Should be `HH:MM:SS`
\bHH:SS:MM\b
# Should be `86400` (seconds in a standard day)
\b84600\b(?:.*\bday\b)
# Should probably be `2006-01-02` (yyyy-mm-dd)
\b2006-01-02\b
# Should probably be `YYYYMMDD`
\b[Yy]{4}[Dd]{2}[Mm]{2}(?!.*[Yy]{4}[Dd]{2}[Mm]{2}).*$
# Should be `anymore`
\bany more[,.]
# Should be `cannot` (or `can't`)
# See https://www.grammarly.com/blog/cannot-or-can-not/
# > Don't use `can not` when you mean `cannot`. The only time you're likely to see `can not` written as separate words is when the word `can` happens to precede some other phrase that happens to start with `not`.
# > `Can't` is a contraction of `cannot`, and it's best suited for informal writing.
# > In formal writing and where contractions are frowned upon, use `cannot`.
# > It is possible to write `can not`, but you generally find it only as part of some other construction, such as `not only . . . but also.`
# - if you encounter such a case, add a pattern for that case to patterns.txt.
\b[Cc]an not\b
# Should be `GitHub`
(?<![&*.]|// |\btype |\bimport )\bGithub\b(?![{()])
\b[Gg]it\s[Hh]ub\b
# Should be `GitLab`
(?<![&*.]|// |\btype )\bGitlab\b(?![{)])
# Should be `JavaScript`
\bJavascript\b
# Should be `macOS` or `Mac OS X` or ...
\bMacOS\b
# Should be `Microsoft`
\bMicroSoft\b
# Should be `OAuth`
(?:^|[^-/*$])[ '"]oAuth(?: [a-z]|\d+ |[^ a-zA-Z0-9:;_.()])
# Should be `RabbitMQ`
\bRabbitmq\b
# Should be `TypeScript`
\bTypescript\b
# Should be `another`
\ban[- ]other\b
# Should be `case-(in)sensitive`
\bcase (?:in|)sensitive\b
# Should be `coinciding`
\bco-inciding\b
# Should be `deprecation warning(s)`
\b[Dd]epreciation [Ww]arnings?\b
# Should be `greater than`
\bgreater then\b
# Should be `ID`
#\bId\b
# Should be `in front of`
\bin from of\b
# Should be `into`
# when not phrasal and when `in order to` would be wrong:
# https://thewritepractice.com/into-vs-in-to/
\sin to\s(?!if\b)
# Should be `use`
\sin used by\b
# Should be `is obsolete`
\bis obsolescent\b
# Should be `it's` or `its`
\bits[']
# Should be `its`
\bit's(?= own\b)
# Should be `perform its`
\bperform it's\b
# Should be `opt-in`
(?<!\sfor)\sopt in\s
# Should be `less than`
\bless then\b
# Should be `load balancer`
\b[Ll]oud balancer
# Should be `one of`
\bon of\b
# Should be `otherwise`
\bother[- ]wise\b
# Should be `or (more|less)`
\bore (?:more|less)\b
# Should be `rather than`
\brather then\b
# Should be `regardless, ...` or `regardless of (whether)`
\b[Rr]egardless if you\b
# Should be `no longer needed`
\bno more needed\b(?! than\b)
# Should be `did not exist`
\bwere not existent\b
# Should be `nonexistent`
\bnon existing\b
# Should be `nonexistent`
\b[Nn]o[nt][- ]existent\b
# Should be `@brief` / `@details` / `@param` / `@return` / `@retval`
(?:^\s*|(?:\*|//|/*)\s+`)[\\@](?:breif|(?:detail|detials)|(?:params(?!\.)|prama?)|ret(?:uns?)|retvl)\b
# Should be `preexisting`
[Pp]re[- ]existing
# Should be `preempt`
[Pp]re[- ]empt\b
# Should be `preemptively`
[Pp]re[- ]emptively
# Should be `recently changed` or `recent changes`
[Rr]ecent changed
# Should be `reentrancy`
[Rr]e[- ]entrancy
# Should be `reentrant`
[Rr]e[- ]entrant
# Should be `understand`
\bunder stand\b
# Should be `workarounds`
\bwork[- ]arounds\b
# Should be `workaround`
(?:(?:[Aa]|[Tt]he|ugly)\swork[- ]around\b|\swork[- ]around\s+for)
# Should be `(coarse|fine)-grained`
\b(?:coarse|fine) grained\b
# Should be `neither/nor` -- or reword
\bnot\b[^.?!"/(]+\bnor\b
# Should be `neither/nor` (plus rewording the beginning)
# This is probably a double negative...
\bnot\b[^.?!"/]*\bneither\b[^.?!"/(]*\bnor\b
# In English, duplicated words are generally mistakes
# There are a few exceptions (e.g. "that that").
# If the highlighted doubled word pair is in:
# * code, write a pattern to mask it.
# * prose, have someone read the English before you dismiss this error.
\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s
# Should be `Gen AI`
\b[gG]enAI\b
# Should be LangChain
\b(?!LangChain\b)(?!langchain\b)[Ll]ang\s?[Cc]hain?\b
# Should be LangGraph
\b(?!LangGraph\b)(?!langgraph\b)[Ll]ang\s?[Gg]raph?\b
# Should be LangServe
\b(?!LangServe\b)(?!langserve\b)[Ll]ang\s?[Ss]erve?\b
# Should be LlamaIndex
\b(?!LlamaIndex\b)[Ll][Ll]ama\s?[Ii]ndex?\s
# Should be Hugging Face
\s(?!Hugging Face\b)[Hh]ugging\s?[Ff]ace?\b
# Should be DeepSeek
\b(?!DeepSeek\b)(?!deepseek\b)[Dd]eep\s?[Ss]eek?\b
# Should be Agent Platform
^(?!.*Agent Platform).*\b[Vv]ertex[-\s][Aa][Ii]\b
# Should be Gemini
\sgemini\s\w
# Should be `Gemini Version Size` (e.g. `Gemini 3 Flash`)
\bGemini\s(Pro|Flash|Ultra)\s?\d\.\d\b
# Gemini Size should be capitalized (e.g. `Gemini 3 Flash`)
\bGemini\s?\d\.\d\s(pro|flash|ultra)\b
# Don't say "Google Gemini" or "Google Gemini"
\b[Gg]oogle(?: [Cc]loud| [Dd]eep[Mm]ind)?'s [Gg]emini\b
# Don't say "Powered by Gemini", instead say "with Gemini"
\b[Pp]owered\s[Bb]y\s[Gg]emini\b
# Should be Gemini Enterprise
[Aa]gent\s?[Ss]pace
# Should be Imagen
\simagen\s\w
# Should be Imagen 2 or Imagen 3
\bImagen\d\b
# Should be BigQuery
\b(?!BigQuery\b)(?!bigquery\b)[Bb]ig\s?[Qq]uery\b
# Should be DataFrame or DataFrames
\b(?!DataFrames?\b)(?!.*[\(\)\{\}\.,=])(?<!")\b[Dd]ata\s?[Ff]rames?\b(?!")
# Should be Google Cloud
\s[Gg][Cc][Pp]\s
# Should be Google Cloud
\b(?!Google\sCloud\b)[Gg]oogle\s?[Cc]loud\b
# Should be DeepMind
\b(?!DeepMind\b)[Dd]eep\s?[Mm]ind\b
# Should be TensorFlow
\b(?!TensorFlow\b)(?!tensorflow\b)[Tt]ensor\s?[Ff]low\b
# Should be AlloyDB
\b(?!AlloyDB\b)(?!alloydb\b)[Aa]lloy\s?[Dd]\s?[Bb]\b
# Should be Translation API
\bTranslate\s?API\b
# Should be Dialogflow
\bDialogFlow\b
# Should be Firebase
\b(?!Firebase\b)Fire\s?[Bb]ase\b
# Should be Firestore
\b(?!Firestore\b)Fire\s?[Ss]tore\b
# Should be Memorystore
\b(?!Memorystore\b)Memory\s?[Ss]tore\b
# Should be Document AI
\bDoc\s?AI\b
# Should be Agent Search
\bVertex\s?Search\b
# Should be Vector Search
\bVertex\sVector\sSearch\b
# Should be Colab
\s(?!Colab)Co[Ll][Ll]?abs?\b
# Should be TPU or TPUs
\btpus?\b
# Should be GCS
\sgcs\s
# Should be Dataflow ML
\b[Dd]ataflowML\b
# Should be API
\s(?!API)(?!.*[\(\)\{\},=#]+)[Aa][Pp][Ii]\s
# Should be arXiv
\bAr[Xx]iv\b
# Should be DeepEval
\b(?!DeepEval\b)(?!deepeval\b)[Dd]eep\s?[Ee]val\b
# Invalid Space Character
\w \w
# Don't use "smart quotes"
(?<!["'])[‘’“”](?!["'])
# "an" should only be before vowels.
\ban\s+(?![FHLMNRSX][A-Z0-9]+\b)(?!hour\b)(?!honest\b)([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]{1}\w*)
# Don't use Google internal links
((corp|prod|sandbox).google.com|googleplex.com|https?://[0-9a-z][0-9a-z-]+/|(?:^|[^/.-])\b(?:go|b|cl|cr)/[a-z0-9_.-]+\b)
# Use `%pip` instead of `!pip` or `!pip3`
!\s?pip3?
# Don't use embedded images, upload to Google Cloud Storage
\(data:image/(?:jpeg|png);base64,[^{]
# Don't use Gemini 1.X
gemini-1\.[05]
# Use the Google Gen AI SDK `google-genai`
google-generativeai
from google import generativeai
cloud-llm-preview[1-4]
+256
View File
@@ -0,0 +1,256 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
# Automatically suggested patterns
# hit-count: 654 file-count: 86
# https/http/file urls
(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]
# hit-count: 98 file-count: 34
# scala imports
^import (?:[\w.]|\{\w*?(?:,\s*(?:\w*|\*))+\})+
# hit-count: 23 file-count: 6
# version suffix <word>v#
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
# hit-count: 19 file-count: 10
# Python string prefix / binary prefix
# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings
(?<!['"])\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)['"](?=[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})
# hit-count: 19 file-count: 2
# kubernetes object suffix
-[0-9a-f]{10}-\w{5}\s
# hit-count: 14 file-count: 13
# Contributor
\[[^\]]+\]\(https://github\.com/[^/\s"]+/?\)
# hit-count: 13 file-count: 5
# Compiler flags (Windows / PowerShell)
# This is a subset of the more general compiler flags pattern.
# It avoids matching `-Path` to prevent it from being treated as `ath`
(?:^|[\t ,"'`=(])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))
# hit-count: 11 file-count: 10
# hex digits including css/html color classes:
(?:[\\0][xX]|\\u|[uU]\+|#x?|%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\d+)\b
# hit-count: 9 file-count: 4
# node packages
(["'])@[^/'" ]+/[^/'" ]+\g{-1}
# hit-count: 8 file-count: 4
# libraries
\blib(?!rar(?:ies|y))(?=[a-z])
# hit-count: 8 file-count: 3
# AWS VPC
vpc-\w+
# hit-count: 7 file-count: 7
# uuid:
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
# hit-count: 7 file-count: 7
# Compiler flags (Unix, Java/Scala)
# Use if you have things like `-Pdocker` and want to treat them as `docker`
(?:^|[\t ,>"'`=(])-(?:(?:J-|)[DPWXY]|[Ll]|f(?!ix))(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
# hit-count: 7 file-count: 7
# set arguments
\b(?:bash|sh|set)(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)*
# hit-count: 7 file-count: 5
# hex runs
\b[0-9a-fA-F]{16,}\b
# hit-count: 4 file-count: 2
# kubernetes pod status lists
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+
# hit-count: 1 file-count: 1
# integrity
integrity=(['"])(?:\s*sha\d+-[-a-zA-Z=;:/0-9+]{40,})+\g{-1}
# hit-count: 1 file-count: 1
# curl arguments
\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
# pom.xml id fields
<\w+Id>[^<]+</
# Questionably acceptable forms of `in to`
# Personally, I prefer `log into`, but people object
# https://www.tprteaching.com/log-into-log-in-to-login/
\b(?:[Ll]og|[Ss]ign) in to\b
# to opt in
\bto opt in\b
# acceptable duplicates
# ls directory listings
[-bcdlpsw](?:[-r][-w][-SsTtx]){3}[\.+*]?\s+\d+\s+\S+\s+\S+\s+\d+\s+
# mount
\bmount\s+-t\s+(\w+)\s+\g{-1}\b
# C types and repeated CSS values
\s(auto|center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?: \g{-1})+\s
# C struct
\bstruct\s+(\w+)\s+\g{-1}\b
# go templates
\s(\w+)\s+\g{-1}\s+\`(?:graphql|inject|json|yaml):
# doxygen / javadoc / .net
(?:[\\@](?:brief|groupname|link|t?param|return|retval)|(?:public|private|\[Parameter(?:\(.+\)|)\])(?:\s+(?:static|override|readonly|required|virtual))*)(?:\s+\{\w+\}|)\s+(\w+)\s+\g{-1}\s
# Python package registry has incorrect spelling for macOS / Mac OS X
"Operating System :: MacOS :: MacOS X"
# Commit message -- Signed-off-by and friends
^\s*(?:(?:Based-on-patch|Co-authored|Helped|Mentored|Reported|Reviewed|Signed-off)-by|Thanks-to): (?:[^<]*<[^>]*>|[^<]*)\s*$
# Autogenerated revert commit message
^This reverts commit [0-9a-f]{40}\.$
# ignore long runs of a single character:
\b([A-Za-z])\g{-1}{3,}\b
# hit-count: 40 file-count: 8
# Non-English
[a-zA-Z]*[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*|[a-zA-Z]{3,}[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]|[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3,}
(?i)thérèse
# hit-count: 22 file-count: 1
# ANSI color codes
(?:\\(?:u00|x)1[Bb]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+|)m
# hit-count: 15 file-count: 3
# base64 encoded content
([`'"])[-a-zA-Z=;:/0-9+]{3,}=\g{-1}
# hit-count: 11 file-count: 2
# c99 hex digits (not the full format, just one I've seen)
0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP]
# hit-count: 10 file-count: 8
# IServiceProvider / isAThing
(?:\b|_)(?:I|isA)(?=(?:[A-Z][a-z]{2,})+(?:[A-Z]|\b))
# hit-count: 6 file-count: 1
# in check-spelling@v0.0.22+, printf markers aren't automatically consumed
# printf markers
(?<!\\)\\[nrt](?=[a-z]{2,})
# hit-count: 6 file-count: 1
# alternate printf markers if you run into latex and friends
(?<!\\)\\[nrt](?=[a-z]{2,})(?=.*['"`])
# hit-count: 3 file-count: 3
# Google APIs
\b[-a-zA-Z0-9.]*\.googleapis\.com(?:\S*|)
# Jupyter Notebook ID
"id":\s*"[a-zA-Z0-9_-]{4,36}"
# Embedded Images
[A-Za-z0-9+\/=-]{10,}
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
# YouTube url
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
# YouTube music
\bmusic\.youtube\.com/youtubei/v1/browse(?:[?&]\w+=[-a-zA-Z0-9?&=_]*)
# YouTube tag
<\s*youtube\s+id=['"][-a-zA-Z0-9?_]*['"]
# YouTube image
\bimg\.youtube\.com/vi/[-a-zA-Z0-9?&=_]*
# Google Accounts
\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]*
# Google Analytics
\bgoogle-analytics\.com/collect.[-0-9a-zA-Z?%=&_.~]*
# Google APIs
\bgoogleapis\.(?:com|dev)/[a-z]+/(?:v\d+/|)[a-z]+/[-@:./?=\w+|&]+
# Google Storage
\b[-a-zA-Z0-9.]*\bstorage\d*\.googleapis\.com(?:/\S*|)
# Google Calendar
\bcalendar\.google\.com/calendar(?:/u/\d+|)/embed\?src=[@./?=\w&%]+
\w+\@group\.calendar\.google\.com\b
# Google DataStudio
\bdatastudio\.google\.com/(?:(?:c/|)u/\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|)
# The leading `/` here is as opposed to the `\b` above
# ... a short way to match `https://` or `http://` since most urls have one of those prefixes
# Google Docs
/docs\.google\.com/[a-z]+/(?:ccc\?key=\w+|(?:u/\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\?[-\w=#.]*|/\?[\w=&]*|))
# Google Drive
\bdrive\.google\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]*
# Google Groups
\bgroups\.google\.com(?:/[a-z]+/(?:#!|)[^/\s"]+)*
# Google Maps
\bmaps\.google\.com/maps\?[\w&;=]*
# Google themes
themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+.
# Google CDN
\bclients2\.google(?:usercontent|)\.com[-0-9a-zA-Z/.]*
# Goo.gl
/goo\.gl/[a-zA-Z0-9]+
# Google Chrome Store
\bchrome\.google\.com/webstore/detail/[-\w]*(?:/\w*|)
# Google Books
\bgoogle\.(?:\w{2,4})/books(?:/\w+)*\?[-\w\d=&#.]*
# Google Fonts
\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]*
# Google Forms
\bforms\.gle/\w+
# Google Scholar
\bscholar\.google\.com/citations\?user=[A-Za-z0-9_]+
# Google Colab Research Drive
\bcolab\.research\.google\.com/drive/[-0-9a-zA-Z_?=]*
# GitHub SHAs (api)
\bapi.github\.com/repos(?:/[^/\s"]+){3}/[0-9a-f]+\b
# GitHub SHAs (markdown)
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
# GitHub SHAs
\bgithub\.com(?:/[^/\s"]+){2}[@#][0-9a-f]+\b
# GitHub SHA refs
\[([0-9a-f]+)\]\(https://(?:www\.|)github.com/[-\w]+/[-\w]+/commit/\g{-1}[0-9a-f]*
# GitHub wiki
\bgithub\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\b
# githubusercontent
/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]*
# githubassets
\bgithubassets.com/[0-9a-f]+(?:[-/\w.]+)
# gist github
\bgist\.github\.com/[^/\s"]+/[0-9a-f]+
# git.io
\bgit\.io/[0-9a-zA-Z]+
# GitHub JSON
"node_id": "[-a-zA-Z=;:/0-9+_]*"
# GHSA
GHSA(?:-[0-9a-z]{4}){3}
# Twitter/X links
\b(twitter|x)\.com[\w/]+\b
# Hugging Face links
\bhuggingface.co[\w\./-]+\b
# GitHub actions
\buses:\s+[-\w.]+/[-\w./]+@[-\w.]+
# GitLab commit
\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b
# GitLab merge requests
\bgitlab\.[^/\s"]*/\S+/\S+/-/merge_requests/\d+/diffs#[0-9a-f]{40}\b
# GitLab uploads
\bgitlab\.[^/\s"]*/uploads/[-a-zA-Z=;:/0-9+]*
# GitLab commits
\bgitlab\.[^/\s"]*/(?:[^/\s"]+/){2}commits?/[0-9a-f]+\b
# Truncated outputs in Jupyter Notebooks
[A-Za-z]+\.\.\.
# mailto urls
mailto:[-a-zA-Z=;:/?%&0-9+@._]{3,}
+11
View File
@@ -0,0 +1,11 @@
^attache$
^bellow$
benefitting
occurences?
^dependan.*
^oer$
Sorce
^[Ss]pae.*
^untill$
^untilling$
^wether.*
+2
View File
@@ -0,0 +1,2 @@
[flake8]
extend-ignore = E501
+15
View File
@@ -0,0 +1,15 @@
title = "gitleaks config"
[extend]
# useDefault will extend the base configuration with the default gitleaks config:
# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml
useDefault = true
[[rules]]
id = "aws-access-token"
description = "AWS Access Token"
regex = '''AKIA[0-9A-Z]{16}'''
[rules.allowlist]
paths = [
"gemini/use-cases/education/ai_quick_build_experience_backend.ipynb"
]
+4
View File
@@ -0,0 +1,4 @@
{
"id-class-value": false,
"attr-lowercase": false
}
+25
View File
@@ -0,0 +1,25 @@
{
"threshold": 3,
"reporters": ["html", "markdown"],
"ignore": [
"**/node_modules/**",
"**/.git/**",
"**/.rbenv/**",
"**/.venv/**",
"**/*cache*/**",
"**/*.json",
"**/*.yaml",
"**/*.yml",
"**/*.md",
"**/*.html",
"**/*.xml",
"**/*.jpg",
"**/*.png",
"**/*.svg",
"**/*.zip",
"**/*.bin",
"**/noxfile.py",
"**/quickbot/**/*.*",
"**/.nox/**"
]
}
+11
View File
@@ -0,0 +1,11 @@
[flake8]
ignore = ["E501", "E402"]
[mypy]
ignore_missing_imports = true # Effectively ignores "import-not-found"
[pylint]
disable = ["line-too-long", "missing-module-docstring", "import-error", "wrong-import-position", "ungrouped-imports"]
[ruff]
ignore = ["E402"]
+2
View File
@@ -0,0 +1,2 @@
no-inline-html: false
line-length: false
+18
View File
@@ -0,0 +1,18 @@
[mypy]
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
no_implicit_optional = True
check_untyped_defs = True
disallow_subclassing_any = True
warn_incomplete_stub = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_unreachable = True
follow_imports = skip
ignore_missing_imports = True
explicit_package_bases = True
disable_error_code = misc, no-untyped-call, no-any-return
+2
View File
@@ -0,0 +1,2 @@
[MESSAGES CONTROL]
disable=E0401,C0301,R0903,R1710,C0114,R0915,W1514,W1203,I1101
+12
View File
@@ -0,0 +1,12 @@
[sqlfluff]
dialect = bigquery
templater = placeholder
exclude_rules = CV06, RF05
[sqlfluff:templater:placeholder]
param_style = question_mark
[sqlfluff:indentation]
tab_space_size = 2
+12
View File
@@ -0,0 +1,12 @@
{
"rules": {
"terminology": {
"defaultTerms": true,
"exclude": [
"README",
"VS Code",
"website"
]
}
}
}
+29
View File
@@ -0,0 +1,29 @@
name: Assign Issue
on:
issues:
types: [opened, edited]
jobs:
assign:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install Dependencies
run: |
python -m pip install --upgrade PyGithub
- name: Assign Issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python .github/workflows/issue_assigner/assign_issue.py
@@ -0,0 +1,93 @@
"""Assigns the issue based on who created the file mentioned."""
# pylint: disable=line-too-long,too-many-arguments
import base64
import json
import os
import re
from github import Github
def get_issue_number(event_path: str) -> int:
"""Retrieves the issue number from GitHub event data."""
# Load event data
with open(event_path, encoding="utf-8") as f:
event_data = json.load(f)
# Determine the issue number based on the event
if "issue" in event_data:
return int(event_data["issue"]["number"])
raise ValueError("Unable to determine issue number from event data.")
def main() -> None:
"""Gets filename mentioned in issue, finds author username in file, assigns issue to user."""
# Get GitHub token and repository details
repo_name = os.getenv("GITHUB_REPOSITORY", "")
token = os.getenv("GITHUB_TOKEN")
issue_number = get_issue_number(os.getenv("GITHUB_EVENT_PATH", ""))
g = Github(token)
repo = g.get_repo(repo_name)
issue = repo.get_issue(number=issue_number)
# Regex to find any file with an extension
file_match = re.search(r"\b([\w-]+\.[\w]+)\b", issue.body, re.IGNORECASE)
if not file_match:
print("No file found in issue.")
return
file_name = file_match.group(1)
result = g.search_code(f"repo:{repo_name} filename:{file_name}")
if result.totalCount == 0:
result = g.search_code(f"repo:{repo_name} {file_name}")
if result.totalCount == 0:
print(f"No files found for {file_name}")
return
file_path = result[0].path
# Get the commits for the file
commits = repo.get_commits(path=file_path)
# Try to get the author of the first commit
if commits.totalCount > 0:
# The last commit in the list is the first commit
first_commit = commits[commits.totalCount - 1]
if first_commit.author:
username = first_commit.author.login
print(
f"Assigning {username} to Issue #{issue_number} for File {file_path} based on the first commit."
)
issue.add_to_assignees(username)
return
# If the file is a notebook and the first commit author wasn't found,
# check the notebook metadata as a fallback.
if file_name.endswith(".ipynb"):
print(
"Could not determine the first commit author, checking the notebook metadata."
)
file_content_encoded = repo.get_contents(file_path).content
file_content = str(base64.b64decode(file_content_encoded))[:10000]
match = re.search(
r"Author.+https://github\.com/([^/\)]+)", file_content, flags=re.DOTALL
)
if match:
username = match.group(1)
print(
f"Assigning {username} to Issue #{issue_number} for File {file_path} based on notebook metadata."
)
issue.add_to_assignees(username)
return
print(f"No User Found for {file_name}")
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
name: Links
on:
repository_dispatch:
workflow_dispatch:
schedule:
- cron: "00 18 * * *"
jobs:
linkChecker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Link Checker
id: lychee
uses: lycheeverse/lychee-action@v2
- name: Create Issue From File
if: env.lychee_exit_code != 0
uses: peter-evans/create-issue-from-file@v5
with:
title: Link Checker Report
content-filepath: ./lychee/out.md
labels: report, automated issue
+66
View File
@@ -0,0 +1,66 @@
#################################
#################################
## Super Linter GitHub Actions ##
#################################
#################################
name: Lint Code Base
#
# Documentation:
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
#
#############################
# Start the job on all push #
#############################
on:
pull_request:
branches: [main]
###############
# Set the Job #
###############
jobs:
build:
# Name the Job
name: Lint Code Base
# Set the agent to run on
runs-on: ubuntu-latest
##################
# Load all steps #
##################
steps:
##########################
# Checkout the code base #
##########################
- name: Checkout Code
uses: actions/checkout@v6
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
################################
# Run Linter against code base #
################################
- name: Lint Code Base
uses: super-linter/super-linter/slim@v7
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FILTER_REGEX_EXCLUDE: "^(.github/|gemini/sample-apps/finance-advisor-spanner/|gemini/sample-apps/quickbot/).*"
LOG_LEVEL: WARN
SHELLCHECK_OPTS: -e SC1091 -e 2086
VALIDATE_ALL_CODEBASE: false
VALIDATE_PYTHON_PYINK: false
VALIDATE_PYTHON_ISORT: false
VALIDATE_PYTHON_BLACK: false
VALIDATE_CHECKOV: false
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_TYPESCRIPT_STANDARD: false
VALIDATE_JUPYTER_NBQA_FLAKE8: false
VALIDATE_JUPYTER_NBQA_PYLINT: false
VALIDATE_JUPYTER_NBQA_ISORT: false
VALIDATE_JUPYTER_NBQA_MYPY: false
VALIDATE_JUPYTER_NBQA_BLACK: false
VALIDATE_JSCPD: false
+156
View File
@@ -0,0 +1,156 @@
name: Check Spelling
# Comment management is handled through a secondary job, for details see:
# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions
#
# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment
# (in odd cases, it might actually run just to collapse a comment, but that's fairly rare)
# it needs `contents: write` in order to add a comment.
#
# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment
# or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment)
# it needs `pull-requests: write` in order to manipulate those comments.
# Updating pull request branches is managed via comment handling.
# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list
#
# These elements work together to make it happen:
#
# `on.issue_comment`
# This event listens to comments by users asking to update the metadata.
#
# `jobs.update`
# This job runs in response to an issue_comment and will push a new commit
# to update the spelling metadata.
#
# `with.experimental_apply_changes_via_bot`
# Tells the action to support and generate messages that enable it
# to make a commit to update the spelling metadata.
#
# `with.ssh_key`
# In order to trigger workflows when the commit is made, you can provide a
# secret (typically, a write-enabled github deploy key).
#
# For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key
# Sarif reporting
#
# Access to Sarif reports is generally restricted (by GitHub) to members of the repository.
#
# Requires enabling `security-events: write`
# and configuring the action with `use_sarif: 1`
#
# For information on the feature, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Sarif-output
# Minimal workflow structure:
#
# on:
# push:
# ...
# pull_request_target:
# ...
# jobs:
# # you only want the spelling job, all others should be omitted
# spelling:
# # remove `security-events: write` and `use_sarif: 1`
# # remove `experimental_apply_changes_via_bot: 1`
# ... otherwise adjust the `with:` as you wish
on:
pull_request:
branches:
- "**"
types:
- "opened"
- "reopened"
- "synchronize"
jobs:
spelling:
name: Check Spelling
permissions:
contents: read
actions: read
security-events: write
outputs:
followup: ${{ steps.spelling.outputs.followup }}
runs-on: ubuntu-latest
if: ${{ contains(github.event_name, 'pull_request') || github.event_name == 'push' }}
concurrency:
group: spelling-${{ github.event.pull_request.number || github.ref }}
# note: If you use only_check_changed_files, you do not want cancel-in-progress
cancel-in-progress: false
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@main
with:
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
checkout: true
check_file_names: 1
spell_check_this: check-spelling/spell-check-this@main
post_comment: 0
use_magic_file: 1
report-timing: 1
warnings: bad-regex,binary-file,deprecated-feature,ignored-expect-variant,large-file,limited-references,no-newline-at-eof,noisy-file,non-alpha-in-dictionary,token-is-substring,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,no-files-to-check,unclosed-block-ignore-begin,unclosed-block-ignore-end
experimental_apply_changes_via_bot: 1
dictionary_source_prefixes: '{"cspell": "https://raw.githubusercontent.com/streetsidesoftware/cspell-dicts/main/dictionaries/"}'
extra_dictionaries: |
cspell:aws/dict/aws.txt
cspell:bash/samples/bash-words.txt
cspell:companies/dict/companies.txt
cspell:css/dict/css.txt
cspell:data-science/dict/data-science-models.txt
cspell:data-science/dict/data-science.txt
cspell:data-science/dict/data-science-tools.txt
cspell:dart/src/dart.txt
cspell:de_DE/src/German_de_DE.dic
cspell:django/requirements.txt
cspell:django/dict/django.txt
cspell:docker/src/docker-words.txt
cspell:en_shared/dict/acronyms.txt
cspell:en_shared/dict/shared-additional-words.txt
cspell:en_GB/en_GB.trie
cspell:en_US/en_US.trie
cspell:filetypes/src/filetypes.txt
cspell:flutter/src/flutter.txt
cspell:fonts/dict/fonts.txt
cspell:fr_FR/fr-fr.trie
cspell:fullstack/dict/fullstack.txt
cspell:golang/dict/go.txt
cspell:google/dict/google.txt
cspell:html/dict/html.txt
cspell:it_IT/dict/it-it.trie
cspell:java/src/java.txt
cspell:k8s/dict/k8s.txt
cspell:mnemonics/dict/mnemonics.txt
cspell:monkeyc/src/monkeyc_keywords.txt
cspell:node/dict/node.txt
cspell:npm/dict/npm.txt
cspell:people-names/dict/people-names.txt
cspell:php/dict/php.txt
cspell:python/dict/python.txt
cspell:python/dict/python-common.txt
cspell:shell/dict/shell-all-words.txt
cspell:software-terms/dict/softwareTerms.txt
cspell:software-terms/dict/webServices.txt
cspell:sql/src/common-terms.txt
cspell:sql/src/sql.txt
cspell:sql/src/tsql.txt
cspell:svelte/dict/svelte.txt
cspell:terraform/dict/terraform.txt
cspell:typescript/dict/typescript.txt
check_extra_dictionaries:
cspell:cryptocurrencies/dict/cryptocurrencies.txt
cspell:gaming-terms/dict/gaming-terms.txt
cspell:latex/dict/latex.txt
cspell:public-licenses/src/additional-licenses.txt
cspell:public-licenses/src/generated/public-licenses.txt
ignore-pattern: "[^'a-záéíóúñçüA-ZÁÉÍÓÚÑÇÜ]"
upper-pattern: "[A-ZÁÉÍÓÚÑÇÜ]"
lower-pattern: "[a-záéíóúñçü]"
not-lower-pattern: "[^a-záéíóúñçü]"
not-upper-or-lower-pattern: "[^A-ZÁÉÍÓÚÑÇÜa-záéíóúñçü]"
punctuation-pattern: "'"
only_check_changed_files: true
longest_word: "10"
+191
View File
@@ -0,0 +1,191 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.pyc
*$py.class
**/dist
/tmp
/out-tsc
/bazel-out
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
Pipfile.lock
Pipfile
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
.venv*
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# ruff cache
.ruff_cache/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# macOS
.DS_Store
# PyCharm
.idea
# User-specific files
language/examples/prompt-design/train.csv
README-TOC*.md
# Terraform
terraform.tfstate**
.terraform*
.Terraform*
tmp*
# Node
**/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.history/*
# Miscellaneous
**/.angular/*
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
.agent/*
tools/llmevalkit/uv.lock
agent-development-answers.yml
+6
View File
@@ -0,0 +1,6 @@
/github/workspace/gemini/use-cases/education/use_cases_for_education.ipynb:aws-access-token:2802
/github/workspace/language/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:308
/github/workspace/vision/use-cases/creating_high_quality_visual_assets_with_gemini_and_imagen.ipynb:facebook-page-access-token:309
/github/workspace/language/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:310
/github/workspace/gemini/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:322
/github/workspace/language/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:297
+3
View File
@@ -0,0 +1,3 @@
{
"bracketSameLine": true
}
+8
View File
@@ -0,0 +1,8 @@
{
"default_version": "v1",
"name": "generative-ai",
"name_pretty": "Google Cloud Generative AI Samples",
"issue_tracker": "https://github.com/GoogleCloudPlatform/generative-ai/issues",
"language": "python",
"repo": "GoogleCloudPlatform/generative-ai"
}
+133
View File
@@ -0,0 +1,133 @@
line-length = 88
indent-width = 4 # Google Style Guide §3.4: 4 spaces
target-version = "py310" # Minimum Python version
[lint]
ignore = [
"COM812", # Trailing comma missing.
"FBT001", # Boolean positional arg in function definition
"FBT002", # Boolean default value in function definition
"D203", # 1 blank line required before class docstring (Google: 0)
"D213", # Multi-line docstring summary should start at the second line (Google: first line)
"D100", # Ignore Missing docstring in public module (often desired at top level __init__.py)
"D104", # Ignore Missing docstring in public package (often desired at top level __init__.py)
"D107", # Ignore Missing docstring in __init__ (use class docstring)
"TC006", # Ignore unquoted type expressions in typing.cast() calls.
"TD002", # Ignore Missing author in TODOs (often not required)
"TD003", # Ignore Missing issue link in TODOs (often not required/available)
"T201", # Ignore print presence
"RUF012", # Ignore Mutable class attributes should be annotated with `typing.ClassVar`
"E501", # Ignore line length (handled by Ruff's dynamic line length)
"UP007", # Do not upgrade `Optional[T]` to `T | None` (PEP 604 syntax)
"ANN002",
"ANN003",
"ANN401",
"TRY003",
"G004",
"TRY201",
]
select = [
"E", # pycodestyle errors (PEP 8)
"W", # pycodestyle warnings (PEP 8)
"F", # Pyflakes (logical errors, unused imports/variables)
"I", # isort (import sorting - Google Style §3.1.2)
"D", # pydocstyle (docstring conventions - Google Style §3.8)
"N", # pep8-naming (naming conventions - Google Style §3.16)
"UP", # pyupgrade (use modern Python syntax)
"ANN",# flake8-annotations (type hint usage/style - Google Style §2.22)
"A", # flake8-builtins (avoid shadowing builtins)
"B", # flake8-bugbear (potential logic errors & style issues - incl. mutable defaults B006, B008)
"C4", # flake8-comprehensions (unnecessary list/set/dict comprehensions)
"ISC",# flake8-implicit-str-concat (disallow implicit string concatenation across lines)
"T20",# flake8-print (discourage `print` - prefer logging)
"SIM",# flake8-simplify (simplify code, e.g., `if cond: return True else: return False`)
"PTH",# flake8-use-pathlib (use pathlib instead of os.path where possible)
"PL", # Pylint rules ported to Ruff (PLC, PLE, PLR, PLW)
"PIE",# flake8-pie (misc code improvements, e.g., no-unnecessary-pass)
"RUF",# Ruff-specific rules (e.g., RUF001-003 ambiguous unicode, RUF013 implicit optional)
"RET",# flake8-return (consistency in return statements)
"SLF",# flake8-self (check for private member access via `self`)
"TID",# flake8-tidy-imports (relative imports, banned imports - configure if needed)
"YTT",# flake8-boolean-trap (checks for boolean positional arguments, truthiness tests - Google Style §3.10)
"TD", # flake8-todos (check TODO format - Google Style §3.7)
"TCH",# flake8-type-checking (helps manage TYPE_CHECKING blocks and imports)
"PYI",# flake8-pyi (best practices for .pyi stub files, some rules are useful for .py too)
"S", # flake8-bandit (security issues)
"DTZ",# flake8-datetimez (timezone-aware datetimes)
"ERA",# flake8-eradicate (commented-out code)
"Q", # flake8-quotes (quote style consistency)
"RSE",# flake8-raise (modern raise statements)
"TRY",# tryceratops (exception handling best practices)
"PERF",# perflint (performance anti-patterns)
"BLE",
"T10",
"ICN",
"G",
"FIX",
"ASYNC",
"INP",
]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
[lint.isort]
#force-sort-within-sections = true
#combine-as-imports = true
case-sensitive = true
#force-single-line = false
#known-first-party = []
#known-third-party = []
lines-after-imports = 1
#lines-between-types = 1
#no-lines-before = ["LOCALFOLDER"]
#required-imports = []
#section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
[lint.pydocstyle]
convention = "google"
ignore-decorators = ["typing.overload", "abc.abstractmethod"]
[lint.flake8-annotations]
mypy-init-return = true
allow-star-arg-any = false
[lint.pep8-naming]
ignore-names = ["test_*", "setUp", "tearDown", "mock_*"]
classmethod-decorators = ["classmethod", "pydantic.validator", "pydantic.root_validator"]
staticmethod-decorators = ["staticmethod"]
[lint.flake8-tidy-imports]
ban-relative-imports = "all" # Google generally prefers absolute imports (§3.1.2)
[lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
[format]
docstring-code-format = true
docstring-code-line-length = "dynamic" # Or set to 80
quote-style = "double"
indent-style = "space"
+23
View File
@@ -0,0 +1,23 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "always",
"source.fixAll.ruff": "explicit"
}
},
"ruff.importStrategy": "fromEnvironment",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"editor.rulers": [
88
]
}
+43
View File
@@ -0,0 +1,43 @@
# Contributor Code of Conduct
As contributors and maintainers of this project,
and in the interest of fostering an open and welcoming community,
we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project
a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information,
such as physical or electronic
addresses, without explicit permission
- Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct.
By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct
may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue
or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
+84
View File
@@ -0,0 +1,84 @@
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Notebook Template
If you're creating a Jupyter Notebook, use [`notebook_template.ipynb`](notebook_template.ipynb) as a template.
## Code Quality Checks
All notebooks in this project are checked for formatting and style, to ensure a
consistent experience. To test notebooks prior to submitting a pull request,
you can follow these steps.
From a command-line terminal (e.g. from Workbench or locally),
run this code block to format your code.
If the fixes can't be performed automatically,
then you will need to manually address them before submitting your PR.
```shell
uv pip install autoflake ruff nbqa "nbformat>=5.10.4" "git+https://github.com/tensorflow/docs"
./scripts/format.sh
```
## Code Reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
## Contributor Guide
If you are new to contributing to open source, you can find helpful information in this contributor guide.
You may follow these steps to contribute:
1. **Fork the official repository.** This will create a copy of the official repository in your own account.
2. **Sync the branches.** This will ensure that your copy of the repository is up-to-date with the latest changes from the official repository.
3. **Work on your forked repository's feature branch.** This is where you will make your changes to the code.
4. **Commit your updates on your forked repository's feature branch.** This will save your changes to your copy of the repository.
5. **Submit a pull request to the official repository's main branch.** This will request that your changes be merged into the official repository.
6. **Resolve any linting errors.** This will ensure that your changes are formatted correctly.
- For errors generated by [check-spelling](https://github.com/check-spelling/check-spelling), go to the [Job Summary](https://github.com/GoogleCloudPlatform/generative-ai/actions/workflows/spelling.yaml) to read the errors.
- Fix any spelling errors that are found.
- Forbidden Patterns are defined as regular expressions, you can copy/paste them into many IDEs to find the instances. [Example for Visual Studio Code](https://medium.com/@nikhilbaxi3/visual-studio-code-secrets-of-regular-expression-search-71723c2ecbd2).
- Add false positives to [`.github/actions/spelling/allow.txt`](.github/actions/spelling/allow.txt). Be sure to check that it's actually spelled correctly!
Here are some additional things to keep in mind during the process:
- **Read the [Google's Open Source Community Guidelines](https://opensource.google/conduct/).** The contribution guidelines will provide you with more information about the project and how to contribute.
- **Test your changes.** Before you submit a pull request, make sure that your changes work as expected.
- **Be patient.** It may take some time for your pull request to be reviewed and merged.
---
## For Google Employees
Complete the following steps to register your GitHub account and be added as a contributor to this repository.
1. Register your GitHub account at [go/GitHub](http://go/github)
1. Once you have registered, go to [go/github-googlecloudplatform](http://go/github-googlecloudplatform) and request to join the GoogleCloudPlatform organization. Check the box "I need write access on a public repository".
1. You'll receive an email to your GitHub registered email to approve the request to join. Approve it.
1. Request to join this team [GoogleCloudPlatform/teams/generative-ai-samples-contributors](https://github.com/orgs/GoogleCloudPlatform/teams/generative-ai-samples-contributors/members)
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+138
View File
@@ -0,0 +1,138 @@
# Generative AI on Google Cloud
> **[Gemini Enterprise Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform)**, the latest evolution of Vertex AI, has been released!
>
> Check out the [`Google-Cloud-AI/agent-platform`](https://goo.gle/agent-platform-github) repository for a curated list of assets for agent building on Google Cloud.
<!-- markdownlint-disable MD033 -->
This repository contains notebooks, code samples, sample apps, and other resources that demonstrate how to use, develop and manage generative AI workflows using [Generative AI](https://cloud.google.com/ai/generative-ai) with [Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform).
## Intro Video
[![What is Gemini Enterprise Agent Platform?](https://img.youtube.com/vi/j8qW5poBkEU/maxresdefault.jpg)](https://goo.gle/agent-platform-video)
<table>
<tr>
<th></th>
<th style="text-align: center;">Description</th>
</tr>
<tr>
<td>
<img src="https://storage.googleapis.com/github-repo/img/gemini/Spark__Gradient_Alpha_100px.gif" width="45px" alt="Gemini">
<br>
<a href="gemini/"><code>gemini/</code></a>
</td>
<td>
Discover Gemini through starter notebooks, use cases, function calling, sample apps, and more.
</td>
</tr>
<tr>
<td>
<img src="https://www.gstatic.com/images/branding/gcpiconscolors/service_discovery/v1/24px.svg" width="40px" alt="Search">
<br>
<a href="search/"><code>search/</code></a>
</td>
<td>Use this folder if you're interested in using <a href="https://cloud.google.com/enterprise-search">Agent Search</a>, a Google-managed solution to help you rapidly build search engines for websites and across enterprise data. (Formerly known as Enterprise Search on Generative AI App Builder).</td>
</tr>
<tr>
<td>
<img src="https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/nature_people/default/40px.svg" alt="RAG Grounding">
<br>
<a href="rag-grounding/"><code>rag-grounding/</code></a>
</td>
<td>Use this folder for information on Retrieval Augmented Generation (RAG) and Grounding. This is an index of notebooks and samples across other directories focused on this topic.</td>
</tr>
<tr>
<td>
<img src="https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/image/default/40px.svg" alt="Vision">
<br>
<a href="vision/"><code>vision/</code></a>
</td>
<td>
Use this folder if you're interested in building your own solutions from scratch using features from Imagen and Veo.
</td>
</tr>
<tr>
<td>
<img src="https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/mic/default/40px.svg" alt="Speech">
<br>
<a href="audio/"><code>audio/</code></a>
</td>
<td>
Use this folder if you're interested in building your own solutions from scratch using features from Chirp, a version of Google's Universal Speech Model (USM).
</td>
</tr>
<tr>
<td>
<img src="https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/build/default/40px.svg" alt="Setup Env">
<br>
<a href="setup-env/"><code>setup-env/</code></a>
</td>
<td>Instructions on how to set up Google Cloud, the Gen AI Python SDK, and notebook environments on Google Colab and Workbench.</td>
</tr>
<tr>
<td>
<img src="https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/media_link/default/40px.svg" alt="Resources">
<br>
<a href="RESOURCES.md"><code>RESOURCES.md</code></a>
</td>
<td>Learning resources (e.g. blogs, YouTube playlists) about Generative AI on Google Cloud.</td>
</tr>
</table>
<!-- markdownlint-enable MD033 -->
## Related Repositories
- ✨ [Agent Development Kit (ADK) Samples](https://github.com/google/adk-samples): This repository provides ready-to-use agents built on top of the Agent Development Kit, designed to accelerate your development process. These agents cover a range of common use cases and complexities, from simple conversational bots to complex multi-agent workflows.
- [🚀 Agent Starter Pack](https://github.com/GoogleCloudPlatform/agent-starter-pack)
- A collection of production-ready Generative AI Agent templates built for Google Cloud.
- It accelerates development by providing a holistic, production-ready solution, addressing common challenges (Deployment & Operations, Evaluation, Customization, Observability) in building and deploying Gen AI agents.
- [Gemini Cookbook](https://github.com/google-gemini/cookbook/)
- [genai-factory](https://github.com/googleCloudPlatform/genai-factory) - A collection of end-to-end infrastructure blueprints to deploy generative AI infrastructures in GCP, using IaC and following security best-practices.
- [Google Cloud Applied AI Engineering](https://github.com/GoogleCloudPlatform/applied-ai-engineering-samples)
- [Vertex AI GenMedia Creative Studio](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio) - Experience Google's generative media foundational models + custom workflows.
- [MCP Servers for GenMedia](https://goo.gle/vertex-genmedia-mcp) - Empower your agents with generative media tools.
- [Generative AI for Marketing using Google Cloud](https://github.com/GoogleCloudPlatform/genai-for-marketing)
- [Generative AI for Developer Productivity](https://github.com/GoogleCloudPlatform/genai-for-developers)
- Vertex AI Core
- [Vertex AI Samples](https://github.com/GoogleCloudPlatform/vertex-ai-samples)
- [MLOps with Vertex AI](https://github.com/GoogleCloudPlatform/mlops-with-vertex-ai)
- [Developing NLP solutions with T5X and Vertex AI](https://github.com/GoogleCloudPlatform/t5x-on-vertex-ai)
- [AlphaFold batch inference with Vertex AI Pipelines](https://github.com/GoogleCloudPlatform/vertex-ai-alphafold-inference-pipeline)
- [Serving Spark ML models using Vertex AI](https://github.com/GoogleCloudPlatform/vertex-ai-spark-ml-serving)
- [Sensitive Data Protection (Cloud DLP) for Vertex AI Generative Models (PaLM2)](https://github.com/GoogleCloudPlatform/Sensitive-Data-Protection-for-Vertex-AI-PaLM2)
- Conversational AI
- [Contact Center AI Samples](https://github.com/GoogleCloudPlatform/contact-center-ai-samples)
- [Reimagining Customer Experience 360](https://github.com/GoogleCloudPlatform/dialogflow-ccai-omnichannel)
- Document AI
- [Document AI Samples](https://github.com/GoogleCloudPlatform/document-ai-samples)
- Gemini in Google Cloud
- [Cymbal Superstore](https://github.com/GoogleCloudPlatform/cymbal-superstore)
- Cloud Databases
- [Gen AI Databases Retrieval App](https://github.com/GoogleCloudPlatform/genai-databases-retrieval-app)
- Other
- [ai-on-gke](https://github.com/GoogleCloudPlatform/ai-on-gke)
- [ai-infra-cluster-provisioning](https://github.com/GoogleCloudPlatform/ai-infra-cluster-provisioning)
- [solutions-genai-llm-workshop](https://github.com/GoogleCloudPlatform/solutions-genai-llm-workshop)
- [terraform-genai-doc-summarization](https://github.com/GoogleCloudPlatform/terraform-genai-doc-summarization)
- [terraform-genai-knowledge-base](https://github.com/GoogleCloudPlatform/terraform-genai-knowledge-base)
- [genai-product-catalog](https://github.com/GoogleCloudPlatform/genai-product-catalog)
- [solutionbuilder-terraform-genai-doc-summarization](https://github.com/GoogleCloudPlatform/solutionbuilder-terraform-genai-doc-summarization)
- [solutions-viai-edge-provisioning-configuration](https://github.com/GoogleCloudPlatform/solutions-viai-edge-provisioning-configuration)
- [mis-ai-accelerator](https://github.com/GoogleCloudPlatform/mis-ai-accelerator)
- [dataflow-opinion-analysis](https://github.com/GoogleCloudPlatform/dataflow-opinion-analysis)
- [genai-beyond-basics](https://github.com/meteatamel/genai-beyond-basics)
- [Gemini by Example](https://geminibyexample.com)
## Contributing
Contributions welcome! See the [Contributing Guide](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/CONTRIBUTING.md).
## Getting help
Please use the [issues page](https://github.com/GoogleCloudPlatform/generative-ai/issues) to provide suggestions, feedback or submit a bug report.
## Disclaimer
This repository itself is not an officially supported Google product. The code in this repository is for demonstrative purposes only.
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`GoogleCloudPlatform/generative-ai`
- 原始仓库:https://github.com/GoogleCloudPlatform/generative-ai
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+109
View File
@@ -0,0 +1,109 @@
# Google Generative AI resources
A collection of resources that helps everyone learn more about Google Generative AI offerings.
Please submit additional resources via a PR.
## Generative AI on Google Cloud
- [Generative AI on Google Cloud](https://cloud.google.com/ai/generative-ai)
- [Overview of Generative AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/overview)
- [Certifications and security controls](https://cloud.google.com/vertex-ai/generative-ai/docs/security-controls)
- [Responsible AI best practices](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/responsible-ai)
- [Quotas and Limits](https://cloud.google.com/vertex-ai/generative-ai/docs/quotas)
- [Pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models)
## Products
### Agent Platform
- Use [Agent Platform](https://cloud.google.com/vertex-ai) to interact with, customize, and embed foundation models into your applications
- Access foundation models on [Model Garden](https://cloud.google.com/model-garden)
- Tune models via a simple UI on [Agent Studio](https://cloud.google.com/generative-ai-studio)
- Use [Agent Search](https://cloud.google.com/products/agent-builder), to build and deploy AI agents grounded in their data
- Google foundation models
- [Gemini API models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)
- [Imagen API models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#imagen-models)
- [MedLM API models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#medlm-models)
### Gemini Models
- [Gemini](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/overview), a multimodal model from Google DeepMind, is capable of understanding virtually any input, combining different types of information, and generating almost any output.
- Prompt and test Gemini in [Agent Platform](https://console.cloud.google.com/freetrial/?redirectPath=%2Fvertex-ai%2Fgenerative%2Fmultimodal%2Fcreate%2Ftext) using text, images, video, or code.
### Gemini for Google Cloud
- [Gemini Code Assist](https://cloud.google.com/products/gemini/code-assist) offers AI-powered assistance to help developers build applications with higher velocity, quality, and security in popular code editors like VS Code and JetBrains, and on developer platforms like Firebase.
- [Gemini for Google Cloud](https://cloud.google.com/products/gemini) offerings assist users in working and coding more effectively, gaining deeper data insights, navigating security challenges, and more.
## Get Started
- Quickstart tutorial using [Agent Studio](https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart) or the [Agent Platform API](https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart-multimodal)
- Explore pretrained models in [Model Garden](https://cloud.google.com/vertex-ai/docs/start/explore-models)
- [Tune a Foundation model](https://cloud.google.com/vertex-ai/generative-ai/docs/models/tune-models)
## Video Courses
- [Real Terms for AI](https://goo.gle/AIwordsExplained)
- [AI Guide for Cloud Developers](https://www.youtube.com/playlist?list=PLIivdWyY5sqJio2yeg1dlfILOUO2FoFRx)
- [Intro to Gen AI - Playlist](https://www.youtube.com/playlist?list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK)
- [Introduction to Generative AI](https://www.youtube.com/watch?v=cZaNf2rA30k&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=1&pp=iAQB)
- [Introduction to Large Language Models](https://www.youtube.com/watch?v=RBzXsQHjptQ&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=2&pp=iAQB)
- [Introduction to Responsible AI](https://www.youtube.com/watch?v=w_3L1Bf2P_g&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=3&pp=iAQB)
- [Intro to Gen AI training course from Google Cloud](https://www.youtube.com/watch?v=9Eh7gsIH5h4&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=4&pp=iAQB)
- [Gen AI for Developers](https://www.youtube.com/watch?v=JR9Gdo-_lx8&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20)
- [Introduction to Image Generation](https://www.youtube.com/watch?v=JR9Gdo-_lx8&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=1&pp=iAQB)
- [Attention Mechanism: Overview](https://www.youtube.com/watch?v=8PmOaVYVeKY&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=2&pp=iAQB)
- [Encoder-Decoder Architecture: Overview](https://www.youtube.com/watch?v=671xny8iows&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=3&pp=iAQB)
- [Transformer Models and BERT Model: Overview](https://www.youtube.com/watch?v=hsp1OAcoLBY&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=4&pp=iAQB)
- [Create Image Captioning Models: Overview](https://www.youtube.com/watch?v=0BaIeMoFEoE&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=5&pp=iAQB)
- [Introduction to Agent Studio](https://www.youtube.com/watch?v=KWarqNq195M&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=6&pp=iAQB)
- [Vector Search and Embeddings](https://www.youtube.com/watch?v=YlAWtEAJl9g&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=7&pp=iAQB)
- [Gemini for Google Cloud - Playlist](https://www.youtube.com/playlist?list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F)
- [Gemini for Application Developers](https://www.youtube.com/watch?v=WsXVGr0Q3C4&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=1&pp=iAQB)
- [Gemini for Cloud Architects](https://www.youtube.com/watch?v=y-dlxWHtfhQ&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=2&pp=iAQB)
- [Gemini for Data Scientists and Analysts](https://www.youtube.com/watch?v=0H7brO5JeCQ&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=3&pp=iAQB)
- [Gemini for Network Engineers](https://www.youtube.com/watch?v=RHla4EEleCE&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=4&pp=iAQB)
- [Gemini for Security Engineers](https://www.youtube.com/watch?v=08xeOzUUL-g&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=5&pp=iAQB)
- [Gemini for Cloud DevOps Engineers](https://www.youtube.com/watch?v=zaVTJVwtyzI&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=6&pp=iAQB)
- [Gemini end-to-end Software Development Lifecycle](https://www.youtube.com/watch?v=h41eoDraUzE&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=7&pp=iAQB)
- [Technical training courses in Generative AI from Google Cloud](https://www.youtube.com/watch?v=5FIlXPSmUUc&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=8&pp=iAQB)
## Cloud Skills Boost
- [Beginner: Introduction to Generative AI Learning Path](https://www.cloudskillsboost.google/paths/118): This learning path provides an overview of generative AI concepts, from the fundamentals of large language models to responsible AI principles. This learning path currently has several courses listed below:
- [Introduction to Generative AI](https://www.cloudskillsboost.google/course_templates/536)
- [Introduction to Large Language Models](https://www.cloudskillsboost.google/course_templates/539)
- [Introduction to Responsible AI](https://www.cloudskillsboost.google/course_templates/554)
- [Prompt Design](https://www.cloudskillsboost.google/paths/118/course_templates/976)
- [Responsible AI: Applying AI Principles with Google Cloud](https://www.cloudskillsboost.google/paths/118/course_templates/388)
- [Intermediate: Gemini for Google Cloud Learning Path](https://www.cloudskillsboost.google/paths/236): The Gemini for Google Cloud learning path provides examples of how Gemini can help make engineers of all types more efficient in their daily activities.
- [Gemini for Application Developers](https://www.cloudskillsboost.google/course_templates/881)
- [Gemini for Cloud Architects](https://www.cloudskillsboost.google/paths/236/course_templates/878)
- [Gemini for Data Scientists and Analysts](https://www.cloudskillsboost.google/paths/236/course_templates/879)
- [Gemini for Network Engineers](https://www.cloudskillsboost.google/paths/236/course_templates/884)
- [Gemini for Security Engineers](https://www.cloudskillsboost.google/paths/236/course_templates/886)
- [Gemini for DevOps Engineers](https://www.cloudskillsboost.google/paths/236/course_templates/882)
- [Gemini for end-to-end SDLC](https://www.cloudskillsboost.google/course_templates/885)
- [Develop Gen AI Apps with Gemini and Streamlit](https://www.cloudskillsboost.google/paths/236/course_templates/978)
- [Advanced: Generative AI for Developers Learning Path](https://www.cloudskillsboost.google/paths/183): A Generative AI Learning Path with a technical focus, built for App Developers, Machine Learning Engineers, and Data Scientists. Recommended prerequisite: Introduction to Generative AI learning path.
- [Introduction to Image Generation](https://www.cloudskillsboost.google/paths/183/course_templates/541)
- [Attention Mechanism](https://www.cloudskillsboost.google/paths/183/course_templates/537)
- [Encoder-Decoder Architecture](https://www.cloudskillsboost.google/paths/183/course_templates/543)
- [Transformer Models and BERT Model](https://www.cloudskillsboost.google/paths/183/course_templates/538)
- [Create Image Captioning Models](https://www.cloudskillsboost.google/paths/183/course_templates/542)
- [Introduction to Agent Studio](https://www.cloudskillsboost.google/paths/183/course_templates/552)
- [Vector Search and Embeddings](https://www.cloudskillsboost.google/paths/183/course_templates/939)
- [Inspect Rich Documents with Gemini Multimodality and Multimodal RAG](https://www.cloudskillsboost.google/paths/183/course_templates/981)
- [Responsible AI for Developers: Fairness and Bias](https://www.cloudskillsboost.google/paths/183/course_templates/985)
- [Responsible AI for Developers: Interpretability & Transparency](https://www.cloudskillsboost.google/paths/183/course_templates/989)
- [Machine Learning Operations (MLOps) for Generative AI](https://www.cloudskillsboost.google/paths/183/course_templates/927)
## Google Cloud Playlists/Videos
- [Generative AI - Google Cloud Next 2024](https://www.youtube.com/watch?v=PyM3Vt6s1GI&list=PLIivdWyY5sqLHERDVwseyPGka96mCiwpq)
- [AI and Machine Learning with Google Cloud](https://www.youtube.com/watch?v=V6YTS5aofqU&list=PLIivdWyY5sqJdmVMjLI8iCul14XkTRosn)
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
+31
View File
@@ -0,0 +1,31 @@
# Agents
This directory contains samples and resources for building AI agents with Google Cloud.
## Subdirectories
### adk
The `adk` directory contains information about the Agent Development Kit (ADK).
### agent_engine
The `agent_engine` directory contains notebooks that demonstrate how to use the Agent Engine on Vertex AI.
**Notebooks:**
- [Get started with Vertex AI Memory Bank](agent_engine/memory_bank/get_started_with_memory_bank.ipynb): Learn how to use Vertex AI Memory Bank to build stateful, context-aware conversational AI agents.
- [A2A on Agent Engine](agent_engine/tutorial_a2a_on_agent_engine.ipynb): A tutorial on how to run a simple agent on Agent Engine.
- [Claude with ADK on Agent Engine](agent_engine/tutorial_claude_with_adk_on_agent_engine.ipynb): A tutorial on using the Claude model with ADK on Agent Engine.
- [Get Started with Code Execution](agent_engine/tutorial_get_started_with_code_execution.ipynb): A tutorial on how to get started with code execution on Agent Engine.
- [Get Started with Live API on Agent Engine](agent_engine/tutorial_get_started_with_live_api_on_agent_engine.ipynb): A tutorial on how to get started with a live API on Agent Engine.
- [MCP on Agent Engine](agent_engine/tutorial_mcp_on_agent_engine.ipynb): A tutorial on how to use the Multi-turn Conversation Platform (MCP) on Agent Engine.
### gemini_data_analytics
The `gemini_data_analytics` directory contains notebooks that demonstrate how to use Gemini for data analytics.
**Notebooks:**
- [Intro to Gemini Data Analytics (HTTP)](gemini_data_analytics/intro_gemini_data_analytics_http.ipynb): An introduction to using Gemini for data analytics with the HTTP API.
- [Intro to Gemini Data Analytics (SDK)](gemini_data_analytics/intro_gemini_data_analytics_sdk.ipynb): An introduction to using Gemini for data analytics with the Python SDK.
+6
View File
@@ -0,0 +1,6 @@
⚠️ The Agent Development Kit (ADK) Sample Agents repository is available at [github.com/google/adk-samples](https://github.com/google/adk-samples).
The repository contains sample agents for:
- [Python](https://github.com/google/adk-samples/tree/main/python)
- [Java](https://github.com/google/adk-samples/tree/main/java)
@@ -0,0 +1,157 @@
# Architecture: Contract Compliance Multi-Agent Team
This document describes the executable architecture in the repo today. The live demo is a local cross-language system:
- Browser cockpit served by Python at `/live-compliance/`
- Python FastAPI service on `127.0.0.1:8000`
- Go A2A compliance service on `:8888`
- ADK `RemoteA2aAgent` handoff through the Go Agent Card and A2A JSON-RPC `SendMessage`
The Go service is deterministic by design. It is not an LLM agent; it enforces policy thresholds that need repeatable audit behavior.
## Runtime Flow
```mermaid
flowchart LR
UI["Browser Cockpit<br/>/live-compliance/"]
API["Python FastAPI<br/>/api/compliance/upload"]
EXTRACT["Deterministic Extraction<br/>tools.py"]
ADK["ADK RemoteA2aAgent<br/>fast_api_app.py"]
CARD["Go Agent Card<br/>/.well-known/agent.json"]
RPC["Go JSON-RPC<br/>SendMessage"]
CHECK["Policy Checker<br/>checker.go"]
ART["Case + HTML Artifacts<br/>live_compliance.py"]
UI -->|"sample text + policy + simulator state"| API
API --> EXTRACT
EXTRACT --> ADK
ADK -->|"GET Agent Card"| CARD
ADK -->|"POST JSON-RPC SendMessage"| RPC
RPC --> CHECK
CHECK -->|"passed / violations / timestamp"| RPC
RPC --> ADK
ADK --> API
API --> ART
ART --> UI
```
## Live Request Path
1. The browser selects a bundled contract fixture from `sample-contracts/`.
2. The browser fetches fixture text from `/api/compliance/sample-contracts/{filename}`.
3. The browser posts the text, active policy values, and simulator settings to `/api/compliance/upload`.
4. Python extracts contract fields with deterministic parsing in `tools.py`.
5. Python classifies risk and builds an A2A data payload.
6. Python creates a focused `RemoteA2aAgent` in `fast_api_app.py`.
7. ADK resolves the Go Agent Card from `GO_AGENT_CARD_URL`.
8. ADK sends A2A JSON-RPC `SendMessage` to the Go service.
9. Go decodes the data part, applies deterministic policy checks, and returns a completed A2A task.
10. Python stores the case state, generates HTML artifacts, and returns the UI-visible payload.
## A2A Payload Shape
Python builds the UI-visible request envelope in `build_go_message_payload(...)`:
```json
{
"jsonrpc": "2.0",
"id": "case-{case_id}",
"method": "SendMessage",
"params": {
"metadata": {
"task_id": "{case_id}"
},
"message": {
"messageId": "case-{case_id}-request",
"taskId": "{case_id}",
"role": "ROLE_USER",
"parts": [
{
"data": {
"schema_version": "contract-compliance.a2a.v1",
"case_id": "{case_id}",
"contract": {
"contract_value": 250000.0,
"contractor_name": "ACME CLOUD SOLUTIONS",
"insurance_coverage": 2000000.0,
"liability_limit": "$1,000,000.00",
"term_length_years": 2,
"auto_renewal": false,
"has_termination_clause": true
},
"policy": {
"max_contract_value": 500000.0,
"required_insurance_minimum": 1000000.0,
"max_term_years": 5,
"required_termination_clause": true,
"prohibited_clauses": ["unlimited liability", "auto-renewal > 3yr"]
}
},
"mediaType": "application/json"
}
]
}
}
}
```
Go returns a completed A2A task whose status message contains a data part:
```json
{
"passed": false,
"violations": [
"Contract value $850000.00 exceeds company framework limit of $500000.00"
],
"verdict_timestamp": "2026-06-03T00:00:00Z"
}
```
## State Outcomes
The live cockpit maps Go results into three visible outcomes:
| Outcome | Trigger |
|:---|:---|
| `APPROVED` | Go returns `passed: true`. |
| `REVIEW_READY` | Go returns `passed: false` with policy violations. |
| `MANUAL_REVIEW` | Go is unavailable or simulator mode is `Crashed (503)`. |
The richer enum in `state_schema.py` still includes intermediate states used by the fuller ADK reference path, but the cockpit completes the healthy path in one API call.
## Trust Boundaries
Browser:
- Chooses bundled sample contracts.
- Sends policy override values.
- Never calls the Go service directly.
Python service:
- Enforces file extension and 5MB upload limits.
- Rejects binary PDF uploads; bundled `.pdf` files are text fixtures.
- Resolves sample and artifact paths with basename and root-bound checks.
- Calls only the configured Go Agent Card URL.
- Fails closed to `MANUAL_REVIEW` when the Go handoff fails.
Go service:
- Serves an Agent Card at `/.well-known/agent.json`.
- Accepts JSON-RPC POST requests.
- Handles current `SendMessage` plus legacy `tasks/send` and `tasks/get`.
- Applies deterministic policy rules from `default_policy.json` or request policy overrides.
## Key Files
| File | Role |
|:---|:---|
| `python-extraction-agent/app/static/live-compliance/index.html` | Browser cockpit. |
| `python-extraction-agent/app/fast_api_app.py` | API routes, ADK handoff, case response. |
| `python-extraction-agent/app/tools.py` | Deterministic extraction and risk classification. |
| `python-extraction-agent/app/live_compliance.py` | Case state, events, artifact generation. |
| `python-extraction-agent/app/agent.py` | Fuller ADK `SequentialAgent` reference. |
| `go-compliance-agent/internal/agentcard/card.go` | Agent Card. |
| `go-compliance-agent/internal/handler/task_handler.go` | A2A JSON-RPC handler. |
| `go-compliance-agent/internal/compliance/checker.go` | Deterministic policy checker. |
| `go-compliance-agent/internal/policies/default_policy.json` | Default policy thresholds. |
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,206 @@
# 📑 Contract Compliance Multi-Agent Team (ADK & A2A)
**Authors**
* [Shubham Saboo](https://github.com/Shubhamsaboo)
* [Eric Dong](https://github.com/gericdong)
This Contract Compliance Engine demonstrates key principles for architecting cross-language, auditable compliance agents. The project contains a Python FastAPI service built with Google ADK for intake, deterministic extraction, and the `RemoteA2aAgent` handoff, plus a Go A2A compliance service that enforces policy rules with repeatable code.
The live demo UI gives contract reviewers a polished cockpit for selecting sample vendor agreements, running compliance audits, watching the Python-to-Go A2A handoff, simulating remote-agent failure modes, and opening generated compliance artifacts outside the raw ADK console.
<img width="1209" height="712" alt="Contract Compliance Engine live demo" src="https://github.com/user-attachments/assets/14c6ed73-d66f-4f0a-8813-d17752a95d14" />
## Overview
Most agent demos overuse LLMs for every step, including places where deterministic code is a better engineering choice. Contract compliance is one of those places. If the policy says vendor agreements cannot exceed `$500,000`, cannot run longer than `5` years, and must include an exit clause, those checks should be auditable and repeatable.
This repository demonstrates that split of responsibility:
- **Python FastAPI + ADK** handles intake, deterministic extraction, risk classification, session state, artifacts, and the focused `RemoteA2aAgent` handoff.
- **Go A2A compliance agent** exposes an Agent Card and validates extracted contract fields through JSON-RPC `SendMessage`.
- **Contract Compliance Engine UI** shows the full pipeline: contract selection, generated artifacts, live A2A payload, Go verdict, trace spans, simulator controls, and the active compliance policy summary.
The core lesson:
> LLMs are useful for ambiguity. Deterministic agents should enforce hard policy.
The Go service is not an LLM agent. It is a deterministic policy agent exposed through an A2A-compatible interface.
## System Architecture & Technology Stack
```mermaid
flowchart TD
subgraph Client ["Client (Frontend)"]
UI["Cockpit Dashboard<br/>(HTML5 / CSS3 / Vanilla JS)"]
end
subgraph Python ["Orchestration Service (Python)"]
FastAPI["FastAPI App<br/>(:8000)"]
ADK["Google ADK Coordinator<br/>(RemoteA2aAgent)"]
Parser["Deterministic Parser<br/>(Regular Expressions)"]
StateDB["Session DB<br/>(SQLite / aiosqlite)"]
end
subgraph Go ["Compliance Service (Go)"]
Server["Go HTTP Server<br/>(:8888)"]
RPC["JSON-RPC 2.0 Handler"]
Checker["Deterministic Policy Engine"]
end
UI -->|"Upload / Run request"| FastAPI
FastAPI --> Parser
FastAPI --> ADK
ADK -->|"A2A Handshake / RPC"| Server
Server --> RPC
RPC --> Checker
ADK -.->|"Persist checkpoints"| StateDB
Checker -->|"Task status verdict"| ADK
FastAPI -->|"Render HTML certificate"| UI
```
### Core Components & Technologies
* **Frontend Dashboard (HTML5 / CSS3 / JavaScript)**: A rich, interactive user interface featuring code syntax highlighting, live state trace timelines, real-time payload visualizers, and an integrated policy overrides editor.
* **Orchestration Service (Python 3.13 / FastAPI / Google ADK)**:
* **FastAPI**: Serves the cockpit UI, manages upload end-points, and simulates service latencies/outages.
* **Google ADK (Agent Development Kit)**: Orchestrates the multi-agent execution pipeline and abstracts remote service communication via `RemoteA2aAgent`.
* **SQLite / aiosqlite**: Stores session logs, execution traces, and agent-state checkpoints.
* **Compliance Validator Service (Go / JSON-RPC 2.0)**:
* **Go Standard Library (`net/http`)**: High-performance HTTP server that exposes the A2A Agent Card config (`/.well-known/agent.json`).
* **JSON-RPC 2.0**: The protocol layer used for agent-to-agent communication (supporting `SendMessage`, `tasks/send`, and `tasks/get`).
* **Go Policy Checker**: Performs synchronous, auditable compliance validation against company thresholds.
## Quick Start
Terminal 1, start the Go A2A compliance agent:
```bash
cd go-compliance-agent
go run cmd/server/main.go
```
Terminal 2, start the Python FastAPI cockpit:
```bash
cd python-extraction-agent
uv sync
uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000
```
Open the live cockpit:
```text
http://127.0.0.1:8000/live-compliance/
```
This live cockpit path does **not** require a Gemini API key. The extraction fixtures and Go policy checks are deterministic so the demo stays reproducible.
Docker Compose is also available:
```bash
docker-compose up --build
```
## How To Use The Demo
1. Open `/live-compliance/`.
2. Select one of the bundled vendor contracts.
3. Keep **A2A Simulator Mode** on `Healthy` for the normal path.
4. Click **Run Pipeline Audit**.
5. Watch **Agent Exchange** populate with the Python-to-Go `SendMessage` handoff.
6. Open the generated artifacts:
- legal parameters sheet
- Go compliance certificate
The UI sends real contract text and active policy settings to Python. Python extracts deterministic contract facts, ADK performs the A2A handoff, and Go returns an auditable policy verdict.
## Sample Outcomes
| Contract | Expected Result | Why |
|:---|:---|:---|
| `standard-vendor-agreement.pdf` | Pass | Value, insurance, term, liability, renewal, and exit terms are within policy. |
| `high-risk-liability-contract.pdf` | Review | Unlimited liability language is prohibited by the Go policy checker. |
| `non-compliant-contract.pdf` | Review | Value, insurance, term, auto-renewal, liability, and exit-safety rules fail. |
The files in `sample-contracts/` are plain-text fixtures with `.pdf` names. That keeps the demo inspectable without hiding behavior behind PDF parsing.
## What Actually Runs
```mermaid
flowchart LR
USER["End User<br/>Contract Compliance Engine UI"]
PY["Python FastAPI (:8000)<br/>intake, extraction, risk"]
ADK["ADK RemoteA2aAgent<br/>Agent Card + SendMessage"]
GO["Go A2A Compliance Agent (:8888)<br/>deterministic policy checks"]
OUT["Returned Case<br/>verdict, trace, artifacts"]
USER -->|"Select contract + run audit"| PY
PY -->|"Extract facts + attach active policy"| ADK
ADK -->|"GET /.well-known/agent.json<br/>POST JSON-RPC SendMessage"| GO
GO -->|"Completed A2A Task<br/>pass/review + violations"| ADK
ADK -->|"Go verdict"| PY
PY -->|"Case payload + artifact links"| OUT
OUT -->|"Render results"| USER
```
The executable cockpit uses `python-extraction-agent/app/fast_api_app.py` for the live browser path. `python-extraction-agent/app/agent.py` remains as a fuller ADK `SequentialAgent` reference for extract -> comply -> report.
For the detailed topology and sequence diagram, see [ARCHITECTURE.md](./ARCHITECTURE.md).
## Active Policy
The live cockpit summarizes the active Go policy as:
```text
Value Cap: $500k
Term Limit: 5 yrs
Insurance: $1M+
Rules: Exit clause, no unlimited liability, no >3yr renewal
```
Those values are not decorative. The UI sends them as `custom_policies`; FastAPI includes them inside the A2A data part; Go uses them as the effective policy for that audit.
## Key Files
| File | Purpose |
|:---|:---|
| `python-extraction-agent/app/static/live-compliance/index.html` | Live Contract Compliance Engine UI. |
| `python-extraction-agent/app/fast_api_app.py` | FastAPI upload API, sample routes, live ADK `RemoteA2aAgent` handoff. |
| `python-extraction-agent/app/tools.py` | Deterministic extraction and risk classification. |
| `python-extraction-agent/app/live_compliance.py` | Case state, event stream, and generated HTML artifacts. |
| `go-compliance-agent/internal/agentcard/card.go` | Go Agent Card at `/.well-known/agent.json`. |
| `go-compliance-agent/internal/handler/task_handler.go` | JSON-RPC `SendMessage`, `tasks/send`, and `tasks/get` handlers. |
| `go-compliance-agent/internal/compliance/checker.go` | Deterministic policy checker. |
| `go-compliance-agent/internal/policies/default_policy.json` | Default compliance policy. |
## Test Commands
Run Python unit tests:
```bash
cd python-extraction-agent
uv run pytest tests/unit -v
```
Run Go tests:
```bash
cd go-compliance-agent
go test -v ./...
```
Manual smoke test:
```bash
curl http://127.0.0.1:8888/.well-known/agent.json
```
Then run the live cockpit and confirm:
- the Agent Exchange panel shows `SendMessage`
- the A2A payload includes `jsonrpc: "2.0"`
- the Go verdict appears in the UI
- the generated compliance certificate artifact renders
@@ -0,0 +1,180 @@
# Tutorial: Run the Contract Compliance Multi-Agent Demo
This tutorial walks through the executable demo in this repository. A Python FastAPI service handles contract intake, deterministic extraction, ADK `RemoteA2aAgent` handoff, case state, and artifacts. A Go A2A service enforces deterministic compliance rules and returns a JSON-RPC `SendMessage` task verdict.
The live cockpit path does not require a Gemini API key.
## 1. Start the Services
Start the Go compliance agent:
```bash
cd go-compliance-agent
go run cmd/server/main.go
```
The Go service exposes:
```text
http://localhost:8888/.well-known/agent.json
http://localhost:8888/
```
Start the Python cockpit:
```bash
cd python-extraction-agent
uv sync
uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000
```
Open:
```text
http://127.0.0.1:8000/live-compliance/
```
## 2. Run the Happy Path
1. Select `standard-vendor-agreement.pdf`.
2. Keep **A2A Simulator Mode** on `Healthy`.
3. Click **Run Pipeline Audit**.
4. Confirm the Agent Exchange panel shows:
- source: `python-extraction-agent`
- target: `Security Compliance Validator`
- method: `SendMessage`
- a task ID
- a JSON-RPC payload
- a passed Go verdict
5. Open the generated compliance certificate artifact.
Expected result: the contract passes because value, term, insurance, liability, renewal, and exit terms are within policy.
## 3. Run the Review Paths
Test the bundled failure cases:
| Contract | Expected Result |
|:---|:---|
| `high-risk-liability-contract.pdf` | Review because unlimited liability is prohibited. |
| `non-compliant-contract.pdf` | Review with multiple policy violations. |
For `non-compliant-contract.pdf`, the UI should show review required with these violation categories:
- value above `$500k`
- unlimited liability
- auto-renewal longer than 3 years
- insurance below `$1M`
- term longer than 5 years
- missing usable exit clause
## 4. Simulate A2A Failure
Use **A2A Simulator Mode** to test remote-agent failure behavior:
- `Healthy`: normal Go service path.
- `Delayed`: waits before the Go call, bounded by the backend.
- `Crashed (503)`: simulates Go service failure and routes to manual review.
Expected crashed result: Python fails closed to `MANUAL_REVIEW` and the certificate explains that manual legal review is required.
## 5. What the Browser Sends
The UI posts contract text and policy settings to:
```text
POST /api/compliance/upload
```
Important form fields:
| Field | Purpose |
|:---|:---|
| `file` | Text contract fixture, including `.pdf`-named text samples. |
| `custom_policies` | Active policy values sent to Go. |
| `simulated_latency` | Bounded delay for simulator testing. |
| `simulated_server_state` | `normal` or `crashed`. |
The browser does not call the Go service directly.
## 6. What Python Sends to Go
`python-extraction-agent/app/fast_api_app.py` builds the A2A request in `build_go_message_payload(...)`.
The live method is:
```text
SendMessage
```
The payload contains:
- `jsonrpc: "2.0"`
- `task_id`
- extracted contract fields
- active policy settings
ADK sends the request through `RemoteA2aAgent`; it is not a manual browser-to-Go call.
## 7. What Go Validates
`go-compliance-agent/internal/compliance/checker.go` checks:
- contract value <= `max_contract_value`
- insurance coverage >= `required_insurance_minimum`
- term length <= `max_term_years`
- termination clause present when required
- no prohibited unlimited liability clause
- no prohibited auto-renewal longer than 3 years
Default thresholds live in:
```text
go-compliance-agent/internal/policies/default_policy.json
```
## 8. Key Code To Read
| File | Why it matters |
|:---|:---|
| `python-extraction-agent/app/fast_api_app.py` | API routes, request validation, ADK handoff, simulator handling. |
| `python-extraction-agent/app/tools.py` | Deterministic contract extraction and risk classification. |
| `python-extraction-agent/app/live_compliance.py` | Case state and generated HTML artifacts. |
| `python-extraction-agent/app/static/live-compliance/index.html` | Live cockpit UI and A2A payload display. |
| `go-compliance-agent/internal/agentcard/card.go` | Agent Card discovery response. |
| `go-compliance-agent/internal/handler/task_handler.go` | JSON-RPC `SendMessage` and legacy task handlers. |
| `go-compliance-agent/internal/compliance/checker.go` | Deterministic policy verdict. |
## 9. Test the Demo
Run Python tests:
```bash
cd python-extraction-agent
uv run pytest tests/unit -v
```
Run Go tests:
```bash
cd go-compliance-agent
go test -v ./...
```
Smoke-test the Agent Card:
```bash
curl http://127.0.0.1:8888/.well-known/agent.json
```
From the repository root, smoke-test a non-compliant upload:
```bash
curl -sS -X POST http://127.0.0.1:8000/api/compliance/upload \
-F file=@sample-contracts/non-compliant-contract.pdf \
-F simulated_latency=0 \
-F simulated_server_state=normal \
-F 'custom_policies={"max_contract_value":500000,"required_insurance_minimum":1000000,"max_term_years":5,"required_termination_clause":true,"prohibited_clauses":["unlimited liability","auto-renewal > 3yr"]}'
```
Expected result: response JSON contains `passed: false` and a list of Go policy violations.
Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

@@ -0,0 +1,53 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
services:
# Python intake, extraction, dashboard, and JSON-RPC handoff service
python-extraction-agent:
build:
context: ./python-extraction-agent
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./sample-contracts:/app/sample-contracts
environment:
- GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT:-mock-gcp-project}
- GOOGLE_CLOUD_LOCATION=global
# Optional: kept for the ADK reference agent path; not required by the live cockpit.
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
# A2A: Point to the Go agent's Agent Card URL via Docker networking
- GO_AGENT_CARD_URL=http://go-compliance-agent:8888/.well-known/agent.json
depends_on:
go-compliance-agent:
condition: service_started
networks:
- pipeline-net
# Go Security Compliance Agent (A2A Server)
go-compliance-agent:
build:
context: ./go-compliance-agent
dockerfile: Dockerfile
ports:
- "8888:8888"
environment:
- HOST=0.0.0.0
- AGENT_URL=http://go-compliance-agent:8888
networks:
- pipeline-net
networks:
pipeline-net:
driver: bridge
@@ -0,0 +1,41 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Stage 1: Build the Go validation engine binary
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copy modules files and cache packages download
COPY go.mod ./
# Expose default config copies
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o compliance-server cmd/server/main.go
# Stage 2: Bundle execution package inside minimal Alpine container
FROM alpine:latest
RUN apk --no-cache add ca-certificates curl
WORKDIR /root/
COPY --from=builder /app/compliance-server .
COPY --from=builder /app/internal/policies/default_policy.json internal/policies/default_policy.json
EXPOSE 8888
CMD ["./compliance-server", "--port", "8888", "--policy", "internal/policies/default_policy.json"]
@@ -0,0 +1,87 @@
package main
import (
"context"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"go-compliance-agent/internal/agentcard"
"go-compliance-agent/internal/handler"
)
func main() {
// Parse CLI parameters
port := flag.String("port", "8888", "Go A2A Service listener port")
policyPath := flag.String("policy", "internal/policies/default_policy.json", "Compliance rules JSON file path")
flag.Parse()
fmt.Println("--- Go Compliance Agent (A2A Protocol) ---")
fmt.Printf("Bootstrapping validation policies from %s\n", *policyPath)
handler.InitPolicies(*policyPath)
// API Routing definition — A2A uses a single JSON-RPC endpoint at root.
// The agent card endpoint must be registered first so it takes priority.
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/agent.json", agentcard.Handler)
mux.HandleFunc("/", handler.HandleJSONRPC)
// Global Middleware enabling CORS checks
corsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
mux.ServeHTTP(w, r)
})
// Bind address — configurable via HOST env var, defaults to 0.0.0.0 for container compatibility
host := os.Getenv("HOST")
if host == "" {
host = "0.0.0.0"
}
bindAddr := net.JoinHostPort(host, *port)
server := &http.Server{
Addr: bindAddr,
Handler: corsHandler,
}
// Graceful shutdown listeners
idleConnsClosed := make(chan struct{})
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
fmt.Println("\nReceived termination signal. Shutting down A2A service gracefully...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
fmt.Printf("Error shutting down server: %v\n", err)
}
close(idleConnsClosed)
}()
fmt.Printf("Agent Card: http://%s/.well-known/agent.json\n", bindAddr)
fmt.Printf("JSON-RPC: http://%s/\n", bindAddr)
fmt.Printf("A2A compliance validation engine listening on %s...\n", bindAddr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("Error starting server: %v\n", err)
os.Exit(1)
}
<-idleConnsClosed
fmt.Println("Security validation service exited safely.")
}
@@ -0,0 +1,3 @@
module go-compliance-agent
go 1.21
@@ -0,0 +1,88 @@
package agentcard
import (
"encoding/json"
"net/http"
"os"
)
// AgentCard describes the agent's capabilities following the A2A protocol specification.
// Served at /.well-known/agent.json for discovery by other agents.
type AgentCard struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
SupportedInterfaces []AgentInterface `json:"supportedInterfaces"`
Capabilities Capabilities `json:"capabilities"`
DefaultInputModes []string `json:"defaultInputModes"`
DefaultOutputModes []string `json:"defaultOutputModes"`
Skills []Skill `json:"skills"`
}
// AgentInterface specifies a transport mechanism and protocol version for agent communication.
type AgentInterface struct {
URL string `json:"url"`
ProtocolBinding string `json:"protocolBinding"`
ProtocolVersion string `json:"protocolVersion"`
}
// Capabilities declares which optional A2A features the agent supports.
type Capabilities struct {
ExtendedAgentCard bool `json:"extendedAgentCard"`
}
// Skill describes a specific capability the agent can perform.
type Skill struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Tags []string `json:"tags"`
Examples []string `json:"examples"`
}
// GetCard returns the preloaded Go Compliance service Agent Card configuration structure.
func GetCard() AgentCard {
// Allow the agent URL to be configured via environment variable for deployment flexibility.
agentURL := os.Getenv("AGENT_URL")
if agentURL == "" {
agentURL = "http://localhost:8888"
}
return AgentCard{
Name: "Security Compliance Validator",
Description: "Go-based validation engine that checks vendor contracts against corporate compliance policy rules. Accepts extracted contract fields and returns pass/fail verdict with specific violations.",
Version: "1.0.0",
SupportedInterfaces: []AgentInterface{
{
URL: agentURL,
ProtocolBinding: "JSONRPC",
ProtocolVersion: "1.0",
},
},
Capabilities: Capabilities{
ExtendedAgentCard: false,
},
DefaultInputModes: []string{"application/json"},
DefaultOutputModes: []string{"application/json"},
Skills: []Skill{
{
ID: "contract_compliance_check",
Name: "Contract Compliance Check",
Description: "Validates extracted contract fields against corporate policy rules. Returns pass/fail verdict with specific violations.",
Tags: []string{"compliance", "contract", "validation"},
Examples: []string{
"Check this contract for compliance violations",
"Validate vendor agreement terms against policy",
},
},
},
}
}
// Handler serves agent card configurations as JSON objects.
func Handler(w http.ResponseWriter, r *http.Request) {
card := GetCard()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(card)
}
@@ -0,0 +1,107 @@
package compliance
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
)
// Policy represents compliance validation rules configuration.
type Policy struct {
MaxContractValue float64 `json:"max_contract_value"`
ProhibitedClauses []string `json:"prohibited_clauses"`
RequiredInsuranceMinimum float64 `json:"required_insurance_minimum"`
MaxTermYears int `json:"max_term_years"`
RequiredTerminationClause bool `json:"required_termination_clause"`
}
// ContractDetails matches legal fields extracted by the Python subagent.
type ContractDetails struct {
ContractValue float64 `json:"contract_value"`
ContractorName string `json:"contractor_name"`
ClientName string `json:"client_name"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
LiabilityLimit string `json:"liability_limit"`
InsuranceCoverage float64 `json:"insurance_coverage"`
AutoRenewal bool `json:"auto_renewal"`
HasTerminationClause bool `json:"has_termination_clause"`
TermLengthYears int `json:"term_length_years"`
}
// ComplianceResult holds safety pass audit verdicts and violation details.
type ComplianceResult struct {
Passed bool `json:"passed"`
Violations []string `json:"violations"`
VerdictTimestamp string `json:"verdict_timestamp"`
}
// LoadPolicy reads legal policy metrics from local file paths.
func LoadPolicy(path string) (Policy, error) {
var policy Policy
bytes, err := os.ReadFile(path)
if err != nil {
return policy, fmt.Errorf("error reading policy file: %w", err)
}
err = json.Unmarshal(bytes, &policy)
if err != nil {
return policy, fmt.Errorf("error decoding policy JSON: %w", err)
}
return policy, nil
}
// CheckCompliance evaluates contract parameters against standard corporate policies.
func CheckCompliance(details ContractDetails, policy Policy) ComplianceResult {
violations := make([]string, 0)
// 1. Contract Value Audit
if details.ContractValue > policy.MaxContractValue {
violations = append(violations, fmt.Sprintf(
"Contract value $%.2f exceeds company framework limit of $%.2f",
details.ContractValue, policy.MaxContractValue,
))
}
// 2. Prohibited Clauses (Unlimited liability checks)
liabilityUpper := strings.ToLower(details.LiabilityLimit)
for _, clause := range policy.ProhibitedClauses {
if clause == "unlimited liability" {
if strings.Contains(liabilityUpper, "unlimited") || strings.Contains(liabilityUpper, "waived") {
violations = append(violations, "Unlimited contractor liability clauses are strictly prohibited by company policy")
}
}
if clause == "auto-renewal > 3yr" && details.AutoRenewal && details.TermLengthYears > 3 {
violations = append(violations, "Auto-renewal spans extending beyond 3 years are unauthorized without general counsel signatures")
}
}
// 3. Liability Insurance Bounds Check
if details.InsuranceCoverage < policy.RequiredInsuranceMinimum {
violations = append(violations, fmt.Sprintf(
"Insurance coverage $%.2f is under the security minimum threshold of $%.2f",
details.InsuranceCoverage, policy.RequiredInsuranceMinimum,
))
}
// 4. Term Length Limits
if details.TermLengthYears > policy.MaxTermYears {
violations = append(violations, fmt.Sprintf(
"Engagement length of %d years exceeds framework limit bounds of %d years",
details.TermLengthYears, policy.MaxTermYears,
))
}
// 5. Written Exit Notice Audit
if policy.RequiredTerminationClause && !details.HasTerminationClause {
violations = append(violations, "Missing required written exit termination safety clauses")
}
passed := len(violations) == 0
return ComplianceResult{
Passed: passed,
Violations: violations,
VerdictTimestamp: time.Now().UTC().Format(time.RFC3339),
}
}
@@ -0,0 +1,117 @@
package compliance
import (
"strings"
"testing"
)
func TestCheckCompliance_Pass(t *testing.T) {
policy := Policy{
MaxContractValue: 500000.0,
ProhibitedClauses: []string{"unlimited liability", "auto-renewal > 3yr"},
RequiredInsuranceMinimum: 1000000.0,
MaxTermYears: 5,
RequiredTerminationClause: true,
}
// Acme Cloud Solutions values (Should Pass)
contract := ContractDetails{
ContractValue: 250000.0,
ContractorName: "Acme Cloud Solutions",
ClientName: "GFD Platform Systems",
StartDate: "2026-06-01",
EndDate: "2028-06-01",
LiabilityLimit: "$1,000,000.00 standard cap",
InsuranceCoverage: 2000000.0,
AutoRenewal: false,
HasTerminationClause: true,
TermLengthYears: 2,
}
result := CheckCompliance(contract, policy)
if !result.Passed {
t.Errorf("Expected compliance check to PASS, but got FAIL. Violations: %v", result.Violations)
}
if len(result.Violations) != 0 {
t.Errorf("Expected 0 violations, got %d: %v", len(result.Violations), result.Violations)
}
}
func TestCheckCompliance_UnlimitedLiability(t *testing.T) {
policy := Policy{
MaxContractValue: 500000.0,
ProhibitedClauses: []string{"unlimited liability"},
RequiredInsuranceMinimum: 1000000.0,
MaxTermYears: 5,
RequiredTerminationClause: true,
}
// Apex Data Systems (Should Fail due to unlimited liability)
contract := ContractDetails{
ContractValue: 450000.0,
ContractorName: "Apex Data Systems",
ClientName: "GFD Platform Systems",
StartDate: "2026-06-01",
EndDate: "2027-06-01",
LiabilityLimit: "Contractor liability shall be entirely unlimited under all conditions",
InsuranceCoverage: 1500000.0,
AutoRenewal: false,
HasTerminationClause: true,
TermLengthYears: 1,
}
result := CheckCompliance(contract, policy)
if result.Passed {
t.Errorf("Expected compliance check to FAIL, but got PASS.")
}
hasLiabilityViolation := false
for _, v := range result.Violations {
if strings.Contains(v, "Unlimited contractor liability") {
hasLiabilityViolation = true
}
}
if !hasLiabilityViolation {
t.Errorf("Expected unlimited liability violation, but got none in: %v", result.Violations)
}
}
func TestCheckCompliance_MultiViolations(t *testing.T) {
policy := Policy{
MaxContractValue: 500000.0,
ProhibitedClauses: []string{"unlimited liability", "auto-renewal > 3yr"},
RequiredInsuranceMinimum: 1000000.0,
MaxTermYears: 5,
RequiredTerminationClause: true,
}
// Legacy Networks Corp — triggers all 6 violations:
// 1. Contract value exceeds limit
// 2. Unlimited liability (waived)
// 3. Auto-renewal > 3yr
// 4. Insurance below minimum
// 5. Term length exceeds max
// 6. Missing termination clause
contract := ContractDetails{
ContractValue: 850000.0,
ContractorName: "Legacy Networks Corp",
ClientName: "GFD Platform Systems",
StartDate: "2026-06-01",
EndDate: "2032-06-01",
LiabilityLimit: "Contractor GFD liability limits are waived, resulting in unlimited GFD exposure",
InsuranceCoverage: 500000.0,
AutoRenewal: true,
HasTerminationClause: false,
TermLengthYears: 6,
}
result := CheckCompliance(contract, policy)
if result.Passed {
t.Errorf("Expected compliance check to FAIL, but got PASS.")
}
expectedViolations := 6
if len(result.Violations) != expectedViolations {
t.Errorf("Expected %d violations, got %d: %v", expectedViolations, len(result.Violations), result.Violations)
}
}
@@ -0,0 +1,473 @@
package handler
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"go-compliance-agent/internal/compliance"
)
// --- JSON-RPC 2.0 request/response structures per A2A protocol specification ---
// JSONRPCRequest is the standard JSON-RPC 2.0 request envelope.
// The A2A protocol uses JSON-RPC as its wire format for all agent communication.
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
// JSONRPCResponse is the standard JSON-RPC 2.0 response envelope.
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Result interface{} `json:"result,omitempty"`
Error *JSONRPCError `json:"error,omitempty"`
}
// JSONRPCError carries error details inside a JSON-RPC response.
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// --- A2A Task, Message, and Part structures ---
// MessageSendParams contains the parameters for the current A2A message/send method.
type MessageSendParams struct {
Message Message `json:"message"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// TaskSendParams contains the parameters for the legacy tasks/send JSON-RPC method.
type TaskSendParams struct {
ID string `json:"id"`
Message Message `json:"message"`
}
// TaskGetParams contains the parameters for the tasks/get JSON-RPC method.
type TaskGetParams struct {
ID string `json:"id"`
}
// Task represents an A2A task with its current status and any produced artifacts.
type Task struct {
ContextID string `json:"contextId"`
ID string `json:"id"`
Kind string `json:"kind,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Status TaskStatus `json:"status"`
Artifacts []Artifact `json:"artifacts,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
LastModified string `json:"lastModified,omitempty"`
}
// TaskStatus tracks the lifecycle state of a task.
type TaskStatus struct {
State string `json:"state"`
Message *Message `json:"message,omitempty"`
}
// Message represents a conversational turn between user and agent, containing typed Parts.
type Message struct {
ContextID string `json:"contextId,omitempty"`
Kind string `json:"kind,omitempty"`
MessageID string `json:"messageId,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Role string `json:"role"`
TaskID string `json:"taskId,omitempty"`
Parts []Part `json:"parts"`
}
// Part is a polymorphic content unit — either text, raw bytes, URL, or structured data.
type Part struct {
Kind string `json:"kind,omitempty"` // legacy v0.3
Type string `json:"type,omitempty"` // legacy v0.3
Text string `json:"text,omitempty"` // v1.0 & v0.3
URL string `json:"url,omitempty"` // v1.0
Filename string `json:"filename,omitempty"` // v1.0
MediaType string `json:"mediaType,omitempty"` // v1.0
Raw string `json:"raw,omitempty"` // v1.0
Data map[string]interface{} `json:"data,omitempty"` // v1.0 & v0.3
}
// Artifact is a named output produced by the agent during task execution.
type Artifact struct {
Name string `json:"name,omitempty"`
Parts []Part `json:"parts"`
}
var (
tasksMu sync.RWMutex
tasks = make(map[string]*Task)
policy compliance.Policy
)
// InitPolicies configures the checker thresholds from targeted config paths.
func InitPolicies(policyPath string) {
loadedPolicy, err := compliance.LoadPolicy(policyPath)
if err != nil {
fmt.Printf("Warning: Failed to load policy file at %s: %v. Bootstrapping defaults.\n", policyPath, err)
policy = compliance.Policy{
MaxContractValue: 500000.0,
ProhibitedClauses: []string{"unlimited liability", "auto-renewal > 3yr"},
RequiredInsuranceMinimum: 1000000.0,
MaxTermYears: 5,
RequiredTerminationClause: true,
}
return
}
policy = loadedPolicy
}
// HandleJSONRPC is the single entry point for all A2A JSON-RPC 2.0 requests.
// The A2A protocol routes all operations through JSON-RPC method dispatch
// rather than separate REST endpoints.
func HandleJSONRPC(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
writeJSONRPCError(w, nil, -32600, "Only POST method is accepted")
return
}
// Decode the JSON-RPC envelope
var req JSONRPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONRPCError(w, nil, -32700, "Parse error: invalid JSON")
return
}
// Validate JSON-RPC version
if req.JSONRPC != "2.0" {
writeJSONRPCError(w, req.ID, -32600, "Invalid request: jsonrpc must be '2.0'")
return
}
// Dispatch to the appropriate handler based on the A2A method name
switch req.Method {
case "message/send", "SendMessage":
result, rpcErr := handleMessageSend(req.Params)
if rpcErr != nil {
writeJSONRPCError(w, req.ID, rpcErr.Code, rpcErr.Message)
return
}
writeJSONRPCResult(w, req.ID, result)
case "tasks/send":
result, rpcErr := handleTasksSend(req.Params)
if rpcErr != nil {
writeJSONRPCError(w, req.ID, rpcErr.Code, rpcErr.Message)
return
}
writeJSONRPCResult(w, req.ID, result)
case "tasks/get", "GetTask":
result, rpcErr := handleTasksGet(req.Params)
if rpcErr != nil {
writeJSONRPCError(w, req.ID, rpcErr.Code, rpcErr.Message)
return
}
writeJSONRPCResult(w, req.ID, result)
default:
writeJSONRPCError(w, req.ID, -32601, fmt.Sprintf("Method not found: %s", req.Method))
}
}
// handleMessageSend processes the current A2A message/send request shape.
// It accepts a Message containing a structured DataPart and returns a completed Task.
func handleMessageSend(params json.RawMessage) (*Task, *JSONRPCError) {
var sendParams MessageSendParams
if err := json.Unmarshal(params, &sendParams); err != nil {
return nil, &JSONRPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
}
taskID := sendParams.Message.TaskID
if taskID == "" {
taskID = stringValue(sendParams.Metadata, "task_id")
}
return validateMessage(taskID, sendParams.Message, false)
}
// handleTasksSend processes the legacy A2A tasks/send request shape.
// It is retained for older clients; the live demo uses message/send.
func handleTasksSend(params json.RawMessage) (*Task, *JSONRPCError) {
var sendParams TaskSendParams
if err := json.Unmarshal(params, &sendParams); err != nil {
return nil, &JSONRPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
}
if sendParams.ID == "" {
return nil, &JSONRPCError{Code: -32602, Message: "Missing required task id"}
}
return validateMessage(sendParams.ID, sendParams.Message, true)
}
func validateMessage(taskID string, message Message, includeLegacyType bool) (*Task, *JSONRPCError) {
contractPayload, rpcErr := extractContractPayload(message)
if rpcErr != nil {
return nil, rpcErr
}
if taskID == "" {
taskID = stringValue(contractPayload, "case_id")
}
if taskID == "" {
taskID = fmt.Sprintf("task-%d", time.Now().UnixNano())
}
contextID := message.ContextID
if contextID == "" {
contextID = taskID
}
detailsPayload, effectivePolicy, rpcErr := unpackContractPayload(contractPayload)
if rpcErr != nil {
return nil, rpcErr
}
details, rpcErr := decodeContractDetails(detailsPayload)
if rpcErr != nil {
return nil, rpcErr
}
// Run compliance checks synchronously — pure computation, no I/O wait needed.
result := compliance.CheckCompliance(details, effectivePolicy)
resultMap := complianceResultMap(result)
task := completedTask(taskID, contextID, resultMap, includeLegacyType)
// Store the task for later retrieval via tasks/get.
tasksMu.Lock()
tasks[taskID] = task
tasksMu.Unlock()
return task, nil
}
func extractContractPayload(message Message) (map[string]interface{}, *JSONRPCError) {
for _, part := range message.Parts {
if (part.Kind == "data" || part.Type == "data") && part.Data != nil {
return part.Data, nil
}
if part.Kind == "text" && part.Text != "" {
var data map[string]interface{}
if err := json.Unmarshal([]byte(part.Text), &data); err == nil {
return data, nil
}
}
}
return nil, &JSONRPCError{Code: -32602, Message: "No data part found in message"}
}
func unpackContractPayload(payload map[string]interface{}) (map[string]interface{}, compliance.Policy, *JSONRPCError) {
effectivePolicy := policy
if policyData, ok := payload["policy"]; ok {
parsedPolicy, rpcErr := decodePolicy(policyData)
if rpcErr != nil {
return nil, effectivePolicy, rpcErr
}
effectivePolicy = parsedPolicy
}
if nested, ok := mapValue(payload, "contract"); ok {
return nested, effectivePolicy, nil
}
if nested, ok := mapValue(payload, "contract_details"); ok {
return nested, effectivePolicy, nil
}
// Legacy clients send the contract fields directly in the DataPart.
direct := make(map[string]interface{}, len(payload))
for key, value := range payload {
if key == "policy" || key == "risk_assessment" || key == "schema_version" || key == "case_id" {
continue
}
direct[key] = value
}
return direct, effectivePolicy, nil
}
func decodePolicy(policyData interface{}) (compliance.Policy, *JSONRPCError) {
policyBytes, err := json.Marshal(policyData)
if err != nil {
return compliance.Policy{}, &JSONRPCError{Code: -32602, Message: "Invalid policy override"}
}
var override compliance.Policy
if err := json.Unmarshal(policyBytes, &override); err != nil {
return compliance.Policy{}, &JSONRPCError{Code: -32602, Message: "Invalid policy override format: " + err.Error()}
}
return override, nil
}
func decodeContractDetails(contractData map[string]interface{}) (compliance.ContractDetails, *JSONRPCError) {
if contractData == nil {
return compliance.ContractDetails{}, &JSONRPCError{Code: -32602, Message: "No data part found in message"}
}
dataBytes, err := json.Marshal(contractData)
if err != nil {
return compliance.ContractDetails{}, &JSONRPCError{Code: -32603, Message: "Failed to marshal contract data"}
}
var details compliance.ContractDetails
if err := json.Unmarshal(dataBytes, &details); err != nil {
return compliance.ContractDetails{}, &JSONRPCError{Code: -32602, Message: "Invalid contract data format: " + err.Error()}
}
return details, nil
}
func complianceResultMap(result compliance.ComplianceResult) map[string]interface{} {
resultBytes, _ := json.Marshal(result)
var resultMap map[string]interface{}
_ = json.Unmarshal(resultBytes, &resultMap)
return resultMap
}
func completedTask(taskID string, contextID string, resultMap map[string]interface{}, includeLegacyType bool) *Task {
resultPart := Part{
Data: resultMap,
MediaType: "application/json",
}
if includeLegacyType {
resultPart.Kind = "data"
resultPart.Type = "data"
}
agentMessage := Message{
ContextID: contextID,
Kind: "message",
MessageID: fmt.Sprintf("msg-%s", taskID),
Role: "ROLE_AGENT", // "agent" -> "ROLE_AGENT"
TaskID: taskID,
Parts: []Part{resultPart},
}
now := time.Now().Format("2006-01-02T15:04:05.000Z")
return &Task{
ContextID: contextID,
ID: taskID,
Kind: "task",
Status: TaskStatus{
State: "TASK_STATE_COMPLETED", // "completed" -> "TASK_STATE_COMPLETED"
Message: &agentMessage,
},
CreatedAt: now,
LastModified: now,
}
}
func mapValue(data map[string]interface{}, key string) (map[string]interface{}, bool) {
value, ok := data[key]
if !ok {
return nil, false
}
typed, ok := value.(map[string]interface{})
return typed, ok
}
func stringValue(data map[string]interface{}, key string) string {
if data == nil {
return ""
}
value, ok := data[key]
if !ok {
return ""
}
typed, ok := value.(string)
if !ok {
return ""
}
return typed
}
// handleTasksGet retrieves a previously submitted task by ID.
func handleTasksGet(params json.RawMessage) (*Task, *JSONRPCError) {
var getParams TaskGetParams
if err := json.Unmarshal(params, &getParams); err != nil {
return nil, &JSONRPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
}
tasksMu.RLock()
task, exists := tasks[getParams.ID]
tasksMu.RUnlock()
if !exists {
return nil, &JSONRPCError{Code: -32602, Message: "Task not found: " + getParams.ID}
}
return task, nil
}
// writeJSONRPCResult sends a successful JSON-RPC 2.0 response.
func writeJSONRPCResult(w http.ResponseWriter, id interface{}, result interface{}) {
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: id,
Result: result,
}
_ = json.NewEncoder(w).Encode(resp)
}
// writeJSONRPCError sends a JSON-RPC 2.0 error response.
func writeJSONRPCError(w http.ResponseWriter, id interface{}, code int, message string) {
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: id,
Error: &JSONRPCError{Code: code, Message: message},
}
_ = json.NewEncoder(w).Encode(resp)
}
// dispatchWebhook sends an A2A-formatted callback notification to the orchestrator.
func dispatchWebhook(url, taskID string, result compliance.ComplianceResult) {
// Convert result to a generic map for the A2A DataPart
resultBytes, _ := json.Marshal(result)
var resultMap map[string]interface{}
_ = json.Unmarshal(resultBytes, &resultMap)
// Build an A2A-formatted webhook payload
payload := map[string]interface{}{
"jsonrpc": "2.0",
"method": "tasks/pushNotification",
"params": map[string]interface{}{
"id": taskID,
"status": map[string]interface{}{
"state": "completed",
"message": map[string]interface{}{
"role": "agent",
"parts": []map[string]interface{}{
{"type": "data", "data": resultMap},
},
},
},
},
}
bytesPayload, _ := json.Marshal(payload)
fmt.Printf("A2A Webhook Dispatcher: Hitting callback endpoint %s\n", url)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bytesPayload))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("A2A Webhook Trigger Error: Connection failed: %v\n", err)
return
}
_ = resp.Body.Close()
fmt.Printf("A2A Webhook Trigger Callback: Received code %d from backend listener.\n", resp.StatusCode)
}
@@ -0,0 +1,168 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHandleJSONRPCMessageSend(t *testing.T) {
InitPolicies("../policies/default_policy.json")
resetStoredTasks()
payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": "rpc-1",
"method": "SendMessage",
"params": map[string]interface{}{
"metadata": map[string]interface{}{"task_id": "case-123"},
"message": map[string]interface{}{
"kind": "message",
"messageId": "msg-1",
"role": "ROLE_USER",
"parts": []map[string]interface{}{
{
"data": map[string]interface{}{
"schema_version": "contract-compliance.a2a.v1",
"case_id": "case-123",
"contract": map[string]interface{}{
"contract_value": 250000.0,
"contractor_name": "ACME CLOUD SOLUTIONS",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2028-06-01",
"liability_limit": "$1,000,000",
"insurance_coverage": 2000000.0,
"auto_renewal": false,
"has_termination_clause": true,
"term_length_years": 2,
},
},
"mediaType": "application/json",
},
},
},
},
}
response := postJSONRPC(t, payload)
if response.Error != nil {
t.Fatalf("unexpected JSON-RPC error: %+v", response.Error)
}
if response.Result.Kind != "task" {
t.Fatalf("expected A2A task result, got %q", response.Result.Kind)
}
if response.Result.ID != "case-123" {
t.Fatalf("expected task id case-123, got %q", response.Result.ID)
}
if response.Result.Status.State != "TASK_STATE_COMPLETED" {
t.Fatalf("expected completed task state TASK_STATE_COMPLETED, got %q", response.Result.Status.State)
}
message := response.Result.Status.Message
if message == nil {
t.Fatal("expected status message")
}
if message.Role != "ROLE_AGENT" {
t.Fatalf("expected message role ROLE_AGENT, got %q", message.Role)
}
if len(message.Parts) != 1 || message.Parts[0].MediaType != "application/json" {
t.Fatalf("expected one A2A data part with mediaType application/json, got %+v", message.Parts)
}
if passed, ok := message.Parts[0].Data["passed"].(bool); !ok || !passed {
t.Fatalf("expected passed verdict, got %+v", message.Parts[0].Data["passed"])
}
}
func TestHandleJSONRPCLegacyTasksSendStillWorks(t *testing.T) {
InitPolicies("../policies/default_policy.json")
resetStoredTasks()
payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": "rpc-legacy",
"method": "tasks/send",
"params": map[string]interface{}{
"id": "legacy-task",
"message": map[string]interface{}{
"role": "user",
"parts": []map[string]interface{}{
{
"type": "data",
"data": map[string]interface{}{
"contract_value": 900000.0,
"contractor_name": "Risky Vendor",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2032-06-01",
"liability_limit": "unlimited liability",
"insurance_coverage": 500000.0,
"auto_renewal": true,
"has_termination_clause": false,
"term_length_years": 6,
},
},
},
},
},
}
response := postJSONRPC(t, payload)
if response.Error != nil {
t.Fatalf("unexpected JSON-RPC error: %+v", response.Error)
}
if response.Result.ID != "legacy-task" {
t.Fatalf("expected legacy task id, got %q", response.Result.ID)
}
parts := response.Result.Status.Message.Parts
if len(parts) != 1 || parts[0].Type != "data" {
t.Fatalf("expected legacy type=data response part, got %+v", parts)
}
if passed, ok := parts[0].Data["passed"].(bool); !ok || passed {
t.Fatalf("expected failing verdict, got %+v", parts[0].Data["passed"])
}
}
func postJSONRPC(t *testing.T, payload map[string]interface{}) struct {
JSONRPC string `json:"jsonrpc"`
ID string `json:"id"`
Result Task `json:"result"`
Error *JSONRPCError `json:"error"`
} {
t.Helper()
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
request := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
recorder := httptest.NewRecorder()
HandleJSONRPC(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d", recorder.Code)
}
var response struct {
JSONRPC string `json:"jsonrpc"`
ID string `json:"id"`
Result Task `json:"result"`
Error *JSONRPCError `json:"error"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
return response
}
func resetStoredTasks() {
tasksMu.Lock()
defer tasksMu.Unlock()
tasks = make(map[string]*Task)
}
@@ -0,0 +1,10 @@
{
"max_contract_value": 500000.0,
"prohibited_clauses": [
"unlimited liability",
"auto-renewal > 3yr"
],
"required_insurance_minimum": 1000000.0,
"max_term_years": 5,
"required_termination_clause": true
}
@@ -0,0 +1,47 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM python:3.11-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install UV
ADD https://astral.sh/uv/install.sh /install.sh
RUN chmod -R 755 /install.sh && /install.sh && rm /install.sh
ENV PATH="/root/.local/bin:${PATH}"
WORKDIR /app
# Copy project files
COPY pyproject.toml uv.lock* ./
RUN uv venv .venv
ENV PATH="/app/.venv/bin:${PATH}"
COPY . .
# Sync dependencies after package sources are present
RUN uv pip install -e .
EXPOSE 8000
ENV HOST=0.0.0.0
ENV PORT=8000
# Runs standard FastAPI uvicorn listener
CMD ["uvicorn", "app.fast_api_app:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1 @@
# Contract Compliance Pipeline - Python Extraction Service
@@ -0,0 +1,202 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""ADK orchestration reference for the Contract Compliance Pipeline.
The browser cockpit uses fast_api_app.py for the stable, observable demo path:
deterministic extraction plus a focused ADK RemoteA2aAgent handoff to Go. This
module keeps the fuller ADK agent hierarchy as a reference implementation for
the same cross-language design:
SequentialAgent (coordinator)
├── Agent: extractor_agent — Parses contracts, extracts key legal fields
├── RemoteA2aAgent: compliance_agent — Sends fields to Go compliance service via A2A
└── Agent: report_agent — Generates final audit summary report
Keep this distinction clear in docs and demos: fast_api_app.py runs the live
cockpit path with one focused RemoteA2aAgent call; this file shows the fuller
SequentialAgent architecture.
"""
import os
import google.auth
from google.adk.agents import Agent, SequentialAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.apps import App
from google.adk.models import Gemini
from google.genai import types
from app.state_schema import ComplianceStep
from app.tools import (
read_contract_text,
save_extracted_fields,
classify_risk_level,
generate_summary_report,
)
# Establish Google Cloud project details for Gemini API
try:
_, project_id = google.auth.default()
os.environ["GOOGLE_CLOUD_PROJECT"] = project_id
except Exception:
os.environ["GOOGLE_CLOUD_PROJECT"] = "mock-gcp-project"
os.environ["GOOGLE_CLOUD_LOCATION"] = "global"
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"
# Go compliance agent endpoint (configurable via environment variable)
GO_AGENT_CARD_URL = os.environ.get(
"GO_AGENT_CARD_URL",
"http://localhost:8888/.well-known/agent.json"
)
async def initialize_compliance_state(callback_context: CallbackContext) -> None:
"""Initialize pipeline state before the SequentialAgent begins execution.
Sets up all state keys that sub-agents will read and write during the pipeline.
Using ToolContext.state for shared state is a core ADK pattern — all sub-agents
in a SequentialAgent share the same session state dictionary.
"""
state = callback_context.state
if "current_step" not in state:
state["current_step"] = ComplianceStep.INGESTED
if "contract_details" not in state:
state["contract_details"] = {}
if "risk_assessment" not in state:
state["risk_assessment"] = {}
if "compliance_verdict" not in state:
state["compliance_verdict"] = {}
if "trace_logs" not in state:
state["trace_logs"] = []
if "final_report" not in state:
state["final_report"] = {}
# ---------------------------------------------------------------------------
# Sub-Agent 1: Extraction Agent
# ---------------------------------------------------------------------------
# Focused on one job: parse contract text and extract structured legal fields.
# Uses Gemini to understand natural language contract clauses and convert them
# to typed data (floats, dates, booleans).
extractor_agent = Agent(
name="extractor_agent",
model=Gemini(
model="gemini-3.5-flash",
retry_options=types.HttpRetryOptions(attempts=3),
),
instruction="""You are a Legal Data Extraction Agent.
Parse contract documents and extract key legal parameters.
Current pipeline state:
- Step: {current_step}
- Contract Details: {contract_details}
Instructions:
1. If current_step is 'INGESTED', use 'read_contract_text' with the contract filename.
2. Extract these fields from the text:
- contract_value (float, e.g., 250000.0)
- contractor_name (the vendor)
- client_name (the corporate entity)
- start_date (YYYY-MM-DD)
- end_date (YYYY-MM-DD)
- liability_limit (text description, e.g., '$1,000,000' or 'unlimited liability')
- insurance_coverage (float, minimum coverage amount)
- auto_renewal (boolean)
- has_termination_clause (boolean)
3. Call 'save_extracted_fields' with all extracted parameters.
4. Call 'classify_risk_level' to determine the risk tier.
5. Hand control to the next agent.
""",
tools=[read_contract_text, save_extracted_fields, classify_risk_level],
)
# ---------------------------------------------------------------------------
# Sub-Agent 2: A2A Compliance Agent (Cross-Language Handoff)
# ---------------------------------------------------------------------------
# This is the key pattern: ADK's RemoteA2aAgent wraps a remote A2A-compliant service
# as a local sub-agent. ADK handles:
# - Agent Card discovery at the well-known URL
# - JSON-RPC 2.0 message submission (SendMessage)
# - Task response conversion back into ADK events
# - Message/Part serialization
#
# The Go compliance agent runs as a separate process (potentially on a different
# machine, written in a different language) but appears as just another sub-agent
# in the pipeline.
compliance_agent = RemoteA2aAgent(
name="compliance_agent",
agent_card=GO_AGENT_CARD_URL,
description="Validates extracted contract fields against corporate compliance "
"policy rules via the Go-based Security Compliance Validator service.",
)
# ---------------------------------------------------------------------------
# Sub-Agent 3: Report Agent
# ---------------------------------------------------------------------------
# Generates the final audit summary report from the accumulated state.
report_agent = Agent(
name="report_agent",
model=Gemini(
model="gemini-3.5-flash",
retry_options=types.HttpRetryOptions(attempts=3),
),
instruction="""You are a Compliance Reporting Specialist.
Generate the final multi-agent compliance summary report.
Current pipeline state:
- Step: {current_step}
- Compliance Verdict: {compliance_verdict}
- Final Report: {final_report}
Instructions:
1. Call the 'generate_summary_report' tool.
2. Present the completed audit summary in clean Markdown:
- Case ID & Timestamps
- Contractor Name & Contract Value
- Risk Tier
- Compliance Verdict (APPROVED or REJECTED)
- Specific policy violations (if any)
3. Hand control back to the coordinator.
""",
tools=[generate_summary_report],
)
# ---------------------------------------------------------------------------
# Coordinator: SequentialAgent
# ---------------------------------------------------------------------------
# Runs all three sub-agents in order: extract → comply → report.
# This is the "micro-agent" pattern — each agent has one job, a focused prompt,
# and a narrow tool set. Compare this to a "god agent" that would try to handle
# extraction, compliance checking, and reporting all in a single massive prompt.
root_agent = SequentialAgent(
name="contract_compliance_coordinator",
description="Orchestrates contract parsing, A2A compliance validation, "
"and final reporting in sequence.",
sub_agents=[extractor_agent, compliance_agent, report_agent],
before_agent_callback=initialize_compliance_state,
)
app = App(
root_agent=root_agent,
name="app",
)
@@ -0,0 +1 @@
# Shared utilities for telemetry and tracing context propagation
@@ -0,0 +1,67 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import logging
logger = logging.getLogger(__name__)
def setup_telemetry() -> None:
"""Configures system OTel spans tracing options.
Ties together native OpenTelemetry bindings exporting logs to Cloud Trace
if active GCP credentials exist. Falls back safely to standard dev formatters.
"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger.info("Observability Engine: Tracing telemetry pipelines synchronized.")
def inject_trace_propagation_headers(target_headers: dict) -> dict:
"""Injects W3C tracecontext propagation headers into outbound A2A network requests.
Allows distributed tracing tracking metrics across Python/Go boundaries.
"""
# Simple mockup tracing transaction spans generator for dev
trace_id = os.urandom(16).hex()
span_id = os.urandom(8).hex()
# Propagating traceparent header (W3C standard)
traceparent = f"00-{trace_id}-{span_id}-01"
target_headers["traceparent"] = traceparent
logger.info(f"Injecting distributed context headers traceparent: {traceparent}")
return target_headers
def extract_trace_context(incoming_headers: dict) -> dict:
"""Extracts propagation metrics context from incoming boundary headers."""
traceparent = incoming_headers.get("traceparent") or incoming_headers.get("Traceparent")
if not traceparent:
return {}
try:
parts = traceparent.split("-")
if len(parts) == 4:
return {
"trace_id": parts[1],
"parent_span_id": parts[2],
"flags": parts[3]
}
except Exception:
pass
return {}
@@ -0,0 +1,205 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import json
import logging
import time
from google.adk.tools import ToolContext
from app.state_schema import ComplianceStep
# NOTE: This module is retained as a reference implementation showing the
# resilience pattern: timeout, retry, and fail-close to MANUAL_REVIEW. The live
# cockpit path calls the Go compliance agent explicitly from fast_api_app.py so
# the handoff is observable in the browser. agent.py keeps the ADK
# RemoteA2aAgent architecture reference.
async def invoke_a2a_compliance_check(tool_context):
"""Placeholder for the legacy A2A compliance check.
In the original implementation, this function was imported from
a2a_client.py which used hand-rolled HTTP calls to the Go agent.
The live cockpit now uses fast_api_app.invoke_go_compliance_service().
"""
raise NotImplementedError(
"Legacy A2A client removed. Use fast_api_app.invoke_go_compliance_service()."
)
logger = logging.getLogger(__name__)
def _log_structured_json(severity: str, message: str, **kwargs) -> None:
"""Standardized structured JSON logger for GCP/Cloud Logging visibility.
Rule 53 Enforcement: Logs diagnostic messages in parseable JSON structures,
ensuring sensitive session tokens are never printed.
"""
payload = {
"severity": severity,
"message": message,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%Z"),
**kwargs
}
logger.info(json.dumps(payload))
async def invoke_resilient_compliance_check(
tool_context: ToolContext,
timeout_sec: float = 30.0,
max_retries: int = 3,
backoff_factor: float = 1.5,
) -> dict:
"""Wraps A2A task audit dispatching in high-resilience triggers:
1. Handles 503 Crashed Server exceptions using exponential retry loops.
2. Enforces a strict 30-second timeout boundary.
3. Triggers safe fail-close path migrations (transitions step to MANUAL_REVIEW).
"""
state = tool_context.state
case_id = state.get("case_id", "local-case")
# Check if a custom timeout is toggled in memory settings
simulated_timeout = state.get("simulated_timeout", timeout_sec)
_log_structured_json(
severity="INFO",
message=f"Resilience Handler: Initializing compliance check for case {case_id}",
event="compliance_audit_start",
case_id=case_id,
timeout_boundary_sec=simulated_timeout
)
attempt = 0
backoff_delay = 1.0
while attempt < max_retries:
attempt += 1
try:
# Wrap the A2A submission and check in a strict timeout boundary
_log_structured_json(
severity="INFO",
message=f"Dispatching A2A audit request (Attempt {attempt} of {max_retries})",
event="a2a_dispatch_attempt",
case_id=case_id,
attempt=attempt
)
# Execute A2A client validation with timeout caps
res = await asyncio.wait_for(
invoke_a2a_compliance_check(tool_context),
timeout=simulated_timeout
)
# Check response status
if res.get("status") == "dormant_paused":
_log_structured_json(
severity="INFO",
message="External connection reports lag. Task submitted and pipeline entered dormant state.",
event="dormancy_entered",
case_id=case_id,
task_id=res.get("task_id")
)
return res
_log_structured_json(
severity="INFO",
message="A2A compliance validation audit finished successfully.",
event="compliance_audit_completed",
case_id=case_id
)
return res
except asyncio.TimeoutError:
# 30-second timeout reached
_log_structured_json(
severity="WARNING",
message=f"A2A connection timed out after {simulated_timeout} seconds limit.",
event="timeout_reached",
case_id=case_id,
timeout_limit_sec=simulated_timeout
)
break # Move straight to MANUAL_REVIEW fallback
except ConnectionError as e:
# 503 connection error or socket faults -> retry loop triggers
_log_structured_json(
severity="WARNING",
message=f"A2A connection failed on attempt {attempt}: {e!s}",
event="connection_fault",
case_id=case_id,
error=str(e)
)
if attempt >= max_retries:
_log_structured_json(
severity="ERROR",
message="Max A2A delivery retries exhausted. Triggering safety manual routing.",
event="retries_exhausted",
case_id=case_id
)
break
# Perform backoff
logger.info(f"Retrying compliance check after {backoff_delay} seconds delay...")
await asyncio.sleep(backoff_delay)
backoff_delay *= backoff_factor
except Exception as e:
# Non-retryable errors — fail immediately
_log_structured_json(
severity="ERROR",
message=f"A2A audit failed with non-retryable error: {e!s}",
event="non_retryable_error",
case_id=case_id,
error=str(e)
)
break
# --- FALLBACK GATEWAY (Fail Safe Transition to MANUAL_REVIEW) ---
_log_structured_json(
severity="ALERT",
message="Resilience Gate triggered: Transitioning case to MANUAL_REVIEW checkpoint.",
event="fallback_recovery_triggered",
case_id=case_id
)
# Update persistent state metrics safely
state["current_step"] = ComplianceStep.MANUAL_REVIEW
state["pending_signals"] = []
fallback_verdict = {
"passed": False,
"violations": [
"SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold.",
"FAIL-SAFE ACTION: Document routed for legal manager manual verification."
],
"verdict_timestamp": time.strftime("%Y-%m-%d %H:%M:%S %Z")
}
state["compliance_verdict"] = fallback_verdict
# Append trace event logs
state["trace_logs"] = state.get("trace_logs", [])
state["trace_logs"].append({
"span": "resilience_fallback_gate",
"service": "python-extraction-agent",
"duration_ms": 450,
"status": "manual_review_routed"
})
return {
"status": "fallback_manual_review",
"verdict": fallback_verdict,
"message": "Resilience safety check triggered. Case successfully routed for manual legal review."
}
@@ -0,0 +1,574 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""FastAPI application for the Contract Compliance Pipeline.
Provides REST API endpoints for contract upload, status checking, and
result retrieval. The live cockpit uses deterministic extraction and a real
ADK RemoteA2aAgent handoff to the Go compliance service so the demo remains
stable while still exercising the current A2A SendMessage protocol.
"""
import json
import os
import secrets
import shutil
import logging
import uuid
import time
import asyncio
import urllib.error
import urllib.request
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import PlainTextResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from a2a.types import DataPart as A2ADataPart
from google.adk.a2a.agent.config import A2aRemoteAgentConfig, RequestInterceptor
from google.adk.a2a.converters.part_converter import (
A2A_DATA_PART_END_TAG,
A2A_DATA_PART_START_TAG,
A2A_DATA_PART_TEXT_MIME_TYPE,
)
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.apps import App as ADKApp
from google.adk.cli.fast_api import get_fast_api_app
from google.adk.runners import Runner
from google.adk.sessions.database_session_service import DatabaseSessionService
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.genai import types as genai_types
from app.state_schema import ComplianceStep
from app.tools import (
SANDBOX_DIR,
UPLOAD_DIR,
classify_contract_risk,
extract_contract_details_from_text,
)
from app.app_utils.telemetry import setup_telemetry
from app.live_compliance import (
create_compliance_case,
get_case,
latest_case_payload,
sync_case_with_session_state,
case_payload,
artifact_response,
_event,
)
setup_telemetry()
logger = logging.getLogger(__name__)
GO_AGENT_CARD_URL = os.environ.get(
"GO_AGENT_CARD_URL",
"http://localhost:8888/.well-known/agent.json",
)
PROJECT_ROOT = Path(SANDBOX_DIR)
SAMPLE_CONTRACTS_DIR = PROJECT_ROOT / "sample-contracts"
def _go_jsonrpc_url() -> str:
if GO_AGENT_CARD_URL.endswith("/.well-known/agent.json"):
return GO_AGENT_CARD_URL.removesuffix("/.well-known/agent.json")
return GO_AGENT_CARD_URL.rstrip("/")
def build_contract_handoff_data(case_id: str, details: dict, policy: dict | None) -> dict:
data = {
"schema_version": "contract-compliance.a2a.v1",
"case_id": case_id,
"contract": details,
}
if policy:
data["policy"] = policy
return data
def build_go_message_payload(case_id: str, details: dict, policy: dict | None) -> dict:
return {
"jsonrpc": "2.0",
"id": f"case-{case_id}",
"method": "SendMessage",
"params": {
"metadata": {"task_id": case_id},
"message": {
"messageId": f"case-{case_id}-request",
"taskId": case_id,
"role": "ROLE_USER",
"parts": [{
"data": build_contract_handoff_data(case_id, details, policy),
"mediaType": "application/json"
}],
},
},
}
def extract_verdict_from_go_response(response: dict) -> dict:
result = response.get("result", response)
parts = (
result
.get("status", {})
.get("message", {})
.get("parts", [])
)
for part in parts:
if "data" in part:
return part["data"]
if part.get("kind") == "data" or part.get("type") == "data":
return part.get("data", {})
raise RuntimeError("Go compliance service returned no verdict data")
def _a2a_data_part_as_genai_part(data: dict) -> genai_types.Part:
"""Build a GenAI Part that ADK converts back into an A2A DataPart."""
data_part = A2ADataPart(data=data)
return genai_types.Part(
inline_data=genai_types.Blob(
data=(
A2A_DATA_PART_START_TAG
+ data_part.model_dump_json(by_alias=True, exclude_none=True).encode("utf-8")
+ A2A_DATA_PART_END_TAG
),
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
)
)
def _http_json(method: str, url: str, payload: dict | None = None, timeout: float = 10.0) -> dict:
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json", "Accept": "application/json"},
method=method,
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
async def invoke_go_compliance_service(
case_id: str,
details: dict,
policy: dict | None,
timeout: float = 10.0,
) -> dict:
"""Calls the Go service through ADK RemoteA2aAgent and the A2A SDK."""
agent_card = await asyncio.to_thread(_http_json, "GET", GO_AGENT_CARD_URL, None, timeout)
payload = build_go_message_payload(case_id, details, policy)
request_data = build_contract_handoff_data(case_id, details, policy)
async def add_task_metadata(_ctx, a2a_message, parameters):
parameters.request_metadata = {"task_id": case_id}
return a2a_message, parameters
remote_agent = RemoteA2aAgent(
name="compliance_agent",
agent_card=GO_AGENT_CARD_URL,
description="Validates extracted contract fields with the Go A2A compliance service.",
timeout=timeout,
config=A2aRemoteAgentConfig(
request_interceptors=[
RequestInterceptor(before_request=add_task_metadata)
]
),
)
session_service = InMemorySessionService()
adk_app_name = "contract_compliance_remote_handoff"
await session_service.create_session(
app_name=adk_app_name,
user_id="live-cockpit",
session_id=case_id,
state={},
)
runner = Runner(
app=ADKApp(name=adk_app_name, root_agent=remote_agent),
session_service=session_service,
)
response_task = None
request_message = None
error_message = None
try:
async for event in runner.run_async(
user_id="live-cockpit",
session_id=case_id,
new_message=genai_types.Content(
role="user",
parts=[_a2a_data_part_as_genai_part(request_data)],
),
):
metadata = event.custom_metadata or {}
request_message = metadata.get("a2a:request", request_message)
response_task = metadata.get("a2a:response", response_task)
if event.error_message:
error_message = event.error_message
finally:
await remote_agent.cleanup()
if error_message:
raise RuntimeError(error_message)
if response_task is None:
raise RuntimeError("ADK RemoteA2aAgent returned no A2A task response")
response = {
"jsonrpc": "2.0",
"id": payload["id"],
"result": response_task,
}
return {
"agent_card": agent_card,
"request": payload,
"response": response,
"adk_request_message": request_message,
"adk_response_task": response_task,
"verdict": extract_verdict_from_go_response(response),
}
# --- Session Credentials ---
def get_session_secret() -> str:
"""Generate or load a session secret key."""
if os.getenv("SESSION_SECRET_KEY"):
return os.getenv("SESSION_SECRET_KEY")
secret_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "session_secret.txt")
if os.path.exists(secret_file):
try:
with open(secret_file, "r", encoding="utf-8") as f:
return f.read().strip()
except Exception:
pass
ephemeral = secrets.token_hex(32)
try:
with open(secret_file, "w", encoding="utf-8") as f:
f.write(ephemeral)
except Exception:
pass
return ephemeral
# --- App Configuration ---
AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
session_service_uri = "sqlite+aiosqlite:///sessions.db"
app: FastAPI = get_fast_api_app(
agents_dir=AGENT_DIR,
web=True,
session_service_uri=session_service_uri,
allow_origins=[
"http://127.0.0.1:8000",
"http://localhost:8000",
"null",
],
)
app.title = "contract-compliance-pipeline"
app.description = "Multi-agent contract compliance pipeline using ADK and A2A protocol."
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# --- Startup ---
@app.on_event("startup")
def boot_sandbox_bounds():
"""Initialize upload directory and preload sample contracts."""
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Preload sample contracts from project root
sample_dir = str(SAMPLE_CONTRACTS_DIR)
if os.path.exists(sample_dir):
for filename in os.listdir(sample_dir):
src = os.path.join(sample_dir, filename)
dst = os.path.join(UPLOAD_DIR, filename)
if os.path.isfile(src):
try:
shutil.copy2(src, dst)
except Exception as ex:
logger.warning(f"Preloading contract {filename} failed: {ex}")
logger.info(f"Upload directory ready: {UPLOAD_DIR}")
# --- ADK Session Storage ---
db_session_service = DatabaseSessionService(db_url=session_service_uri)
# --- Static Files ---
static_folder = Path(__file__).parent / "static" / "live-compliance"
if static_folder.exists():
app.mount(
"/live-compliance",
StaticFiles(directory=str(static_folder), html=True),
name="live-compliance",
)
@app.get("/demo")
def demo_router() -> RedirectResponse:
return RedirectResponse(url="/live-compliance/")
# --- API Endpoints ---
@app.get("/api/compliance/current")
def get_current_case_status() -> dict:
return latest_case_payload()
@app.get("/api/compliance/cases/{case_id}")
def get_compliance_case(case_id: str) -> dict:
case = get_case(case_id)
return {"active": True, "case": case_payload(case)}
@app.get("/api/compliance/cases/{case_id}/artifacts/{artifact_id}")
def get_compliance_artifact(case_id: str, artifact_id: str):
return artifact_response(case_id, artifact_id)
@app.get("/api/compliance/sample-contracts/{filename}")
def get_sample_contract(filename: str) -> PlainTextResponse:
"""Serves bundled sample contract text for the browser-driven demo."""
safe_filename = os.path.basename(filename)
target = (SAMPLE_CONTRACTS_DIR / safe_filename).resolve()
sample_root = SAMPLE_CONTRACTS_DIR.resolve()
if not str(target).startswith(str(sample_root) + os.path.sep):
raise HTTPException(status_code=403, detail="Invalid sample contract path.")
if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail="Sample contract not found.")
if target.suffix.lower() not in {".pdf", ".txt", ".md"}:
raise HTTPException(status_code=400, detail="Unsupported sample contract type.")
return PlainTextResponse(
target.read_text(encoding="utf-8", errors="ignore"),
headers={"Cache-Control": "no-store"},
)
@app.post("/api/compliance/upload")
async def upload_contract_file(
file: UploadFile = File(...),
simulated_latency: float = Form(0.0),
simulated_server_state: str = Form("normal"),
custom_policies: Optional[str] = Form(None),
) -> dict:
"""Upload a text contract fixture and run the compliance pipeline.
Accepts text files and .pdf-named text fixtures, saves them securely, and kicks off the
multi-agent pipeline: extraction → A2A compliance check → reporting.
"""
filename = file.filename or "contract.pdf"
original_filename = os.path.basename(filename)
# Verify extensions
ext = os.path.splitext(filename)[1].lower()
if ext not in [".pdf", ".txt", ".md"]:
raise HTTPException(status_code=400, detail="Only .pdf, .txt, and .md text fixtures are supported.")
# Rename to UUID for security
case_id = str(uuid.uuid4())
safe_filename = f"{case_id}{ext}"
os.makedirs(UPLOAD_DIR, exist_ok=True)
secure_target = os.path.join(UPLOAD_DIR, safe_filename)
# Path traversal check
absolute_root = os.path.abspath(UPLOAD_DIR) + os.path.sep
absolute_target = os.path.abspath(secure_target)
if not absolute_target.startswith(absolute_root):
raise HTTPException(status_code=403, detail="Invalid upload path.")
# Size limit (5MB)
content = await file.read()
if len(content) > 5 * 1024 * 1024:
raise HTTPException(status_code=400, detail="File size exceeds 5MB limit.")
if ext == ".pdf" and content.lstrip().startswith(b"%PDF"):
raise HTTPException(
status_code=400,
detail=(
"Binary PDF parsing is not included in this demo. "
"Use bundled text fixtures or upload .txt/.md contract text."
),
)
# Save file
try:
with open(absolute_target, "wb") as f:
f.write(content)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to save file: {exc!s}")
# Create compliance case
case = await create_compliance_case(original_filename, db_session_service)
content_text = content.decode("utf-8", errors="ignore")
details = extract_contract_details_from_text(original_filename, content_text)
risk = classify_contract_risk(details)
policy = None
if custom_policies:
try:
policy = json.loads(custom_policies)
except json.JSONDecodeError as exc:
raise HTTPException(status_code=400, detail=f"Invalid custom_policies JSON: {exc!s}")
# Keep simulator values bounded so UI experiments cannot lock the API.
simulated_latency = max(0.0, min(float(simulated_latency), 5.0))
simulated_server_state = simulated_server_state.lower().strip()
trace_logs = [
{
"span": "POST /api/compliance/upload",
"service": "python-extraction-agent",
"duration_ms": 140,
"status": "file_ingested",
},
{
"span": "extract_contract_fields",
"service": "python-extraction-agent",
"duration_ms": 180,
"status": "fields_extracted",
},
{
"span": "classify_risk_level",
"service": "python-extraction-agent",
"duration_ms": 25,
"status": risk["risk_tier"].lower(),
},
]
_event(case, "agent", "Extractor completed", f"{details['contractor_name']} fields extracted and risk classified as {risk['risk_tier']}.")
handoff = {
"status": "prepared",
"source_agent": "python-extraction-agent",
"target_agent": "Security Compliance Validator",
"remote_agent": "google.adk.agents.RemoteA2aAgent(compliance_agent)",
"agent_card_url": GO_AGENT_CARD_URL,
"jsonrpc_url": _go_jsonrpc_url(),
"method": "SendMessage",
"task_id": case.id,
"contract_details": details,
"risk_assessment": risk,
"policy_override": bool(policy),
"request": build_go_message_payload(case.id, details, policy),
}
if simulated_latency:
await asyncio.sleep(simulated_latency)
try:
if simulated_server_state == "crashed":
raise ConnectionError("Simulated Go compliance service 503 failure")
handoff_result = await invoke_go_compliance_service(
case_id=case.id,
details=details,
policy=policy,
timeout=10.0,
)
verdict = handoff_result["verdict"]
handoff.update(
{
"status": "completed",
"agent_card": handoff_result["agent_card"],
"request": handoff_result["request"],
"response": handoff_result["response"],
"adk_request_message": handoff_result["adk_request_message"],
"adk_response_task": handoff_result["adk_response_task"],
"verdict": verdict,
}
)
current_step = (
ComplianceStep.APPROVED
if verdict.get("passed", False)
else ComplianceStep.REVIEW_READY
)
trace_logs.extend(
[
{
"span": "GET /.well-known/agent.json",
"service": "go-compliance-agent",
"duration_ms": 45,
"status": "agent_card_discovered",
},
{
"span": "ADK RemoteA2aAgent SendMessage",
"service": "go-compliance-agent",
"duration_ms": 120,
"status": "completed",
},
{
"span": "go_validate_policy",
"service": "go-compliance-agent",
"duration_ms": 18,
"status": "passed" if verdict.get("passed", False) else "violations_found",
},
]
)
except (ConnectionError, TimeoutError, urllib.error.URLError, RuntimeError) as exc:
logger.warning(f"Go compliance handoff failed for case {case.id}: {exc!s}")
current_step = ComplianceStep.MANUAL_REVIEW
verdict = {
"passed": False,
"violations": [
"SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold.",
"FAIL-SAFE ACTION: Document routed for legal manager manual verification.",
],
"verdict_timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
handoff.update(
{
"status": "failed",
"error": str(exc),
"verdict": verdict,
}
)
trace_logs.append(
{
"span": "resilience_fallback_gate",
"service": "python-extraction-agent",
"duration_ms": 450,
"status": "manual_review_routed",
}
)
session_state = {
"case_id": case.id,
"current_step": current_step,
"contract_filename": original_filename,
"contract_details": details,
"risk_assessment": risk,
"compliance_verdict": verdict,
"handoff": handoff,
"pending_signals": [],
"trace_logs": trace_logs,
"completion_time": time.strftime("%Y-%m-%d %H:%M:%S %Z"),
}
sync_case_with_session_state(case, session_state)
return {"active": True, "case": case_payload(case)}
# --- Local Dev Server ---
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
@@ -0,0 +1,682 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import html
import os
import time
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from fastapi import HTTPException
from fastapi.responses import HTMLResponse
from app.state_schema import ComplianceStep
# Directories for artifact storage (relative to project root)
SANDBOX_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ARTIFACTS_ROOT = os.path.join(SANDBOX_ROOT, "local_artifacts")
def _secure_artifact_dir(case_id: str) -> Path:
"""Resolves target case directories securely and verifies bounds limit (Rule 8)."""
safe_case_id = os.path.basename(case_id)
target_dir = os.path.join(ARTIFACTS_ROOT, "compliance", safe_case_id)
# Fully resolve absolute path boundaries
absolute_root = os.path.abspath(ARTIFACTS_ROOT) + os.path.sep
absolute_target = os.path.abspath(target_dir)
if not absolute_target.startswith(absolute_root):
raise PermissionError("Access Denied: Path traversal detected on artifact write.")
path_obj = Path(absolute_target)
path_obj.mkdir(parents=True, exist_ok=True)
return path_obj
@dataclass
class LiveComplianceCase:
id: str
session_id: str
user_id: str
filename: str
current_step: str = ComplianceStep.INGESTED
pending_signals: list[str] = field(default_factory=list)
status: str = "started"
adk_status: str = "session_created"
risk_tier: str = "MEDIUM"
passed: bool = False
handoff: dict[str, Any] = field(default_factory=dict)
events: list[dict[str, str]] = field(default_factory=list)
artifacts: list[dict[str, str]] = field(default_factory=list)
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
CASES: dict[str, LiveComplianceCase] = {}
SESSION_TO_CASE: dict[str, str] = {}
LATEST_CASE_ID: str | None = None
def get_case(case_id: str) -> LiveComplianceCase:
case = CASES.get(case_id)
if not case:
raise HTTPException(status_code=404, detail="Compliance case not found")
return case
def _event(case: LiveComplianceCase, kind: str, title: str, detail: str) -> None:
case.events.insert(0, {
"kind": kind,
"title": title,
"detail": detail,
"time": time.strftime("%H:%M:%S"),
})
case.updated_at = time.time()
def _artifact(case: LiveComplianceCase, artifact_id: str, title: str, filename: str) -> None:
href = f"/api/compliance/cases/{case.id}/artifacts/{artifact_id}"
existing = next((item for item in case.artifacts if item["id"] == artifact_id), None)
payload = {
"id": artifact_id,
"title": title,
"filename": filename,
"href": href,
"created_at": time.strftime("%H:%M:%S"),
}
if existing:
existing.update(payload)
else:
case.artifacts.insert(0, payload)
# --- HTML VISUAL REPORTS RENDERERS ---
def _extracted_fields_html(case: LiveComplianceCase, details: dict, risk: dict) -> str:
factors_li = "".join([f"<li>{html.escape(f)}</li>" for f in risk.get("risk_factors", [])])
if not factors_li:
factors_li = "<li>No significant legal risk factors identified.</li>"
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Extraction Summary - Case {html.escape(case.id)}</title>
<style>
:root {{
--bg: #121212;
--card: #181818;
--card-soft: #1f1f1f;
--ink: #ffffff;
--muted: #b3b3b3;
--quiet: #7c7c7c;
--line: #4d4d4d;
--accent: #1ed760;
--accent-hover: #1db954;
--error: #f3727f;
--amber: #ffa42b;
--shadow-heavy: rgba(0, 0, 0, 0.5) 0px 8px 24px;
--shadow-card: rgba(0, 0, 0, 0.3) 0px 8px 8px;
--shadow-inset: rgb(18, 18, 18) 0px 1px 0px, rgb(124, 124, 124) 0px 0px 0px 1px inset;
--font-title: SpotifyMixUITitle, CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl, CircularSp-Grek, CircularSp-Deva, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans", "Hiragino Kaku Gothic ProN", Meiryo, "MS Gothic", sans-serif;
--font-ui: SpotifyMixUI, CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl, CircularSp-Grek, CircularSp-Deva, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans", "Hiragino Kaku Gothic ProN", Meiryo, "MS Gothic", sans-serif;
}}
body {{
margin: 0;
padding: clamp(14px, 4vw, 34px);
color: var(--ink);
font-family: var(--font-ui);
background: var(--bg);
}}
main {{
max-width: 800px;
margin: 0 auto;
padding: clamp(20px, 4vw, 30px);
border: 0;
border-radius: 8px;
background: var(--card);
box-shadow: var(--shadow-heavy);
}}
header {{
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: flex-start;
gap: 20px;
border-bottom: 1px solid var(--line);
padding-bottom: 18px;
margin-bottom: 24px;
}}
h1 {{
margin: 0;
font-family: var(--font-title);
font-size: 24px;
font-weight: 700;
line-height: 1.18;
}}
h2 {{
margin: 24px 0 10px;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.4px;
color: var(--ink);
}}
p, li {{
font-size: 14px;
line-height: 1.5;
color: var(--muted);
}}
ul {{
padding-left: 20px;
margin: 0;
}}
.meta {{
color: var(--quiet);
font-family: monospace;
font-size: 11px;
text-align: right;
}}
.grid {{
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin: 18px 0;
}}
.item {{
border: 0;
padding: 12px;
background: var(--card-soft);
border-radius: 8px;
box-shadow: var(--shadow-inset);
}}
.item span {{
display: block;
color: var(--quiet);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.4px;
margin-bottom: 4px;
}}
.item strong {{
display: block;
font-size: 16px;
}}
.badge {{
display: inline-block;
padding: 4px 8px;
border-radius: 99px;
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
border: 1px solid currentColor;
}}
.badge.high {{ background: transparent; color: var(--error); }}
.badge.medium {{ background: transparent; color: var(--amber); }}
.badge.low {{ background: transparent; color: var(--accent); }}
</style>
</head>
<body>
<main>
<header>
<div>
<h1>Legal Ingestion Parameter Sheet</h1>
<p>Extracted variables from raw document text by Extractor Specialist subagent.</p>
</div>
<div class="meta">
Case: {html.escape(case.id[:8])}<br />
Ingested: {html.escape(case.filename)}
</div>
</header>
<h2>Core Parameters</h2>
<section class="grid">
<div class="item"><span>Contractor / Vendor</span><strong>{html.escape(details.get("contractor_name", "N/A"))}</strong></div>
<div class="item"><span>Client Entity</span><strong>{html.escape(details.get("client_name", "N/A"))}</strong></div>
<div class="item"><span>Total Contract Value</span><strong>${details.get("contract_value", 0.0):,.2f}</strong></div>
<div class="item"><span>Contract Duration</span><strong>{details.get("term_length_years", 1)} year(s) ({html.escape(details.get("start_date", ""))} to {html.escape(details.get("end_date", ""))})</strong></div>
<div class="item"><span>Liability Capping Limits</span><strong>{html.escape(details.get("liability_limit", "N/A"))}</strong></div>
<div class="item"><span>Commercial Insurance Coverage</span><strong>${details.get("insurance_coverage", 0.0):,.2f}</strong></div>
<div class="item"><span>Auto-Renewal Trigger</span><strong>{ "Yes (Auto-renewal enabled)" if details.get("auto_renewal") else "No (Term terminates cleanly)" }</strong></div>
<div class="item"><span>Exit Notices Safety</span><strong>{ "Termination Safety Clause exists" if details.get("has_termination_clause") else "None (Missing standard exit notices!)" }</strong></div>
</section>
<h2>Specialist Risk Diagnostics</h2>
<div class="item" style="margin-bottom: 12px;">
<span style="margin-bottom: 6px;">Evaluated Risk Level Bounds</span>
<span class="badge {risk.get('risk_tier', 'MEDIUM').lower()}">{html.escape(risk.get("risk_tier", "MEDIUM"))} Legal Risk</span>
</div>
<h2>Key Risk Indicators (KRIs)</h2>
<ul>
{factors_li}
</ul>
</main>
</body>
</html>"""
def _compliance_cert_html(case: LiveComplianceCase, details: dict, verdict: dict) -> str:
passed = verdict.get("passed", False)
status_class = "approved" if passed else "flagged"
violations = [str(v) for v in verdict.get("violations", [])]
violation_count = len(violations)
if "SYSTEM TIMEOUT" in "".join(violations):
status_class = "manual"
verdict_text = "PASSED COMPLIANCE" if passed else "FLAGGED FOR REVIEW"
if status_class == "manual":
verdict_text = "ROUTED FOR MANUAL REVIEW"
verdict_caption = (
"Zero policy threshold exceptions identified."
if passed
else f"{violation_count} policy exception{'s' if violation_count != 1 else ''} require legal review."
)
if status_class == "manual":
verdict_caption = "Fail-close routing activated because the remote compliance service was unavailable."
violations_li = "".join(
[
(
"<li>"
f"<span>Exception {idx}</span>"
f"<strong>{html.escape(v)}</strong>"
"</li>"
)
for idx, v in enumerate(violations, 1)
]
)
if passed:
violations_li = "<li><span>Clear</span><strong>Zero policy threshold exceptions identified.</strong></li>"
elif not violations_li:
violations_li = "<li><span>Review</span><strong>Go returned a review verdict without enumerated policy exceptions.</strong></li>"
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Compliance Certificate - {html.escape(details.get("contractor_name", "Vendor"))}</title>
<style>
:root {{
--bg: #121212;
--card: #181818;
--card-soft: #1f1f1f;
--ink: #ffffff;
--muted: #b3b3b3;
--quiet: #7c7c7c;
--line: #4d4d4d;
--success: #1ed760;
--success-ink: #121212;
--error: #f3727f;
--amber: #ffa42b;
--shadow-heavy: rgba(0, 0, 0, 0.5) 0px 8px 24px;
--shadow-card: rgba(0, 0, 0, 0.3) 0px 8px 8px;
--font-title: SpotifyMixUITitle, CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl, CircularSp-Grek, CircularSp-Deva, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans", "Hiragino Kaku Gothic ProN", Meiryo, "MS Gothic", sans-serif;
--font-ui: SpotifyMixUI, CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl, CircularSp-Grek, CircularSp-Deva, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans", "Hiragino Kaku Gothic ProN", Meiryo, "MS Gothic", sans-serif;
}}
body {{
margin: 0;
padding: clamp(14px, 4vw, 34px);
color: var(--ink);
font-family: var(--font-ui);
background: var(--bg);
}}
main {{
max-width: 760px;
margin: 0 auto;
padding: clamp(24px, 5vw, 36px);
border: 0;
border-radius: 8px;
background: var(--card);
position: relative;
box-shadow: var(--shadow-heavy);
}}
h1 {{
margin: 0 0 6px;
font-family: var(--font-title);
font-size: 24px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.4px;
line-height: 1.15;
}}
h2 {{
margin: 24px 0 10px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1.4px;
color: var(--quiet);
border-bottom: 1px solid var(--line);
padding-bottom: 6px;
}}
p, li {{
font-size: 14px;
line-height: 1.5;
color: var(--muted);
}}
.badge-strip {{
border: 1px solid var(--line);
padding: 18px;
border-radius: 8px;
margin: 20px 0;
text-align: center;
background: var(--card-soft);
box-shadow: var(--shadow-card);
}}
.badge-strip.approved {{
border-color: var(--success);
color: var(--success);
}}
.badge-strip.flagged {{
border-color: var(--error);
color: var(--error);
}}
.badge-strip.manual {{
border-color: var(--amber);
color: var(--amber);
}}
.badge-strip strong {{
display: block;
font-size: 26px;
font-weight: 700;
letter-spacing: 1.2px;
margin-bottom: 4px;
}}
.badge-strip span {{
display: block;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.8px;
}}
.exception-count {{
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 34px;
height: 28px;
margin-right: 8px;
border-radius: 999px;
background: var(--card-soft);
color: var(--error);
font-weight: 700;
}}
ul {{
padding-left: 20px;
margin: 0;
}}
.exception-list {{
display: flex;
flex-direction: column;
gap: 10px;
padding: 0;
list-style: none;
}}
.exception-list li {{
border: 1px solid var(--line);
border-left: 4px solid { "var(--success)" if passed else ("var(--amber)" if status_class == "manual" else "var(--error)") };
border-radius: 8px;
background: var(--card-soft);
padding: 12px 14px;
}}
.exception-list span {{
display: block;
margin-bottom: 4px;
color: var(--quiet);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.8px;
text-transform: uppercase;
}}
.exception-list strong {{
color: var(--ink);
font-size: 14px;
line-height: 1.45;
}}
.cert-meta {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 28px;
padding-top: 18px;
border-top: 1px solid var(--line);
font-size: 11px;
color: var(--quiet);
}}
.stamp {{
border: 2px dashed var(--line);
padding: 10px;
text-align: center;
border-radius: 9999px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
color: var(--line);
}}
.stamp.approved-stamp {{
border-color: var(--success);
color: var(--success);
}}
.stamp.rejected-stamp {{
border-color: var(--error);
color: var(--error);
}}
.stamp.review-stamp {{
border-color: var(--amber);
color: var(--amber);
}}
</style>
</head>
<body>
<main>
<div style="text-align: center; margin-bottom: 12px;">
<h1 style="color: var(--quiet); font-size: 12px; margin-bottom: 4px; font-family: var(--font-ui); font-weight: 700;">Contract Compliance Engine</h1>
<h1>A2A Audit Certificate</h1>
</div>
<div class="badge-strip {status_class}">
<strong>{verdict_text}</strong>
<span>{html.escape(verdict_caption)}</span>
</div>
<h2>Validation Diagnostics Summary</h2>
<p>
Contract compliance validation conducted by <strong>go-compliance-agent</strong> micro-service on a dedicated Go validation container running policy rules validation.
</p>
<h2><span class="exception-count">{violation_count}</span>Policy Threshold Exception Log</h2>
<ul class="exception-list">
{violations_li}
</ul>
<div class="cert-meta">
<div>
<strong>Case transaction ID:</strong> {html.escape(case.id)}<br />
<strong>A2A target container:</strong> go-compliance-agent:8888<br />
<strong>Audit Timestamp:</strong> {html.escape(verdict.get("verdict_timestamp", ""))}
</div>
<div style="justify-self: end; width: 180px;">
{f"<div class='stamp approved-stamp'>Approved Safe</div>" if passed else (f"<div class='stamp review-stamp'>Manual Review</div>" if status_class == "manual" else "<div class='stamp rejected-stamp'>Review Required</div>")}
</div>
</div>
</main>
</body>
</html>"""
# --- CORE CASE OPERATIONS ---
async def create_compliance_case(filename: str, db_session_service) -> LiveComplianceCase:
"""Scaffolds the active case data structure and registers persistent SQLite sessions."""
global LATEST_CASE_ID
case_id = str(uuid.uuid4())
case = LiveComplianceCase(
id=case_id,
session_id=case_id,
user_id="ops_center",
filename=filename
)
# Register the session in the sqlite persistence engine (enables resume matching)
await db_session_service.create_session(
app_name="app",
user_id=case.user_id,
session_id=case.session_id,
state={
"case_id": case.id,
"current_step": ComplianceStep.INGESTED,
"contract_filename": filename,
"contract_details": {},
"risk_assessment": {},
"compliance_verdict": {},
"pending_signals": []
}
)
_event(case, "system", "Pipeline Initialized", f"Compliance audit started for contract '{filename}'.")
_event(case, "agent", "Coordinator hydrated", "Contract extraction and Go compliance handoff ready.")
CASES[case.id] = case
SESSION_TO_CASE[case.session_id] = case.id
LATEST_CASE_ID = case.id
return case
def save_artifact_file(case: LiveComplianceCase, artifact_id: str, title: str, filename: str, content: str) -> None:
"""Saves generated visual HTML reports securely to sandboxed storage."""
# Strip dangerous parameters to prevent directory traversals (Rule 8)
safe_filename = os.path.basename(filename)
# Enforces boundary and writes
case_dir = _secure_artifact_dir(case.id)
target_path = case_dir / safe_filename
target_path.write_text(content, encoding="utf-8")
_artifact(case, artifact_id, title, safe_filename)
def sync_case_with_session_state(case: LiveComplianceCase, state: dict) -> None:
"""Synchronizes in-memory dashboard cases with ADK database state parameters."""
if not state:
return
case.current_step = state.get("current_step", case.current_step)
case.pending_signals = state.get("pending_signals", case.pending_signals)
details = state.get("contract_details", {})
risk = state.get("risk_assessment", {})
verdict = state.get("compliance_verdict", {})
handoff = state.get("handoff", {})
if handoff:
case.handoff = handoff
# Risk parameters mapping
if risk:
case.risk_tier = risk.get("risk_tier", case.risk_tier)
# Verdict metrics mapping
if verdict:
case.passed = verdict.get("passed", False)
# Synchronize tracking logs
if "trace_logs" in state:
# Avoid duplicate rendering in Cockpit dashboard
case.events = [e for e in case.events if e["kind"] != "trace"]
for log in state["trace_logs"]:
_event(case, "trace", f"Span completed: {log['span']}", f"Service: {log['service']} | Duration: {log['duration_ms']}ms | Status: {log['status']}")
# Check transitions to write artifacts dynamically
if details and "parameters-sheet" not in [a["id"] for a in case.artifacts]:
# extraction artifact generated
html_param = _extracted_fields_html(case, details, risk)
save_artifact_file(case, "parameters-sheet", "Legal parameters sheet", "parameters_sheet.html", html_param)
_event(case, "agent", "Legal sheet generated", "A clean visual parameters sheet was compiled and attached to artifacts.")
if verdict and "compliance-cert" not in [a["id"] for a in case.artifacts]:
# compliance verification card compiled
html_cert = _compliance_cert_html(case, details, verdict)
save_artifact_file(case, "compliance-cert", "Compliance A2A Certificate", "compliance_certificate.html", html_cert)
passed_lbl = "APPROVED" if case.passed else "REJECTED (EXCEPTIONS)"
if "SYSTEM TIMEOUT" in "".join(verdict.get("violations", [])):
passed_lbl = "PENDING MANUAL REVIEW (TIMEOUT)"
_event(case, "agent", "A2A certificate generated", f"Auditing verdict verified: {passed_lbl}.")
# Sync visual status flags
if case.current_step == ComplianceStep.INGESTED:
case.status = "processing_extraction"
elif case.current_step == ComplianceStep.EXTRACTED:
case.status = "processing_risk"
elif case.current_step == ComplianceStep.COMPLIANCE_PENDING:
case.status = "waiting_a2a_task"
_event(case, "network", "A2A task pending", "Connection reports latency. State checkpoints saved for later completion.")
elif case.current_step == ComplianceStep.COMPLIANCE_COMPLETE:
case.status = "compiling_final_report"
elif case.current_step == ComplianceStep.MANUAL_REVIEW:
case.status = "manual_review_needed"
elif case.current_step == ComplianceStep.REVIEW_READY:
case.status = "review_completed_with_violations"
elif case.current_step == ComplianceStep.APPROVED:
case.status = "approved"
case.updated_at = time.time()
def artifact_response(case_id: str, artifact_id: str) -> HTMLResponse:
"""Serves case artifact documents dynamically, verifying bounds (Rule 8 & 120)."""
case = get_case(case_id)
artifact = next((item for item in case.artifacts if item["id"] == artifact_id), None)
if not artifact:
raise HTTPException(status_code=404, detail="Artifact reference not found")
# Enforces boundary limits
safe_filename = os.path.basename(artifact["filename"])
case_dir = _secure_artifact_dir(case.id)
target_path = case_dir / safe_filename
if not target_path.exists():
raise HTTPException(status_code=404, detail="Artifact physical report file missing")
return HTMLResponse(target_path.read_text(encoding="utf-8"))
def case_payload(case: LiveComplianceCase) -> dict[str, Any]:
"""Serializes the complete visual case payload for visual Cockpit frontend."""
return {
"id": case.id,
"session_id": case.session_id,
"user_id": case.user_id,
"filename": case.filename,
"current_step": case.current_step,
"pending_signals": case.pending_signals,
"status": case.status,
"adk_status": case.adk_status,
"risk_tier": case.risk_tier,
"passed": case.passed,
"handoff": case.handoff,
"events": case.events,
"artifacts": case.artifacts,
"updated_at": case.updated_at,
}
def latest_case_payload() -> dict[str, Any]:
"""Returns visual data on the latest audit case."""
if not LATEST_CASE_ID or LATEST_CASE_ID not in CASES:
return {"active": False, "message": "No compliance cases processed yet."}
return {"active": True, "case": case_payload(CASES[LATEST_CASE_ID])}
@@ -0,0 +1,37 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from enum import Enum
class ComplianceStep(str, Enum):
"""Pipeline state machine for contract compliance processing.
Each contract moves through these states as it progresses through
the multi-agent pipeline. Using str+Enum ensures JSON serialization
compatibility with ADK's session state.
State transitions:
INGESTED → EXTRACTED → COMPLIANCE_PENDING → COMPLIANCE_COMPLETE → APPROVED
→ REVIEW_READY
→ MANUAL_REVIEW (on timeout/failure)
"""
INGESTED = "INGESTED" # Contract uploaded, awaiting extraction
EXTRACTED = "EXTRACTED" # Fields extracted, ready for compliance check
COMPLIANCE_PENDING = "COMPLIANCE_PENDING" # A2A task sent to Go agent, awaiting result
COMPLIANCE_COMPLETE = "COMPLIANCE_COMPLETE" # Go agent returned compliance verdict
MANUAL_REVIEW = "MANUAL_REVIEW" # Timeout/error — routed for human review
REVIEW_READY = "REVIEW_READY" # Compliance failed — flagged for legal review
APPROVED = "APPROVED" # Compliance passed — contract approved
@@ -0,0 +1,415 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
import secrets
from pathlib import Path
from google.adk.tools import ToolContext
from app.state_schema import ComplianceStep
# Bounded directories configuration for security path traversal check
# Local checkout root is two levels above python-extraction-agent/app. In the
# Docker image, only python-extraction-agent is copied, so fall back to /app.
_APP_DIR = Path(__file__).resolve().parent
_PYTHON_PROJECT_DIR = _APP_DIR.parent
_LOCAL_REPO_ROOT = _PYTHON_PROJECT_DIR.parent
SANDBOX_DIR = str(
_LOCAL_REPO_ROOT
if (_LOCAL_REPO_ROOT / "sample-contracts").exists()
else _PYTHON_PROJECT_DIR
)
UPLOAD_DIR = os.path.join(SANDBOX_DIR, "uploads")
def _secure_resolve_path(filename: str) -> str:
"""Security Boundary Resolver (enforces path limits to prevent directory traversals).
Rule 8 Enforcement: Sanitizes inputs and resolved target boundaries.
"""
# Active detection blocks (fail-close on traversal vectors)
if ".." in filename or "\\" in filename or (filename.startswith("/") and not filename.startswith(UPLOAD_DIR)):
raise PermissionError("Access Denied: Path traversal attack detected.")
safe_filename = os.path.basename(filename)
target_path = os.path.join(UPLOAD_DIR, safe_filename)
# Fully resolve absolute boundary paths
absolute_root = os.path.abspath(UPLOAD_DIR) + os.path.sep
absolute_target = os.path.abspath(target_path)
if not absolute_target.startswith(absolute_root):
raise PermissionError("Access Denied: Path traversal attack detected.")
return absolute_target
# Mock texts mapping for reliable local demo processing
MOCK_CONTRACT_TEXTS = {
"standard-vendor-agreement.pdf": """
SOFTWARE SERVICES VENDOR AGREEMENT
PARTIES:
This Agreement is entered into by and between ACME CLOUD SOLUTIONS ("Contractor") and GFD PLATFORM SYSTEMS ("Client").
VALUE & SERVICES:
Client agrees to pay Contractor a total consideration of $250,000.00 (Two Hundred and Fifty Thousand Dollars) for the performance of the cloud architectural services.
TERM & COMMENCEMENT:
This Agreement shall commence on June 1, 2026, and shall continue in full force and effect until June 1, 2028 (a duration of exactly 2 years), unless terminated earlier in accordance with the terms herein.
LIMITATION OF LIABILITY:
Except for breach of confidentiality obligations, each party's maximum aggregate liability to the other party under this Agreement shall be strictly capped and limited to $1,000,000.00 (One Million Dollars). Under no circumstances shall either party be liable for indirect or consequential damages.
INSURANCE:
Contractor shall maintain Commercial General Liability insurance during the term of this agreement with a minimum coverage limit of $2,000,000.00.
TERMINATION:
Either party may terminate this agreement upon 30 days prior written notice.
""",
"high-risk-liability-contract.pdf": """
ENTERPRISE CLOUD CONSULTING AGREEMENT
PARTIES:
This Agreement is entered into by and between APEX DATA SYSTEMS ("Contractor") and GFD PLATFORM SYSTEMS ("Client").
VALUE & SERVICES:
Client agrees to pay Contractor a total fee of $450,000.00 for enterprise data engineering services.
TERM & COMMENCEMENT:
This Agreement shall commence on June 1, 2026, and terminate on June 1, 2027 (1 year duration).
LIMITATION OF LIABILITY:
LIABILITY LIMITS ARE EXPLICITLY WAIVED. CONTRACTOR LIABILITY SHALL BE UNLIMITED UNDER ALL CIRCUMSTANCES. GFD PLATFORM SYSTEMS ASSUMES FULL COMPENSATORY RESPONSIBILITY FOR ALL THIRD-PARTY CLAIMS ARISEN OUT OF PERFORMANCE ACTIONS.
INSURANCE:
Contractor shall maintain standard professional liability insurance with minimum coverage of $1,500,000.00.
TERMINATION:
Termination requires 30 days notice.
""",
"non-compliant-contract.pdf": """
LEGACY SYSTEMS INTEGRATION SERVICES CHARTER
PARTIES:
This Agreement is entered into by and between LEGACY NETWORKS CORP ("Contractor") and GFD PLATFORM SYSTEMS ("Client").
VALUE & SERVICES:
Client agrees to pay Contractor a sum of $850,000.00 (Eight Hundred and Fifty Thousand Dollars) for legacy migration activities.
TERM & COMMENCEMENT:
This Charter shall commence on June 1, 2026, and extend until June 1, 2032 (a total term of exactly 6 years).
LIMITATION OF LIABILITY:
THE PARTIES EXPRESSLY AGREE THAT ALL CONTRACTOR RESPONSIBILITY LIMITS ARE INAPPLICABLE. CONTRACTOR'S TOTAL LIABILITY UNDER THIS CONTRACT SHALL BE ENTIRELY UNLIMITED.
INSURANCE:
Contractor shall maintain general liability protection limits of $500,000.00.
TERMINATION:
THIS AGREEMENT SHALL AUTOMATICALLY RENEW FOREVER WITH NO OPTION FOR TERMINATION OR WRITTEN EXIT NOTICES, EXCEPT ON TOTAL LIQUIDATION OF EITHER PARTY.
"""
}
SAMPLE_CONTRACT_DETAILS = {
"standard-vendor-agreement.pdf": {
"contract_value": 250000.0,
"contractor_name": "ACME CLOUD SOLUTIONS",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2028-06-01",
"liability_limit": "$1,000,000.00",
"insurance_coverage": 2000000.0,
"auto_renewal": False,
"has_termination_clause": True,
"term_length_years": 2,
},
"high-risk-liability-contract.pdf": {
"contract_value": 450000.0,
"contractor_name": "APEX DATA SYSTEMS",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2027-06-01",
"liability_limit": "unlimited liability",
"insurance_coverage": 1500000.0,
"auto_renewal": False,
"has_termination_clause": True,
"term_length_years": 1,
},
"non-compliant-contract.pdf": {
"contract_value": 850000.0,
"contractor_name": "LEGACY NETWORKS CORP",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2032-06-01",
"liability_limit": "unlimited liability",
"insurance_coverage": 500000.0,
"auto_renewal": True,
"has_termination_clause": False,
"term_length_years": 6,
},
}
MONTHS = {
"january": "01",
"february": "02",
"march": "03",
"april": "04",
"may": "05",
"june": "06",
"july": "07",
"august": "08",
"september": "09",
"october": "10",
"november": "11",
"december": "12",
}
def _money_to_float(value: str) -> float:
return float(value.replace("$", "").replace(",", ""))
def _iso_date(month: str, day: str, year: str) -> str:
return f"{year}-{MONTHS[month.lower()]}-{int(day):02d}"
def extract_contract_details_from_text(filename: str, text: str) -> dict:
"""Extracts deterministic contract fields from uploaded text for the cockpit demo."""
sample_name = os.path.basename(filename)
normalized_text = text.strip()
if not normalized_text and sample_name in SAMPLE_CONTRACT_DETAILS:
return dict(SAMPLE_CONTRACT_DETAILS[sample_name])
amounts = re.findall(r"\$[\d,]+(?:\.\d{2})?", normalized_text)
dates = re.findall(
r"(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s+(\d{4})",
normalized_text,
flags=re.IGNORECASE,
)
parties = re.search(
r"between\s+(.+?)\s+\(\"Contractor\"\)\s+and\s+(.+?)\s+\(\"Client\"\)",
normalized_text,
flags=re.IGNORECASE | re.DOTALL,
)
duration = re.search(r"(\d+)\s+years?", normalized_text, flags=re.IGNORECASE)
liability_section = ""
liability_match = re.search(
r"LIMITATION OF LIABILITY:\s*(.*?)(?:\n\s*\n|INSURANCE:)",
normalized_text,
flags=re.IGNORECASE | re.DOTALL,
)
if liability_match:
liability_section = liability_match.group(1).strip()
lower_text = normalized_text.lower()
termination_section = ""
termination_match = re.search(
r"TERMINATION:\s*(.*?)(?:\n\s*\n|$)",
normalized_text,
flags=re.IGNORECASE | re.DOTALL,
)
if termination_match:
termination_section = termination_match.group(1).strip().lower()
has_termination_clause = "terminate" in lower_text or "termination" in lower_text
blocked_termination_phrases = (
"no option for termination",
"no option for termination or written exit",
"no termination",
"without termination",
)
if any(phrase in termination_section for phrase in blocked_termination_phrases):
has_termination_clause = False
return {
"contract_value": _money_to_float(amounts[0]) if amounts else 0.0,
"contractor_name": parties.group(1).strip() if parties else "Unknown Contractor",
"client_name": parties.group(2).strip() if parties else "Unknown Client",
"start_date": _iso_date(*dates[0]) if dates else "",
"end_date": _iso_date(*dates[1]) if len(dates) > 1 else "",
"liability_limit": liability_section or "unknown",
"insurance_coverage": _money_to_float(amounts[-1]) if len(amounts) > 1 else 0.0,
"auto_renewal": "automatically renew" in lower_text,
"has_termination_clause": has_termination_clause,
"term_length_years": int(duration.group(1)) if duration else 1,
}
def classify_contract_risk(details: dict) -> dict:
"""Classifies legal risk from extracted details without requiring an ADK context."""
liability = details.get("liability_limit", "").lower()
value = details.get("contract_value", 0.0)
term = details.get("term_length_years", 1)
risk_factors = []
if "unlimited" in liability or "none" in liability:
risk_factors.append("Unlimited contractor liability")
if value > 500000.0:
risk_factors.append("High financial obligation value (> $500k)")
if term > 5:
risk_factors.append("Extended engagement lifecycle duration (> 5 years)")
if not details.get("has_termination_clause", True):
risk_factors.append("Missing exit notice termination safety clauses")
if len(risk_factors) >= 2 or "Unlimited contractor liability" in risk_factors:
risk_tier = "HIGH"
elif len(risk_factors) == 1:
risk_tier = "MEDIUM"
else:
risk_tier = "LOW"
return {"risk_tier": risk_tier, "risk_factors": risk_factors}
def read_contract_text(file_path: str, tool_context: ToolContext) -> str:
"""Reads contract file text content securely, verifying path boundaries.
Args:
file_path: The filename or target path of the contract text fixture.
Returns:
The raw text content extracted from the contract.
"""
try:
# Enforce sandbox isolation limits
secure_path = _secure_resolve_path(file_path)
filename = os.path.basename(secure_path)
if os.path.exists(secure_path):
with open(secure_path, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
# Fallback registry keeps legacy tests and empty demo environments usable.
if filename in MOCK_CONTRACT_TEXTS:
return MOCK_CONTRACT_TEXTS[filename]
return f"[Error: Target contract file '{filename}' was not found in uploads sandbox directory.]"
except Exception as e:
return f"[Error reading file: {e!s}]"
def save_extracted_fields(
contract_value: float,
contractor_name: str,
client_name: str,
start_date: str,
end_date: str,
liability_limit: str,
insurance_coverage: float,
auto_renewal: bool,
has_termination_clause: bool,
tool_context: ToolContext,
) -> dict:
"""Saves extracted contract parameters directly to persistent ADK session state.
Rule 24 & Masking Enforcement: Sensitive identifiers are secured.
"""
state = tool_context.state
# Construct structured contract data profile
contract_data = {
"contract_value": contract_value,
"contractor_name": contractor_name,
"client_name": client_name,
"start_date": start_date,
"end_date": end_date,
"liability_limit": liability_limit,
"insurance_coverage": insurance_coverage,
"auto_renewal": auto_renewal,
"has_termination_clause": has_termination_clause,
}
state["contract_details"] = contract_data
state["current_step"] = ComplianceStep.EXTRACTED
state["pending_signals"] = ["a2a_compliance_check"]
# Calculate term length (approximate)
try:
start_year = int(start_date.split("-")[0])
end_year = int(end_date.split("-")[0])
term_years = max(1, end_year - start_year)
except Exception:
term_years = 1
contract_data["term_length_years"] = term_years
return {
"status": "success",
"message": f"Contract parameters successfully extracted for {contractor_name}.",
"extracted_details": contract_data,
}
def classify_risk_level(tool_context: ToolContext) -> dict:
"""Evaluates extracted clauses to determine initial legal risk tier.
Returns:
A dictionary with risk classification verdict.
"""
state = tool_context.state
details = state.get("contract_details", {})
if not details:
return {"status": "error", "message": "No contract parameters extracted yet."}
state["risk_assessment"] = classify_contract_risk(details)
return {
"status": "success",
"risk_tier": state["risk_assessment"]["risk_tier"],
"risk_factors": state["risk_assessment"]["risk_factors"],
}
def generate_summary_report(tool_context: ToolContext) -> dict:
"""Generates the final multi-agent compliance summary and HTML certificate.
Writes safety report artifacts securely inside Sandbox target bounds.
"""
state = tool_context.state
case_id = state.get("case_id", "local-case")
details = state.get("contract_details", {})
risk = state.get("risk_assessment", {})
verdict = state.get("compliance_verdict", {})
if not details:
return {"status": "error", "message": "No data extracted."}
# Set final steps
state["current_step"] = ComplianceStep.APPROVED if verdict.get("passed", False) else ComplianceStep.REVIEW_READY
state["pending_signals"] = []
report_data = {
"case_id": case_id,
"contractor": details.get("contractor_name", "Unknown Contractor"),
"client": details.get("client_name", "GFD Platform Systems"),
"value": details.get("contract_value", 0.0),
"term": f"{details.get('start_date')} to {details.get('end_date')} ({details.get('term_length_years')} yrs)",
"risk_tier": risk.get("risk_tier", "MEDIUM"),
"risk_factors": risk.get("risk_factors", []),
"passed": verdict.get("passed", False),
"violations": verdict.get("violations", []),
"timestamp": state.get("completion_time", "N/A"),
}
state["final_report"] = report_data
return {
"status": "success",
"message": f"Legal audit report compiled successfully for case {case_id}.",
"report_profile": report_data,
}
@@ -0,0 +1,68 @@
[project]
name = "contract-compliance-pipeline"
version = "0.1.0"
description = "Cross-language multi-agent system for contract compliance using ADK and A2A"
dependencies = [
"google-adk>=1.15.0,<2.0.0",
"opentelemetry-instrumentation-google-genai>=0.1.0,<1.0.0",
"google-cloud-logging>=3.12.0,<4.0.0",
"google-cloud-aiplatform[agent-engines,evaluation]>=1.130.0",
"protobuf>=6.31.1,<7.0.0",
"greenlet>=3.0.0,<4.0.0",
"a2a-sdk>=0.3.4,<0.4",
]
requires-python = ">=3.11,<3.14"
[dependency-groups]
dev = [
"pytest>=8.3.4,<9.0.0",
"pytest-asyncio>=0.23.8,<1.0.0",
"nest-asyncio>=1.6.0,<2.0.0",
]
[project.optional-dependencies]
eval = [
"google-adk[eval]>=1.15.0,<2.0.0",
]
lint = [
"ruff>=0.4.6,<1.0.0",
"ty>=0.0.1a0",
"codespell>=2.2.0,<3.0.0",
]
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"W", # pycodestyle warnings
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
"UP", # pyupgrade
"RUF", # ruff specific rules
]
ignore = ["E501", "C901", "B006"]
[tool.ruff.lint.isort]
known-first-party = ["app"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
pythonpath = "."
asyncio_default_fixture_loop_scope = "function"
[tool.hatch.build.targets.wheel]
packages = ["app"]
[tool.agents-cli]
name = "contract-compliance-pipeline"
description = "Cross-language multi-agent system for contract compliance using ADK and A2A"
base_template = "adk"
agent_directory = "app"
@@ -0,0 +1,132 @@
{
"description": "Mocked A2A response tests — validates Python agent handles every compliance outcome correctly",
"test_cases": [
{
"name": "compliance_pass",
"description": "All contract terms are within policy limits. Agent should mark as APPROVED.",
"mocked_a2a_response": {
"jsonrpc": "2.0",
"id": "test-pass-001",
"result": {
"id": "task-pass-001",
"status": {
"state": "completed",
"message": {
"role": "agent",
"parts": [
{
"type": "data",
"data": {
"passed": true,
"violations": [],
"verdict_timestamp": "2026-06-01T12:00:00Z"
}
}
]
}
}
}
},
"expected_pipeline_state": {
"current_step": "APPROVED",
"compliance_verdict": {
"passed": true,
"violations": []
}
}
},
{
"name": "compliance_fail_single_violation",
"description": "Contract has unlimited liability — should flag for review with violation details.",
"mocked_a2a_response": {
"jsonrpc": "2.0",
"id": "test-fail-001",
"result": {
"id": "task-fail-001",
"status": {
"state": "completed",
"message": {
"role": "agent",
"parts": [
{
"type": "data",
"data": {
"passed": false,
"violations": [
"Unlimited contractor liability clauses are strictly prohibited by company policy"
],
"verdict_timestamp": "2026-06-01T12:01:00Z"
}
}
]
}
}
}
},
"expected_pipeline_state": {
"current_step": "REVIEW_READY",
"compliance_verdict": {
"passed": false,
"violations": [
"Unlimited contractor liability clauses are strictly prohibited by company policy"
]
}
}
},
{
"name": "compliance_fail_multiple_violations",
"description": "Contract has 6 policy violations — should flag with full violation list.",
"mocked_a2a_response": {
"jsonrpc": "2.0",
"id": "test-fail-002",
"result": {
"id": "task-fail-002",
"status": {
"state": "completed",
"message": {
"role": "agent",
"parts": [
{
"type": "data",
"data": {
"passed": false,
"violations": [
"Contract value $850000.00 exceeds company framework limit of $500000.00",
"Unlimited contractor liability clauses are strictly prohibited by company policy",
"Auto-renewal spans extending beyond 3 years are unauthorized",
"Insurance coverage $500000.00 is under the security minimum threshold of $1000000.00",
"Engagement length of 6 years exceeds framework limit bounds of 5 years",
"Missing required written exit termination safety clauses"
],
"verdict_timestamp": "2026-06-01T12:02:00Z"
}
}
]
}
}
}
},
"expected_pipeline_state": {
"current_step": "REVIEW_READY",
"compliance_verdict": {
"passed": false
}
}
},
{
"name": "compliance_timeout",
"description": "Go compliance agent does not respond within 30 seconds — should fall back to MANUAL_REVIEW.",
"mocked_a2a_response": null,
"timeout_seconds": 30,
"expected_pipeline_state": {
"current_step": "MANUAL_REVIEW",
"compliance_verdict": {
"passed": false,
"violations": [
"SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold."
]
}
}
}
]
}
@@ -0,0 +1,73 @@
{
"description": "Golden evaluation tests for contract field extraction accuracy",
"test_cases": [
{
"name": "standard_vendor_agreement_extraction",
"input": {
"filename": "standard-vendor-agreement.pdf",
"user_message": "Audit contract file: 'standard-vendor-agreement.pdf'."
},
"expected_fields": {
"contract_value": 250000.0,
"contractor_name": "ACME CLOUD SOLUTIONS",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2028-06-01",
"liability_limit": "$1,000,000",
"insurance_coverage": 2000000.0,
"auto_renewal": false,
"has_termination_clause": true,
"term_length_years": 2
},
"expected_risk_tier": "LOW",
"expected_risk_factors": []
},
{
"name": "high_risk_liability_extraction",
"input": {
"filename": "high-risk-liability-contract.pdf",
"user_message": "Audit contract file: 'high-risk-liability-contract.pdf'."
},
"expected_fields": {
"contract_value": 450000.0,
"contractor_name": "APEX DATA SYSTEMS",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2027-06-01",
"liability_limit": "unlimited",
"insurance_coverage": 1500000.0,
"auto_renewal": false,
"has_termination_clause": true,
"term_length_years": 1
},
"expected_risk_tier": "HIGH",
"expected_risk_factors": ["Unlimited contractor liability"]
},
{
"name": "non_compliant_contract_extraction",
"input": {
"filename": "non-compliant-contract.pdf",
"user_message": "Audit contract file: 'non-compliant-contract.pdf'."
},
"expected_fields": {
"contract_value": 850000.0,
"contractor_name": "LEGACY NETWORKS CORP",
"client_name": "GFD PLATFORM SYSTEMS",
"start_date": "2026-06-01",
"end_date": "2032-06-01",
"liability_limit": "unlimited",
"insurance_coverage": 500000.0,
"auto_renewal": true,
"has_termination_clause": false,
"term_length_years": 6
},
"expected_risk_tier": "HIGH",
"expected_risk_factors": [
"Unlimited contractor liability",
"High financial obligation value (> $500k)",
"Extended engagement lifecycle duration (> 5 years)",
"Missing exit notice termination safety clauses"
]
}
]
}
@@ -0,0 +1,155 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
from pathlib import Path
from unittest.mock import MagicMock
from google.adk.tools import ToolContext
from app.state_schema import ComplianceStep
from app.tools import (
_secure_resolve_path,
classify_contract_risk,
classify_risk_level,
extract_contract_details_from_text,
read_contract_text,
save_extracted_fields,
)
PROJECT_ROOT = Path(__file__).resolve().parents[3]
@pytest.fixture
def mock_tool_context():
"""Fixture initializing state mapping structures."""
context = MagicMock(spec=ToolContext)
context.state = {
"case_id": "test-case-id",
"current_step": ComplianceStep.INGESTED,
"contract_details": {},
"risk_assessment": {},
"compliance_verdict": {},
"pending_signals": [],
"trace_logs": []
}
return context
def test_read_contract_text_mock_loading(mock_tool_context):
# Acme contract mock loading check
text = read_contract_text("standard-vendor-agreement.pdf", mock_tool_context)
assert "ACME CLOUD SOLUTIONS" in text
assert "GFD PLATFORM SYSTEMS" in text
assert "$250,000.00" in text
def test_extract_contract_details_from_physical_samples():
sample_dir = PROJECT_ROOT / "sample-contracts"
standard_text = (sample_dir / "standard-vendor-agreement.pdf").read_text()
standard = extract_contract_details_from_text(
"standard-vendor-agreement.pdf",
standard_text,
)
assert standard["contract_value"] == 250000.0
assert standard["contractor_name"] == "ACME CLOUD SOLUTIONS"
assert standard["insurance_coverage"] == 2000000.0
assert standard["term_length_years"] == 2
assert standard["has_termination_clause"] is True
assert classify_contract_risk(standard)["risk_tier"] == "LOW"
non_compliant_text = (sample_dir / "non-compliant-contract.pdf").read_text()
non_compliant = extract_contract_details_from_text(
"non-compliant-contract.pdf",
non_compliant_text,
)
assert non_compliant["contract_value"] == 850000.0
assert non_compliant["contractor_name"] == "LEGACY NETWORKS CORP"
assert non_compliant["insurance_coverage"] == 500000.0
assert non_compliant["auto_renewal"] is True
assert non_compliant["has_termination_clause"] is False
assert classify_contract_risk(non_compliant)["risk_tier"] == "HIGH"
def test_read_contract_text_path_traversal_denied(mock_tool_context):
# Verify core resolver raises correct PermissionError boundary violations (Rule 8)
with pytest.raises(PermissionError, match="Access Denied: Path traversal attack detected."):
_secure_resolve_path("../../../etc/passwd")
# Verify client facing tool catches exception and returns safe diagnostic string
res = read_contract_text("../../../etc/passwd", mock_tool_context)
assert "[Error reading file: Access Denied: Path traversal attack detected.]" in res
def test_save_extracted_fields_state_mutations(mock_tool_context):
details = {
"contract_value": 300000.0,
"contractor_name": "Test Vendor Corp",
"client_name": "GFD Platform Systems",
"start_date": "2026-06-01",
"end_date": "2029-06-01",
"liability_limit": "$1,500,000 limits cap",
"insurance_coverage": 2000000.0,
"auto_renewal": False,
"has_termination_clause": True,
}
res = save_extracted_fields(
contract_value=details["contract_value"],
contractor_name=details["contractor_name"],
client_name=details["client_name"],
start_date=details["start_date"],
end_date=details["end_date"],
liability_limit=details["liability_limit"],
insurance_coverage=details["insurance_coverage"],
auto_renewal=details["auto_renewal"],
has_termination_clause=details["has_termination_clause"],
tool_context=mock_tool_context
)
assert res["status"] == "success"
# Assert persistent mutations mapping
state = mock_tool_context.state
assert state["current_step"] == ComplianceStep.EXTRACTED
assert state["contract_details"]["contractor_name"] == "Test Vendor Corp"
assert state["contract_details"]["term_length_years"] == 3 # (2029 - 2026)
def test_classify_risk_level_tiers(mock_tool_context):
state = mock_tool_context.state
# 1. Low risk evaluation mapping
state["contract_details"] = {
"contract_value": 150000.0,
"contractor_name": "A",
"liability_limit": "$1M limited cap",
"term_length_years": 2,
"has_termination_clause": True,
}
classify_risk_level(mock_tool_context)
assert state["risk_assessment"]["risk_tier"] == "LOW"
assert len(state["risk_assessment"]["risk_factors"]) == 0
# 2. High risk evaluation matching unlimited liability
state["contract_details"] = {
"contract_value": 250000.0,
"contractor_name": "B",
"liability_limit": "Unlimited GFD Liability exposure",
"term_length_years": 3,
"has_termination_clause": True,
}
classify_risk_level(mock_tool_context)
assert state["risk_assessment"]["risk_tier"] == "HIGH"
assert "Unlimited contractor liability" in state["risk_assessment"]["risk_factors"]
@@ -0,0 +1,106 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
import asyncio
import logging
from unittest.mock import MagicMock, patch
from google.adk.tools import ToolContext
from app.state_schema import ComplianceStep
from app.fallback_handler import invoke_resilient_compliance_check
logger = logging.getLogger(__name__)
@pytest.fixture
def mock_tool_context():
"""Fixture initializing case checkpoints."""
context = MagicMock(spec=ToolContext)
context.state = {
"case_id": "test-resilience-case",
"current_step": ComplianceStep.EXTRACTED,
"contract_details": {
"contract_value": 250000.0,
"contractor_name": "Standard Corp",
"liability_limit": "Capped liability limits",
"term_length_years": 2,
"insurance_coverage": 2000000.0,
"has_termination_clause": True,
},
"risk_assessment": {"risk_tier": "LOW", "risk_factors": []},
"compliance_verdict": {},
"pending_signals": [],
"trace_logs": []
}
return context
@pytest.mark.asyncio
async def test_invoke_resilient_compliance_check_success(mock_tool_context):
# Mock A2A checker to return immediate success verdict
mock_response = {
"status": "success",
"verdict": {"passed": True, "violations": []}
}
with patch("app.fallback_handler.invoke_a2a_compliance_check", return_value=mock_response):
res = await invoke_resilient_compliance_check(mock_tool_context, timeout_sec=2.0)
assert res["status"] == "success"
assert res["verdict"]["passed"] is True
@pytest.mark.asyncio
async def test_invoke_resilient_compliance_check_connection_crashed_retry(mock_tool_context):
# Proves 503 Crashed Server retry behavior
# Mock failure on first attempt, then success on second
attempt_count = 0
async def mock_compliance_behavior(context):
nonlocal attempt_count
attempt_count += 1
if attempt_count == 1:
raise ConnectionError("503 Server Crashed.")
return {
"status": "success",
"verdict": {"passed": True, "violations": []}
}
with patch("app.fallback_handler.invoke_a2a_compliance_check", side_effect=mock_compliance_behavior):
res = await invoke_resilient_compliance_check(mock_tool_context, timeout_sec=5.0, max_retries=3, backoff_factor=0.1)
assert res["status"] == "success"
assert attempt_count == 2 # Should retry once and then succeed!
@pytest.mark.asyncio
async def test_invoke_resilient_compliance_check_timeout_fallback(mock_tool_context):
# Proves 30s Timeout Recovery Transition (enforces fail-close to MANUAL_REVIEW)
# Mock connection lag (sleeps beyond timeout boundaries limit)
async def mock_lagging_behavior(context):
await asyncio.sleep(5.0) # Sleeps 5 seconds
return {"status": "success"}
with patch("app.fallback_handler.invoke_a2a_compliance_check", side_effect=mock_lagging_behavior):
# Set target timeout barrier to 1.0 second
res = await invoke_resilient_compliance_check(mock_tool_context, timeout_sec=1.0, max_retries=1)
# Verify safety transitions
assert res["status"] == "fallback_manual_review"
state = mock_tool_context.state
assert state["current_step"] == ComplianceStep.MANUAL_REVIEW
assert state["compliance_verdict"]["passed"] is False
assert "SYSTEM TIMEOUT" in state["compliance_verdict"]["violations"][0]
assert "manual_review" in [span["status"] for span in state["trace_logs"]][0]
logger.info("Resilience verification test PASSED successfully.")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
ENTERPRISE CLOUD CONSULTING AGREEMENT
PARTIES:
This Agreement is entered into by and between APEX DATA SYSTEMS ("Contractor") and GFD PLATFORM SYSTEMS ("Client").
VALUE & SERVICES:
Client agrees to pay Contractor a total fee of $450,000.00 for enterprise data engineering services.
TERM & COMMENCEMENT:
This Agreement shall commence on June 1, 2026, and terminate on June 1, 2027 (1 year duration).
LIMITATION OF LIABILITY:
LIABILITY LIMITS ARE EXPLICITLY WAIVED. CONTRACTOR LIABILITY SHALL BE UNLIMITED UNDER ALL CIRCUMSTANCES. GFD PLATFORM SYSTEMS ASSUMES FULL COMPENSATORY RESPONSIBILITY FOR ALL THIRD-PARTY CLAIMS ARISEN OUT OF PERFORMANCE ACTIONS.
INSURANCE:
Contractor shall maintain standard professional liability insurance with minimum coverage of $1,500,000.00.
TERMINATION:
Termination requires 30 days notice.
@@ -0,0 +1,19 @@
LEGACY SYSTEMS INTEGRATION SERVICES CHARTER
PARTIES:
This Agreement is entered into by and between LEGACY NETWORKS CORP ("Contractor") and GFD PLATFORM SYSTEMS ("Client").
VALUE & SERVICES:
Client agrees to pay Contractor a sum of $850,000.00 (Eight Hundred and Fifty Thousand Dollars) for legacy migration activities.
TERM & COMMENCEMENT:
This Charter shall commence on June 1, 2026, and extend until June 1, 2032 (a total term of exactly 6 years).
LIMITATION OF LIABILITY:
THE PARTIES EXPRESSLY AGREE THAT ALL CONTRACTOR RESPONSIBILITY LIMITS ARE INAPPLICABLE. CONTRACTOR'S TOTAL LIABILITY UNDER THIS CONTRACT SHALL BE ENTIRELY UNLIMITED.
INSURANCE:
Contractor shall maintain general liability protection limits of $500,000.00.
TERMINATION:
THIS AGREEMENT SHALL AUTOMATICALLY RENEW FOREVER WITH NO OPTION FOR TERMINATION OR WRITTEN EXIT NOTICES, EXCEPT ON TOTAL LIQUIDATION OF EITHER PARTY.
@@ -0,0 +1,19 @@
SOFTWARE SERVICES VENDOR AGREEMENT
PARTIES:
This Agreement is entered into by and between ACME CLOUD SOLUTIONS ("Contractor") and GFD PLATFORM SYSTEMS ("Client").
VALUE & SERVICES:
Client agrees to pay Contractor a total consideration of $250,000.00 (Two Hundred and Fifty Thousand Dollars) for the performance of the cloud architectural services.
TERM & COMMENCEMENT:
This Agreement shall commence on June 1, 2026, and shall continue in full force and effect until June 1, 2028 (a duration of exactly 2 years), unless terminated earlier in accordance with the terms herein.
LIMITATION OF LIABILITY:
Except for breach of confidentiality obligations, each party's maximum aggregate liability to the other party under this Agreement shall be strictly capped and limited to $1,000,000.00 (One Million Dollars). Under no circumstances shall either party be liable for indirect or consequential damages.
INSURANCE:
Contractor shall maintain Commercial General Liability insurance during the term of this agreement with a minimum coverage limit of $2,000,000.00.
TERMINATION:
Either party may terminate this agreement upon 30 days prior written notice.
+250
View File
@@ -0,0 +1,250 @@
# 👔 Long Running AI Agent Team for New Hire Onboarding
This **HR New Hire Onboarding Coordinator Agent** demonstrates key principles for architecting agents to handle time-agnostic, multi-day workflows. The project contains a ReAct agent powered by [Gemini Flash-Lite](app/agent.py#L67) and built on top of the Google Agent Development Kit (ADK). The agent guides new hires through a structured onboarding state machine and includes a polished live demo UI for showing pause/resume behavior outside the raw ADK web console.
![Live onboarding app overview](assets/live-onboarding-overview.png)
---
## 📖 Overview
Most chatbot tutorials rely on dumping massive conversation history JSON blobs into a vector database to "remember" past turns. However, over multi-day workflows, this unstructured approach introduces severe prompt context pollution, high token costs, and reasoning hallucinations.
To build reliable background agents, we design a **durable, lightweight agent memory schema**. By grounding the [Onboarding Coordinator Agent](app/agent.py#L65) in a strict enum-based state machine, we guarantee the agent maintains its logical reasoning chain across weeks of delay, without relying on raw chat logs. The coordinator coordinates human-in-the-loop task progression and automated system provisioning, ensuring that every onboarding step is completed in order and preventing checkpoints from being bypassed.
---
## 🖥️ Live Onboarding Demo
The repository now includes a production-style React demo served by FastAPI at:
```bash
http://127.0.0.1:8000/live-onboarding/
```
The demo is intentionally built as a two-sided onboarding cockpit:
- **HR command center:** shows backend case state, ADK activity, state-machine progress, local artifacts, and event history.
- **Employee portal:** lets the employee inspect the generated packet, sign it, confirm laptop delivery, and view the resulting artifacts.
The important demo behavior is honest:
- The UI does **not** optimistically advance the workflow.
- Clicking **Sign packet** waits for the backend ADK resume turn to complete before the signed packet/state appears.
- Clicking **Confirm laptop delivered** waits for the hardware ADK resume turn to complete.
- After hardware completion, the employee portal shows the **Hardware delivery receipt** first, then automatically transitions to the **Day One schedule**.
- Local HTML artifacts are real files generated by the FastAPI backend and served through `/api/live-onboarding/cases/{case_id}/artifacts/{artifact_id}`.
This keeps the app demo-friendly while still proving the long-running ADK pattern: the agent pauses at external events, resumes from webhook-like actions, and only moves forward when the backend state has actually changed.
### Demo Flow
1. Start a fresh case.
2. Review the unsigned onboarding packet.
3. Click **Sign packet**.
4. Wait for the ADK signature resume to finish.
5. Review the signed packet.
6. Click **Confirm laptop delivered**.
7. Wait for the ADK hardware resume to finish.
8. See the hardware receipt, then the Day One schedule.
### Run the Live Demo Locally
Authenticate with Google Cloud Application Default Credentials first:
```bash
gcloud auth application-default login
gcloud config set project <your-project-id>
```
Install and run the FastAPI app:
```bash
uv sync
uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000
```
Then open:
```bash
http://127.0.0.1:8000/live-onboarding/
```
If you change the React frontend, rebuild the static assets:
```bash
cd frontend/live-onboarding
npm install
npm run build
```
The build writes directly into `app/static/live-onboarding`, which is what FastAPI serves.
---
## 🔄 Core Design Patterns: Architecting for Time
To transition from building stateless chatbots to reliable background agents, this repository demonstrates three architectural paradigm shifts:
| Stateless Chatbot Pattern | Durable Background Agent Pattern (This Repo) | Why It Matters |
| :--- | :--- | :--- |
| **Stateless memory** (dumps raw JSON logs or chat histories into vector DBs). | **Durable memory schema** (grounded enums serialized to SQLite/Cloud SQL). | Eliminates prompt context pollution, token bloat, and reasoning hallucinations over multi-week wait times. |
| **Active polling / blocked threads** (keeps loops running or actively polls APIs). | **Event-driven dormancy gates** (dormant scale-to-zero wait state resumed via webhooks). | Conserves compute resources; agent sleep state persists durably at rest until external events trigger wake-up. |
| **Monolithic single-agent** (all tools stuffed into one system instruction). | **Multi-agent delegation** (HR coordinator delegates IT setups to specialized subagents). | Decouples complex workflows, keeps prompts targeted, and preserves the logical reasoning chain. |
---
### 🔄 Onboarding State Machine & Idle Time Pause Gates
This coordinator does not run in a single thread or block execution. It employs **durable dormancy gates** during long periods of **"Idle Time"**—specifically when waiting for signatures or shipping delivery carrier callbacks.
```mermaid
stateDiagram-v2
[*] --> START : Initialize details
START --> WELCOME_SENT : send_welcome_packet()
state "WELCOME_SENT" as WELCOME_SENT
note right of WELCOME_SENT
⏳ IDLE TIME PAUSE (Days)
Waiting for local signature webhook callback.
Container scales down; state persisted.
end note
WELCOME_SENT --> DOCUMENTS_SIGNED : Webhook Resume Callback
DOCUMENTS_SIGNED --> IT_PROVISIONED : provision_software_accounts()
state "IT_PROVISIONED" as IT_PROVISIONED
note right of IT_PROVISIONED
⏳ IDLE TIME PAUSE (Days)
Waiting for carrier package delivery webhook.
Container scales down; state persisted.
end note
IT_PROVISIONED --> HARDWARE_DELIVERED : Webhook Resume Callback
HARDWARE_DELIVERED --> COMPLETED : send_day_one_schedule()
COMPLETED --> [*] : Onboarding Completed
```
---
## 🏗️ Project Structure
A clear understanding of the workspace layout and files:
```
new-hire-onboarding/
├── app/ # Core Agent Application
│ ├── agent.py # Main agent logic, model config, & system prompt
│ ├── tools.py # Automated tools (emails, IT provisioning, hardware tracking)
│ ├── state_schema.py # OnboardingStep enum definition
│ ├── resume_handler.py # Callback webhooks to transition states (signatures, delivery)
│ ├── live_onboarding.py # Live demo case state, local artifact generation, and API helpers
│ ├── fast_api_app.py # FastAPI server exposing agent and custom endpoints
│ ├── agent_runtime_app.py # Dedicated Reasoning Engine App wrapper for Agent Runtime
│ ├── static/live-onboarding/ # Built React app served by FastAPI
│ └── app_utils/ # Shared utilities (telemetry, feedback typing)
├── frontend/live-onboarding/ # React + Vite source for the live onboarding UI
├── tests/ # Test Suites
│ ├── unit/ # Basic unit and logic tests
│ ├── integration/ # Local stream and E2E FastAPI server tests
│ └── eval/ # Performance evaluations using Golden Sets
├── pyproject.toml # Poetry/uv project description & dependencies
└── GEMINI.md # AI-assisted development instruction playbook
```
### Key Symbols and Modules
* **State Definitions:** Defined in [OnboardingStep](app/state_schema.py#L15)
* **Agent Instantiation:** Created in [root_agent](app/agent.py#L65) using the `gemini-3.1-flash-lite` model.
* **FastAPI App Setup:** Configured in [fast_api_app.py](app/fast_api_app.py)
* **Webhook Resume Flow:** Implemented in [OnboardingResumeHandler](app/resume_handler.py#L24)
* **Live Demo State and Artifacts:** Implemented in [live_onboarding.py](app/live_onboarding.py)
---
## 🚀 Features and Steps
1. **`START`**
* **Action:** Collects new hire's full name, personal email, and official start date.
* **Tool:** Calls [send_welcome_packet](app/tools.py#L18).
2. **`WELCOME_SENT`**
* **Action:** Pauses for signature. Webhook callback triggers document signing verification.
* **Transition Handler:** [receive_signed_documents_callback](app/resume_handler.py#L29).
3. **`DOCUMENTS_SIGNED`**
* **Action:** Collects desired corporate username prefix.
* **Tool:** Calls [provision_software_accounts](app/tools.py#L44) to set up corporate email and Slack.
4. **`IT_PROVISIONED`**
* **Action:** Collects a laptop hardware tracking ID (e.g., `HW-12345`).
* **Tool/Callback:** Calls [check_hardware_delivery](app/tools.py#L72) or invokes [receive_hardware_delivery_callback](app/resume_handler.py#L50).
5. **`HARDWARE_DELIVERED`**
* **Action:** Finishes hardware step and generates personalized start itinerary.
* **Tool:** Calls [send_day_one_schedule](app/tools.py#L102).
6. **`COMPLETED`**
* **Action:** Onboarding is finished! Prints the completed agenda and congratulates the new employee.
---
## ⚙️ Prerequisites
Ensure you have the following installed:
- **uv**: Python package manager — [Install UV](https://docs.astral.sh/uv/getting-started/installation/)
- **agents-cli**(https://google.github.io/agents-cli/): The official command-line interface for the **Gemini Enterprise Agent Platform**. It manages project scaffolding, local interactive playgrounds (`agents-cli playground`), golden evaluations, and reasoning engine deployments. Install it globally using:
```bash
uv tool install google-agents-cli
```
- **Google Cloud SDK**: Authenticated to Google Cloud — [Install Gcloud](https://cloud.google.com/sdk/docs/install)
---
## 🛠️ Local Development Commands
| Command | Purpose |
| :--- | :--- |
| `agents-cli install` | Installs the project dependencies via `uv` |
| `agents-cli playground` | Runs the agent in an interactive local chat sandbox |
| `uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000` | Runs the FastAPI app and live onboarding UI |
| `cd frontend/live-onboarding && npm run build` | Builds the React UI into `app/static/live-onboarding` |
| `agents-cli lint` | Validates code structure and checks styling formatting |
| `uv run pytest tests/unit` | Runs deterministic live onboarding state and artifact tests |
| `uv run pytest tests/integration` | Runs streaming integration tests & E2E fastapi server validations |
| `.venv/bin/adk eval ./app <evalset.json>` | Runs direct local python evaluation runner bypassing system package registries |
---
## 📊 Evaluation & Validation Loop
The agent's state machine transitions are validated using formal golden evaluation sets:
- **Evaluation Set Config:** [eval_config.json](tests/eval/eval_config.json)
- **Golden Standard Cases:** [onboarding_eval.json](tests/eval/evalsets/onboarding_eval.json)
- **Golden Idle-Time Delay Cases:** [idle_time_delay_eval.json](tests/eval/evalsets/idle_time_delay_eval.json)
Run evaluation metrics locally (using the direct virtualenv runner to avoid credential/conflict overrides):
```bash
.venv/bin/adk eval ./app tests/eval/evalsets/idle_time_delay_eval.json --config_file_path tests/eval/eval_config.json
```
---
## ☁️ Infrastructure & Deployment
1. **Setup Google Cloud Project Config:**
```bash
gcloud config set project <your-project-id>
```
2. **Deploy to Agent Runtime:**
This project's deployment target is scaffolded and pre-configured for **Agent Runtime** (Agent Engines):
```bash
agents-cli deploy
```
Agent Runtime automatically hosts the server, manages persistent sessions out-of-the-box, and natively integrates trace spans with **Cloud Trace** for real-time ambient monitoring.
3. **Enhance Project Infrastructure:**
To add CI/CD runners (GitHub Actions/Cloud Build) or adapt configurations:
```bash
agents-cli scaffold enhance
```
---
## 📡 Observability
This agent includes built-in telemetry pre-configured in [telemetry.py](app/app_utils/telemetry.py). It leverages OpenTelemetry to export trace spans, API logs, and model execution metadata directly to **Cloud Trace**, **Cloud Logging**, and **BigQuery**.
+202
View File
@@ -0,0 +1,202 @@
# 🎓 Tutorial: Building & Deploying Long-running AI Agents with Agent Development Kit and Agent Platform
This tutorial details a profound paradigm shift in generative AI application development: **moving away from stateless chatbots toward building reliable, time-agnostic background processes that can run for weeks.**
Stateless chat applications are excellent for short, immediate Q&A turns. However, real-world enterprise workflows (like HR onboarding, supply chain tracking, or procurement) are dominated by **"idle time"**—periods where the agent must pause execution for days or weeks, waiting for external human actions (like signing a contract) or real-world events (like hardware package deliveries).
This tutorial is styled as an **Iterative Coding Agent Playbook**. You will build a **New Hire Onboarding Coordinator Agent** by feeding high-level, intent-driven prompts incrementally to a coding agent.
---
## 🔄 Conceptual Paradigm: Architecting for Time
When designing long-running background agents, we must shift our thinking across three axes:
1. **From Stateless to Durable**: Instead of keeping conversation history in volatile memory, the agent's state must be durably serialized to a persistent database (e.g. SQLite or Cloud SQL) so it survives system restarts and idle periods.
2. **From Active Polling to Event-Driven Resumption**: Instead of keeping a thread blocked or actively polling APIs for weeks, the agent must enter a dormant "paused" state. It is awakened programmatically only when an external webhook event triggers a resume event.
3. **From Single-Agent Monoliths to Multi-Agent Delegation**: Long-running processes involve disparate tasks. Rather than forcing a single coordinator to hold all tool definitions, we delegate specialized sub-workflows (like IT provisioning) to isolated subagents, handing control back when finished.
---
## 🏗️ Step-by-Step Build Playbook
Before we begin, ensure you have the **Agents CLI** (`agents-cli`) installed. This is the official command-line interface for the **Gemini Enterprise Agent Platform**, providing standard commands to scaffold projects, run local playgrounds, perform golden evaluations, and deploy reasoning engines.
Install it globally using the `uv` tool manager:
```bash
uv tool install google-agents-cli
```
Follow these 6 structured phases to build the time-agnostic onboarding coordinator using iterative agent prompts.
---
### Phase 1: The Foundation Scaffold
We begin by bootstrapping our project structure and preparing it for persistent execution rather than simple chat.
> ### 🤖 Coding Agent Prompt 1: The Foundation Scaffold
>
> "Scaffold a new ADK agent project named 'onboarding-agent' in prototype mode. This is a long-running background process that should survive infrastructure restarts, so please ensure persistent session and memory bank settings are wired up from the start."
#### Key Scaffold Outcomes:
- A standard ADK project structure is initialized.
- Volatile session parameters are cleared to prepare for database storage.
---
### Phase 2: Grounding with a Strict State Machine
Most chatbot tutorials rely on dumping massive conversation history JSON blobs into a vector database to "remember" past turns. However, over multi-day workflows, this unstructured approach introduces severe prompt context pollution, high token costs, and reasoning hallucinations.
To build reliable background agents, we design a **durable, lightweight agent memory schema**. By grounding the coordinator in a strict enum-based state machine, we guarantee the agent maintains its logical reasoning chain across weeks of delay, without relying on raw chat logs.
> ### 🤖 Coding Agent Prompt 2: Grounding with a State Schema
>
> "We need to implement a strict state machine to ground our onboarding coordinator. Define an enum schema with steps: `START`, `WELCOME_SENT`, `DOCUMENTS_SIGNED`, `IT_PROVISIONED`, `HARDWARE_DELIVERED`, and `COMPLETED`, to track and enforce sequential task completion."
#### Resulting Code ([app/state_schema.py](app/state_schema.py)):
```python
from enum import Enum
class OnboardingStep(str, Enum):
START = "START"
WELCOME_SENT = "WELCOME_SENT"
DOCUMENTS_SIGNED = "DOCUMENTS_SIGNED"
IT_PROVISIONED = "IT_PROVISIONED"
HARDWARE_DELIVERED = "HARDWARE_DELIVERED"
COMPLETED = "COMPLETED"
```
---
### Phase 3: Multi-Agent IT Provisioning Delegation
Rather than stuffing all IT and HR tools into a single coordinator agent, we instantiate a specialized subagent responsible solely for software account setups.
> ### 🤖 Coding Agent Prompt 3: Multi-Agent Delegation Setup
>
> "Rather than having our main onboarding coordinator invoke provisioning tools directly, let's delegate IT setup. Add a specialized subagent 'it_agent' to handle software account provisioning, register it under the main coordinator, and update the coordinator's instructions to delegate to it when documents are signed."
#### Resulting Code Segment ([app/agent.py](app/agent.py)):
```python
from google.adk.agents import Agent
from app.tools import provision_software_accounts
it_agent = Agent(
name="it_agent",
instructions="""You are the IT Provisioning Assistant.
Your sole responsibility is to provision corporate accounts.
When asked, collect the desired username prefix and call the 'provision_software_accounts' tool.
Once complete, hand control back to the coordinator immediately.""",
tools=[provision_software_accounts],
model="gemini-3.5-flash"
)
```
---
### Phase 4: Durable Database-Backed Session Storage
In a serverless containerized environment (such as Cloud Run), containers frequently cold-start, scale down to zero when idle, or restart unexpectedly. Furthermore, external API tools might hit transient rate limits. If our agent's state lives in volatile container memory, all in-flight onboarding runs are permanently lost during these events.
To build resilient multi-day background processes, we implement a strict **Checkpoint-and-Resume loop**. By writing our agent's session checkpoints to a persistent database (such as SQLite locally or Cloud SQL in production), the agent can survive container restarts, and failed operations can trigger safe retries without losing or corrupting the onboarding state.
> ### 🤖 Coding Agent Prompt 4: Persistent SQL Session Setup
>
> "To allow our onboarding sessions to survive long delays and server restarts, transition our session storage from volatile in-memory to persistent SQLite storage (sqlite+aiosqlite:///sessions.db) in our FastAPI application."
#### Resulting Code Segment ([app/fast_api_app.py](app/fast_api_app.py)):
```python
from google.adk.sessions import DatabaseSessionService
# Durable SQLite persistence
session_service_uri = "sqlite+aiosqlite:///sessions.db"
db_session_service = DatabaseSessionService(db_url=session_service_uri)
app: FastAPI = get_fast_api_app(
agents_dir=AGENT_DIR,
web=True,
session_service_uri=session_service_uri,
otel_to_cloud=True,
)
```
---
### Phase 5: Event-Driven Resumption & Structured JSON Logging
The most challenging aspect of long-running workflows is managing **"idle time"**—the long gaps when the agent goes dormant to wait for human approvals or slow physical actions (like document signatures or shipping packages).
We handle this by **serializing the agent's working memory** at the moment of the pause, storing both the full conversation thread and our custom step variables in SQLite. When the external signature event triggers the webhook, the resume handler **hydrates the working memory** seamlessly. This ensures the LLM restores its complete context and picks up the logical reasoning chain *exactly* where it left off, without any memory dropouts or hallucinated deviations.
> ### 🤖 Coding Agent Prompt 5: Webhooks & Ambient Wake-up Setup
>
> "Expose webhook endpoints for contract signature and hardware delivery events on our FastAPI server. Create a resume handler that automatically hydrates the matching session state, transitions the onboarding step, and wakes up the agent programmatically using `runner.run_async` to execute the next step ambiently. Include structured JSON logging for all key webhook events."
#### Resulting Webhook Code Segment ([app/fast_api_app.py](app/fast_api_app.py)):
```python
@app.post("/webhooks/document_signed")
async def trigger_document_signed_webhook(payload: WebhookPayload):
await resume_handler.receive_signed_documents_callback(
user_id=payload.user_id,
session_id=payload.session_id
)
return {"status": "success", "message": "Document signature processed, agent resumed."}
```
#### Resulting Resume Handler Segment ([app/resume_handler.py](app/resume_handler.py)):
```python
async for event in self.runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=types.Content(
role="user",
parts=[types.Part.from_text("Resume onboarding: Contract has been signed.")]
)
):
self._log_structured(
severity="INFO",
message=f"Wake-up execution event: {event}",
event="runner_event",
session_id=session_id
)
```
---
### Phase 6: Simulating week-long "Idle Time" delays (Evals)
How do we test an onboarding flow that spans days? We write multi-turn Golden simulation tests that mock delays and webhook triggers.
> ### 🤖 Coding Agent Prompt 6: Golden Case Delay Simulation
>
> "Use the agents-cli evaluation skills to generate an eval set for our onboarding workflow. Create test trajectories that specifically simulate 'idle time'. I need a test case that mocks a 48-hour delay for IT hardware provisioning, and verifies that the agent resumes and successfully routes the final schedule without dropping the new hire's original context."
#### Verifying Evaluations:
Execute the direct ADK virtualenv script to run your delay simulations locally bypassing transient packaging registry issues:
```bash
.venv/bin/adk eval ./app tests/eval/evalsets/idle_time_delay_eval.json --config_file_path tests/eval/eval_config.json
```
---
## 🚀 Production Deployment Setup
When evaluations are green, elevate your project target to **Gemini Enterprise Agent Platform (Agent Runtime)**:
> ### 🤖 Coding Agent Prompt 7: Managed Platform Scaffolding
>
> "We are ready to deploy. Use `agents-cli scaffold enhance` to target Gemini Enterprise Agent Runtime (Agent Engine). Scaffold the Reasoning Engine app wrapper, and ensure Cloud Trace integration is wired up so we can monitor our pause-and-resume latencies in production."
This command:
- Prepares the project to run on Google Cloud Agent Runtime.
- Natively handles session persistence directly in the cloud.
- Enables **Cloud Trace** out-of-the-box by default.
---
## 🎯 Summary of Key Learnings
- **Chatbots are a subset**: Background processes are the future of enterprise AI automation.
- **Grounded state machines**: Enums and persistent databases protect your coordinator from losing tracks during days of inactivity.
- **Mocking time**: ADK evaluations are powerful tools to simulate multi-day delays instantly in continuous integration pipelines.
@@ -0,0 +1,17 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .agent import app
__all__ = ["app"]
+100
View File
@@ -0,0 +1,100 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import google.auth
from google.adk.agents import Agent
from google.adk.agents.callback_context import CallbackContext
from google.adk.apps import App
from google.adk.models import Gemini
from google.genai import types
from app.state_schema import OnboardingStep
from app.tools import (
check_hardware_delivery,
provision_software_accounts,
send_day_one_schedule,
send_welcome_packet,
)
_, project_id = google.auth.default()
os.environ["GOOGLE_CLOUD_PROJECT"] = project_id
os.environ["GOOGLE_CLOUD_LOCATION"] = "global"
os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True"
async def initialize_onboarding_state(callback_context: CallbackContext) -> None:
"""Ensures all onboarding state machine keys are initialized to prevent errors."""
state = callback_context.state
if "current_step" not in state:
state["current_step"] = OnboardingStep.START
if "new_hire_details" not in state:
state["new_hire_details"] = {}
if "pending_signals" not in state:
state["pending_signals"] = []
instruction = """You are an HR Onboarding Coordinator Agent. Your goal is to safely guide the onboarding process of new hires through a sequence of checkpoint steps.
Current Step: {current_step}
New Hire Details: {new_hire_details}
Pending Signals: {pending_signals}
Follow this state machine flow exactly:
1. If current_step is 'START': Ask for the new hire's name, email, and start date. Once provided, invoke the 'send_welcome_packet' tool.
2. If current_step is 'WELCOME_SENT': Inform the user that you are currently in a "idle-time" pause waiting for the employee to sign documents. Do not call other tools.
3. If current_step is 'DOCUMENTS_SIGNED': Delegate the IT accounts provisioning to the 'it_agent' subagent. Do not call tools directly for provisioning accounts; transfer execution to 'it_agent'.
4. If current_step is 'IT_PROVISIONED': Ask for the hardware tracking ID (e.g. HW-12345) to check laptop shipping status. Once provided, invoke 'check_hardware_delivery'.
5. If current_step is 'HARDWARE_DELIVERED': Invoke the 'send_day_one_schedule' tool using the new hire's corporate email.
6. If current_step is 'COMPLETED': State that onboarding is fully complete, congratulate the user, and list the day-one schedule.
Always stay grounded in your tools and current state. Do not skip steps or invent details.
"""
it_agent = Agent(
name="it_agent",
model=Gemini(
model="gemini-3.1-flash-lite",
retry_options=types.HttpRetryOptions(attempts=3),
),
instruction="""You are an IT Provisioning Agent. Your goal is to provision corporate software accounts (email, Slack) for the new hire.
Current Step: {current_step}
New Hire Details: {new_hire_details}
Follow these instructions:
1. Prompt the user/coordinator for the desired corporate username prefix if they haven't provided one.
2. Once provided, invoke the 'provision_software_accounts' tool.
3. After accounts are provisioned, inform the user that provisioning is complete and that control is being returned. Transfer execution back to the parent coordinator agent.
""",
tools=[provision_software_accounts],
)
root_agent = Agent(
name="hr_onboarding_coordinator",
model=Gemini(
model="gemini-3.1-flash-lite",
retry_options=types.HttpRetryOptions(attempts=3),
),
instruction=instruction,
tools=[send_welcome_packet, check_hardware_delivery, send_day_one_schedule],
sub_agents=[it_agent],
before_agent_callback=initialize_onboarding_state,
)
app = App(
root_agent=root_agent,
name="app",
)
@@ -0,0 +1,65 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
from typing import Any
import vertexai
from dotenv import load_dotenv
from google.adk.artifacts import GcsArtifactService, InMemoryArtifactService
from google.cloud import logging as google_cloud_logging
from vertexai.agent_engines.templates.adk import AdkApp
from app.agent import app as adk_app
from app.app_utils.telemetry import setup_telemetry
from app.app_utils.typing import Feedback
# Load environment variables from .env file at runtime
load_dotenv()
class AgentEngineApp(AdkApp):
def set_up(self) -> None:
"""Initialize the agent engine app with logging and telemetry."""
vertexai.init()
setup_telemetry()
super().set_up()
logging.basicConfig(level=logging.INFO)
logging_client = google_cloud_logging.Client()
self.logger = logging_client.logger(__name__)
if gemini_location:
os.environ["GOOGLE_CLOUD_LOCATION"] = gemini_location
def register_feedback(self, feedback: dict[str, Any]) -> None:
"""Collect and log feedback."""
feedback_obj = Feedback.model_validate(feedback)
self.logger.log_struct(feedback_obj.model_dump(), severity="INFO")
def register_operations(self) -> dict[str, list[str]]:
"""Registers the operations of the Agent."""
operations = super().register_operations()
operations[""] = [*operations.get("", []), "register_feedback"]
return operations
gemini_location = os.environ.get("GOOGLE_CLOUD_LOCATION")
logs_bucket_name = os.environ.get("LOGS_BUCKET_NAME")
agent_runtime = AgentEngineApp(
app=adk_app,
artifact_service_builder=lambda: (
GcsArtifactService(bucket_name=logs_bucket_name)
if logs_bucket_name
else InMemoryArtifactService()
),
)
@@ -0,0 +1,52 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
def setup_telemetry() -> str | None:
"""Configure OpenTelemetry and Gen AI telemetry with GCS upload."""
os.environ.setdefault("GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY", "true")
bucket = os.environ.get("LOGS_BUCKET_NAME")
capture_content = os.environ.get(
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false"
)
if bucket and capture_content != "false":
logging.info(
"Prompt-response logging enabled - mode: NO_CONTENT (metadata only, no prompts/responses)"
)
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "NO_CONTENT"
os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT", "jsonl")
os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK", "upload")
os.environ.setdefault(
"OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental"
)
commit_sha = os.environ.get("COMMIT_SHA", "dev")
os.environ.setdefault(
"OTEL_RESOURCE_ATTRIBUTES",
f"service.namespace=new-hire-onboarding,service.version={commit_sha}",
)
path = os.environ.get("GENAI_TELEMETRY_PATH", "completions")
os.environ.setdefault(
"OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH",
f"gs://{bucket}/{path}",
)
else:
logging.info(
"Prompt-response logging disabled (set LOGS_BUCKET_NAME=gs://your-bucket and OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT to enable)"
)
return bucket
@@ -0,0 +1,33 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import uuid
from typing import (
Literal,
)
from pydantic import (
BaseModel,
Field,
)
class Feedback(BaseModel):
"""Represents feedback for a conversation."""
score: int | float
text: str | None = ""
log_type: Literal["feedback"] = "feedback"
service_name: Literal["new-hire-onboarding"] = "new-hire-onboarding"
user_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
@@ -0,0 +1,184 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from google.adk.cli.fast_api import get_fast_api_app
from google.adk.runners import Runner
from google.adk.sessions.database_session_service import DatabaseSessionService
from google.cloud import logging as google_cloud_logging
from pydantic import BaseModel
from app.agent import app as agent_app
from app.app_utils.telemetry import setup_telemetry
from app.app_utils.typing import Feedback
from app.live_onboarding import (
artifact_response,
case_payload,
create_live_case,
get_case,
latest_case_payload,
mark_document_signed,
mark_hardware_delivered,
static_root,
)
from app.resume_handler import OnboardingResumeHandler
setup_telemetry()
logging_client = google_cloud_logging.Client()
logger = logging_client.logger(__name__)
allow_origins = (
os.getenv("ALLOW_ORIGINS", "").split(",") if os.getenv("ALLOW_ORIGINS") else None
)
# Artifact bucket for ADK (created by Terraform, passed via env var)
logs_bucket_name = os.environ.get("LOGS_BUCKET_NAME")
AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Persistent SQLite session configuration
session_service_uri = "sqlite+aiosqlite:///sessions.db"
artifact_service_uri = f"gs://{logs_bucket_name}" if logs_bucket_name else None
app: FastAPI = get_fast_api_app(
agents_dir=AGENT_DIR,
web=True,
artifact_service_uri=artifact_service_uri,
allow_origins=allow_origins,
session_service_uri=session_service_uri,
otel_to_cloud=True,
)
app.title = "new-hire-onboarding"
app.description = "API for interacting with the Agent new-hire-onboarding"
db_session_service = DatabaseSessionService(db_url=session_service_uri)
webhook_runner = Runner(app=agent_app, session_service=db_session_service)
resume_handler = OnboardingResumeHandler(runner=webhook_runner)
app.mount(
"/live-onboarding",
StaticFiles(directory=static_root() / "live-onboarding", html=True),
name="live-onboarding",
)
@app.get("/demo")
def demo_index() -> RedirectResponse:
return RedirectResponse(url="/live-onboarding/")
@app.post("/api/live-onboarding/start")
async def start_live_onboarding() -> dict:
case = await create_live_case(db_session_service)
return {
"active": True,
"case": case_payload(get_case(case.id)),
}
@app.get("/api/live-onboarding/cases/{case_id}")
def get_live_onboarding_case(case_id: str) -> dict:
return {
"active": True,
"case": case_payload(get_case(case_id)),
}
@app.post("/api/live-onboarding/cases/{case_id}/sign")
async def sign_live_onboarding_packet(case_id: str) -> dict:
case = get_case(case_id)
if case.document_signed:
return {"active": True, "case": case_payload(case)}
await mark_document_signed(case, resume_handler)
return {"active": True, "case": case_payload(case)}
@app.post("/api/live-onboarding/cases/{case_id}/deliver-hardware")
async def confirm_live_hardware_delivery(case_id: str) -> dict:
case = get_case(case_id)
if case.hardware_delivered:
return {"active": True, "case": case_payload(case)}
await mark_hardware_delivered(case, resume_handler)
return {"active": True, "case": case_payload(case)}
@app.get("/api/live-onboarding/cases/{case_id}/artifacts/{artifact_id}")
def get_live_onboarding_artifact(case_id: str, artifact_id: str):
return artifact_response(case_id, artifact_id)
@app.get("/api/live-onboarding/current")
def get_current_live_onboarding_case() -> dict:
return latest_case_payload()
class WebhookPayload(BaseModel):
user_id: str
session_id: str
class HardwareWebhookPayload(WebhookPayload):
tracking_id: str
@app.post("/webhooks/document_signed")
async def trigger_document_signed_webhook(payload: WebhookPayload) -> dict[str, str]:
"""Webhook called when employee signs their contract. Wakes up the onboarding agent."""
await resume_handler.receive_signed_documents_callback(
user_id=payload.user_id, session_id=payload.session_id
)
return {
"status": "success",
"message": "Document signature processed, agent resumed.",
}
@app.post("/webhooks/hardware_delivered")
async def trigger_hardware_delivered_webhook(
payload: HardwareWebhookPayload,
) -> dict[str, str]:
"""Webhook called when carrier confirms delivery of the laptop. Wakes up the onboarding agent."""
await resume_handler.receive_hardware_delivery_callback(
user_id=payload.user_id,
session_id=payload.session_id,
tracking_id=payload.tracking_id,
)
return {
"status": "success",
"message": "Hardware delivery processed, agent resumed.",
}
@app.post("/feedback")
def collect_feedback(feedback: Feedback) -> dict[str, str]:
"""Collect and log feedback.
Args:
feedback: The feedback data to log
Returns:
Success message
"""
logger.log_struct(feedback.model_dump(), severity="INFO")
return {"status": "success"}
# Main execution
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,684 @@
import html
import time
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from fastapi import HTTPException
from fastapi.responses import HTMLResponse
from app.state_schema import OnboardingStep
@dataclass
class LiveOnboardingCase:
id: str
session_id: str
user_id: str
employee: dict[str, str]
current_step: str = OnboardingStep.WELCOME_SENT
pending_signals: list[str] = field(default_factory=lambda: ["document_signed"])
status: str = "waiting_for_employee_signature"
document_signed: bool = False
hardware_delivered: bool = False
adk_status: str = "session_ready"
events: list[dict[str, str]] = field(default_factory=list)
artifacts: list[dict[str, str]] = field(default_factory=list)
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
CASES: dict[str, LiveOnboardingCase] = {}
SESSION_TO_CASE: dict[str, str] = {}
LATEST_CASE_ID: str | None = None
def static_root() -> Path:
return Path(__file__).parent / "static"
def artifact_root() -> Path:
path = Path(__file__).resolve().parents[1] / "local_artifacts" / "onboarding"
path.mkdir(parents=True, exist_ok=True)
return path
def _employee() -> dict[str, str]:
return {
"name": "Olivia Bennett",
"email": "olivia.bennett@example.com",
"start_date": "2026-06-01",
"role": "Product Manager",
"team": "Platform Systems",
"manager": "Avery Stone",
"corporate_email": "olivia.bennett@example.com",
"tracking_id": "HW-55443",
"photo_url": "/live-onboarding/olivia-bennett.jpg",
}
def _case_dir(case_id: str) -> Path:
path = artifact_root() / case_id
path.mkdir(parents=True, exist_ok=True)
return path
def _event(case: LiveOnboardingCase, kind: str, title: str, detail: str) -> None:
case.events.insert(
0,
{
"kind": kind,
"title": title,
"detail": detail,
"time": time.strftime("%H:%M:%S"),
},
)
case.updated_at = time.time()
def _artifact(
case: LiveOnboardingCase, artifact_id: str, title: str, kind: str, filename: str
) -> None:
href = f"/api/live-onboarding/cases/{case.id}/artifacts/{artifact_id}"
existing = next(
(item for item in case.artifacts if item["id"] == artifact_id), None
)
payload = {
"id": artifact_id,
"title": title,
"kind": kind,
"filename": filename,
"href": href,
"created_at": time.strftime("%H:%M:%S"),
}
if existing:
existing.update(payload)
else:
case.artifacts.insert(0, payload)
def _packet_html(case: LiveOnboardingCase, signed: bool = False) -> str:
employee = case.employee
signature = (
html.escape(employee["name"]) if signed else "Pending employee signature"
)
signed_at = time.strftime("%Y-%m-%d %H:%M:%S %Z") if signed else ""
signature_class = "signed" if signed else "pending"
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding Packet - {html.escape(employee["name"])}</title>
<style>
:root {{
color-scheme: dark;
--canvas: #12110e;
--panel: #1f1e19;
--panel-soft: #181713;
--ink: #f4f1e8;
--muted: #c8c2b6;
--quiet: #989184;
--line: #343229;
--line-strong: #514e43;
--accent: #f54e00;
--green: #58b991;
}}
body {{
margin: 0;
padding: clamp(14px, 4vw, 34px);
color: var(--ink);
font-family: Inter, system-ui, "Helvetica Neue", Arial, sans-serif;
background: var(--canvas);
}}
main {{
max-width: 820px;
margin: 0 auto;
padding: clamp(18px, 4vw, 30px);
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel);
}}
header {{
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: flex-start;
gap: 20px;
border-bottom: 1px solid var(--line-strong);
padding-bottom: 18px;
}}
h1 {{
margin: 0;
font-size: clamp(22px, 4vw, 28px);
letter-spacing: 0;
line-height: 1.18;
}}
h2 {{
margin: 24px 0 8px;
font-size: 16px;
}}
p, li {{
font-size: 13px;
line-height: 1.55;
}}
p, li, .signature {{
color: var(--muted);
}}
.meta {{
color: var(--quiet);
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 12px;
line-height: 1.5;
text-align: right;
overflow-wrap: anywhere;
}}
.terms {{
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin: 18px 0;
}}
.term {{
border: 1px solid var(--line);
padding: 12px;
font-family: ui-sans-serif, system-ui, sans-serif;
background: var(--panel-soft);
}}
.term span {{
display: block;
color: var(--quiet);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}}
.term strong {{
display: block;
margin-top: 5px;
font-size: 14px;
overflow-wrap: anywhere;
}}
.signature {{
margin-top: 24px;
border: 1px solid var(--line);
padding: 14px;
font-family: ui-sans-serif, system-ui, sans-serif;
}}
.signature strong {{
display: block;
margin-top: 8px;
color: var(--accent);
font-size: clamp(20px, 4vw, 24px);
font-family: "Brush Script MT", "Segoe Script", cursive;
font-weight: 500;
}}
.signature.signed {{
border-color: rgba(88, 185, 145, 0.45);
background: #18251d;
}}
.stamp {{
display: inline-block;
margin-top: 10px;
color: var(--green);
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
}}
@media (max-width: 620px) {{
header,
.terms {{
grid-template-columns: 1fr;
}}
.meta {{
text-align: left;
}}
ul {{
padding-left: 20px;
}}
}}
</style>
</head>
<body>
<main>
<header>
<div>
<h1>New Hire Onboarding Packet</h1>
<p>This local packet is generated for the demo workflow. It is not a legal contract or e-signature integration.</p>
</div>
<div class="meta">
Packet ID: {html.escape(case.id)}<br />
Session ID: {html.escape(case.session_id)}<br />
Generated locally
</div>
</header>
<section class="terms">
<div class="term"><span>Employee</span><strong>{html.escape(employee["name"])}</strong></div>
<div class="term"><span>Email</span><strong>{html.escape(employee["email"])}</strong></div>
<div class="term"><span>Role</span><strong>{html.escape(employee["role"])}</strong></div>
<div class="term"><span>Start date</span><strong>{html.escape(employee["start_date"])}</strong></div>
<div class="term"><span>Team</span><strong>{html.escape(employee["team"])}</strong></div>
<div class="term"><span>Manager</span><strong>{html.escape(employee["manager"])}</strong></div>
</section>
<h2>Welcome</h2>
<p>
Welcome to Platform Systems. This packet confirms the onboarding details that the HR
onboarding coordinator will use to prepare access, equipment, and the Day One schedule.
</p>
<h2>Employee acknowledgements</h2>
<ul>
<li>I confirm that my onboarding profile is accurate.</li>
<li>I acknowledge that IT access and hardware delivery depend on completing this packet.</li>
<li>I understand this demo stores a local signed artifact for inspection by HR.</li>
</ul>
<section class="signature {signature_class}">
Signature
<strong>{signature}</strong>
{"<span class='stamp'>Signed locally at " + html.escape(signed_at) + "</span>" if signed else ""}
</section>
</main>
</body>
</html>"""
def _schedule_html(case: LiveOnboardingCase) -> str:
employee = case.employee
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Day One Schedule</title>
<style>
:root {{
color-scheme: dark;
--canvas: #12110e;
--panel: #1f1e19;
--panel-soft: #181713;
--ink: #f4f1e8;
--muted: #c8c2b6;
--quiet: #989184;
--line: #343229;
--accent: #f54e00;
}}
body {{
margin: 0;
padding: clamp(14px, 4vw, 34px);
color: var(--ink);
font-family: Inter, system-ui, "Helvetica Neue", Arial, sans-serif;
background: var(--canvas);
}}
main {{
max-width: 760px;
margin: 0 auto;
padding: clamp(20px, 5vw, 34px);
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel);
}}
h1 {{
margin: 0 0 10px;
font-size: clamp(24px, 5vw, 34px);
font-weight: 400;
letter-spacing: -0.68px;
line-height: 1.2;
}}
p {{
margin: 0 0 28px;
color: var(--muted);
line-height: 1.55;
}}
ol {{
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}}
li {{
display: grid;
grid-template-columns: minmax(58px, 72px) minmax(0, 1fr);
gap: 16px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--muted);
background: var(--panel-soft);
}}
strong {{
color: var(--accent);
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
font-weight: 500;
}}
.meta {{
color: var(--quiet);
font-size: 13px;
}}
@media (max-width: 520px) {{
li {{
grid-template-columns: 1fr;
gap: 6px;
}}
}}
</style>
</head>
<body>
<main>
<h1>Day One Schedule for {html.escape(employee["name"])}</h1>
<p>Sent to {html.escape(employee["corporate_email"])} after hardware delivery was confirmed.</p>
<ol>
<li><strong>09:00</strong><span>Welcome and IT login setup</span></li>
<li><strong>10:00</strong><span>Meet {html.escape(employee["manager"])} and the team</span></li>
<li><strong>11:30</strong><span>Platform Systems overview</span></li>
<li><strong>14:00</strong><span>Security and device walkthrough</span></li>
</ol>
</main>
</body>
</html>"""
def _hardware_receipt_html(case: LiveOnboardingCase) -> str:
employee = case.employee
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hardware Delivery Receipt</title>
<style>
:root {{
color-scheme: dark;
--canvas: #12110e;
--panel: #1f1e19;
--panel-soft: #181713;
--ink: #f4f1e8;
--muted: #c8c2b6;
--quiet: #989184;
--line: #343229;
--success: #58b991;
}}
body {{
margin: 0;
padding: clamp(14px, 4vw, 34px);
color: var(--ink);
font-family: Inter, system-ui, "Helvetica Neue", Arial, sans-serif;
background: var(--canvas);
}}
main {{
max-width: 680px;
margin: 0 auto;
padding: clamp(20px, 5vw, 34px);
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel);
}}
h1 {{
margin: 0 0 12px;
font-size: clamp(24px, 5vw, 34px);
font-weight: 400;
letter-spacing: -0.68px;
line-height: 1.2;
}}
p {{
margin: 0;
color: var(--muted);
line-height: 1.55;
}}
.receipt {{
margin-top: 24px;
padding: 18px;
border: 1px solid rgba(88, 185, 145, 0.42);
border-radius: 8px;
background: #18251d;
}}
.receipt span {{
display: block;
margin-bottom: 6px;
color: var(--quiet);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.88px;
text-transform: uppercase;
}}
strong {{
color: var(--success);
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 14px;
font-weight: 500;
}}
</style>
</head>
<body>
<main>
<h1>Hardware Delivery Receipt</h1>
<p>Employee-confirmed local receipt generated by the onboarding demo.</p>
<section class="receipt">
<span>Confirmed delivery</span>
<p><strong>{html.escape(employee["tracking_id"])}</strong> delivered to {html.escape(employee["name"])}.</p>
</section>
</main>
</body>
</html>"""
def _write_artifact(case: LiveOnboardingCase, filename: str, body: str) -> None:
(_case_dir(case.id) / filename).write_text(body, encoding="utf-8")
def case_payload(case: LiveOnboardingCase) -> dict[str, Any]:
return {
"id": case.id,
"session_id": case.session_id,
"user_id": case.user_id,
"employee": case.employee,
"current_step": case.current_step,
"pending_signals": case.pending_signals,
"status": case.status,
"document_signed": case.document_signed,
"hardware_delivered": case.hardware_delivered,
"adk_status": case.adk_status,
"events": case.events,
"artifacts": case.artifacts,
"updated_at": case.updated_at,
}
async def create_live_case(session_service) -> LiveOnboardingCase:
global LATEST_CASE_ID
employee = _employee()
case_id = str(uuid.uuid4())
case = LiveOnboardingCase(
id=case_id,
session_id=case_id,
user_id="employee",
employee=employee,
)
await session_service.create_session(
app_name="app",
user_id=case.user_id,
session_id=case.session_id,
state={
"current_step": OnboardingStep.WELCOME_SENT,
"new_hire_details": {
"name": employee["name"],
"email": employee["email"],
"start_date": employee["start_date"],
},
"pending_signals": ["document_signed"],
},
)
_write_artifact(case, "welcome_packet.html", _packet_html(case))
_artifact(
case,
"welcome-packet",
"Unsigned onboarding packet",
"html",
"welcome_packet.html",
)
_event(
case,
"agent",
"Welcome packet generated",
"A real local HTML onboarding packet was generated and attached to this case.",
)
_event(
case,
"state",
"Agent is waiting",
"ADK session is parked at WELCOME_SENT until the employee signs the packet.",
)
CASES[case.id] = case
SESSION_TO_CASE[case.session_id] = case.id
LATEST_CASE_ID = case.id
return case
async def mark_document_signed(case: LiveOnboardingCase, resume_handler) -> None:
case.document_signed = True
case.status = "waking_after_signature"
case.current_step = OnboardingStep.DOCUMENTS_SIGNED
case.pending_signals = []
_write_artifact(
case, "signed_onboarding_packet.html", _packet_html(case, signed=True)
)
_artifact(
case,
"signed-packet",
"Signed onboarding packet",
"html",
"signed_onboarding_packet.html",
)
_event(
case,
"employee",
f"{case.employee['name']} signed the packet",
"The employee clicked Sign Packet. A signed local artifact was stored.",
)
_event(
case,
"webhook",
"document_signed webhook fired",
"The app called the same ADK resume handler used by /webhooks/document_signed.",
)
try:
await resume_handler.receive_signed_documents_callback(
user_id=case.user_id, session_id=case.session_id
)
case.adk_status = "document_resume_completed"
case.current_step = OnboardingStep.IT_PROVISIONED
case.pending_signals = ["hardware_delivered"]
case.status = "waiting_for_hardware_delivery"
_event(
case,
"agent",
"ADK resume completed",
"The document signature wake turn completed. HR can now wait for hardware delivery.",
)
except Exception as exc:
case.adk_status = "document_resume_failed"
case.status = "document_signed_adk_resume_failed"
_event(case, "error", "ADK resume failed", str(exc))
finally:
case.updated_at = time.time()
async def mark_hardware_delivered(case: LiveOnboardingCase, resume_handler) -> None:
case.hardware_delivered = True
case.status = "waking_after_hardware_delivery"
case.current_step = OnboardingStep.HARDWARE_DELIVERED
case.pending_signals = []
_write_artifact(
case,
"hardware_delivery_receipt.html",
_hardware_receipt_html(case),
)
_artifact(
case,
"hardware-receipt",
"Hardware delivery receipt",
"html",
"hardware_delivery_receipt.html",
)
_event(
case,
"employee",
f"{case.employee['name']} confirmed laptop delivery",
f"Employee confirmed receipt of tracking ID {case.employee['tracking_id']}.",
)
_event(
case,
"webhook",
"hardware_delivered webhook fired",
"The app called the same ADK resume handler used by /webhooks/hardware_delivered.",
)
try:
await resume_handler.receive_hardware_delivery_callback(
user_id=case.user_id,
session_id=case.session_id,
tracking_id=case.employee["tracking_id"],
)
case.adk_status = "hardware_resume_completed"
case.current_step = OnboardingStep.COMPLETED
case.status = "completed"
_write_artifact(case, "day_one_schedule.html", _schedule_html(case))
_artifact(
case,
"day-one-schedule",
"Day One schedule",
"html",
"day_one_schedule.html",
)
_event(
case,
"agent",
"Onboarding completed",
"The hardware wake turn completed and a local Day One schedule artifact was stored.",
)
except Exception as exc:
case.adk_status = "hardware_resume_failed"
case.status = "hardware_delivered_adk_resume_failed"
_event(case, "error", "ADK resume failed", str(exc))
finally:
case.updated_at = time.time()
def case_for_session(session_id: str) -> LiveOnboardingCase | None:
case_id = SESSION_TO_CASE.get(session_id)
if not case_id:
return None
return CASES.get(case_id)
def get_case(case_id: str) -> LiveOnboardingCase:
case = CASES.get(case_id)
if not case:
raise HTTPException(status_code=404, detail="Onboarding case not found")
return case
def artifact_response(case_id: str, artifact_id: str) -> HTMLResponse:
case = get_case(case_id)
artifact = next(
(item for item in case.artifacts if item["id"] == artifact_id), None
)
if not artifact:
raise HTTPException(status_code=404, detail="Artifact not found")
path = _case_dir(case.id) / artifact["filename"]
if not path.exists():
raise HTTPException(status_code=404, detail="Artifact file not found")
return HTMLResponse(path.read_text(encoding="utf-8"))
def empty_case_payload() -> dict[str, Any]:
return {"active": False, "message": "No live onboarding case has been started."}
def latest_case_payload() -> dict[str, Any]:
if not LATEST_CASE_ID or LATEST_CASE_ID not in CASES:
return empty_case_payload()
return {"active": True, "case": case_payload(CASES[LATEST_CASE_ID])}
@@ -0,0 +1,173 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
from google.adk.runners import Runner
from google.genai import types
from app.state_schema import OnboardingStep
logger = logging.getLogger(__name__)
class OnboardingResumeHandler:
def __init__(self, runner: Runner):
"""Initializes the resume handler with the active ADK Runner."""
self.runner = runner
def _log_structured(self, severity: str, message: str, **kwargs) -> None:
"""Helper to output formatted JSON logs that Cloud Logging can parse natively."""
payload = {"severity": severity, "message": message, **kwargs}
logger.info(json.dumps(payload))
async def receive_signed_documents_callback(
self, user_id: str, session_id: str
) -> None:
"""Simulates an external webhook notifying that the employee signed onboarding documents.
Hydrates the existing session, transitions the checkpoint to DOCUMENTS_SIGNED, and resumes.
"""
self._log_structured(
severity="INFO",
message=f"Received document signature notification for session {session_id}",
event="webhook_received",
webhook_type="document_signed",
session_id=session_id,
user_id=user_id,
)
try:
self._log_structured(
severity="INFO",
message=f"State machine transitioned to {OnboardingStep.DOCUMENTS_SIGNED}",
event="state_transition",
session_id=session_id,
user_id=user_id,
new_step=OnboardingStep.DOCUMENTS_SIGNED,
)
# Trigger runner wake-up and run execution ambiently
async for event in self.runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=types.Content(
role="user",
parts=[
types.Part.from_text(
text="Resume onboarding: Contract has been signed."
)
],
),
state_delta={
"current_step": OnboardingStep.DOCUMENTS_SIGNED,
"pending_signals": [],
},
):
self._log_structured(
severity="INFO",
message=f"Wake-up execution event: {event}",
event="runner_event",
session_id=session_id,
user_id=user_id,
)
self._log_structured(
severity="INFO",
message="Ambient document signature execution turn completed successfully",
event="runner_turn_success",
session_id=session_id,
user_id=user_id,
)
except Exception as e:
self._log_structured(
severity="ERROR",
message=f"Ambient document signature execution turn failed: {e!s}",
event="runner_turn_failure",
session_id=session_id,
user_id=user_id,
error=str(e),
)
raise
async def receive_hardware_delivery_callback(
self, user_id: str, session_id: str, tracking_id: str
) -> None:
"""Simulates a carrier callback confirming laptop package delivery at the employee's house.
Hydrates the session, transitions the checkpoint to HARDWARE_DELIVERED, and resumes.
"""
self._log_structured(
severity="INFO",
message=f"Received hardware package delivery webhook for tracking ID {tracking_id}",
event="webhook_received",
webhook_type="hardware_delivered",
session_id=session_id,
user_id=user_id,
tracking_id=tracking_id,
)
try:
self._log_structured(
severity="INFO",
message=f"State machine transitioned to {OnboardingStep.HARDWARE_DELIVERED}",
event="state_transition",
session_id=session_id,
user_id=user_id,
new_step=OnboardingStep.HARDWARE_DELIVERED,
)
# Trigger runner wake-up and run execution ambiently
async for event in self.runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=types.Content(
role="user",
parts=[
types.Part.from_text(
text=f"Resume onboarding: Hardware delivered with tracking ID {tracking_id}."
)
],
),
state_delta={
"current_step": OnboardingStep.HARDWARE_DELIVERED,
"pending_signals": [],
},
):
self._log_structured(
severity="INFO",
message=f"Wake-up execution event: {event}",
event="runner_event",
session_id=session_id,
user_id=user_id,
)
self._log_structured(
severity="INFO",
message="Ambient hardware delivery execution turn completed successfully",
event="runner_turn_success",
session_id=session_id,
user_id=user_id,
)
except Exception as e:
self._log_structured(
severity="ERROR",
message=f"Ambient hardware delivery execution turn failed: {e!s}",
event="runner_turn_failure",
session_id=session_id,
user_id=user_id,
error=str(e),
)
raise
@@ -0,0 +1,22 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class OnboardingStep:
START = "START"
WELCOME_SENT = "WELCOME_SENT"
DOCUMENTS_SIGNED = "DOCUMENTS_SIGNED"
IT_PROVISIONED = "IT_PROVISIONED"
HARDWARE_DELIVERED = "HARDWARE_DELIVERED"
COMPLETED = "COMPLETED"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Live Onboarding Desk</title>
<script type="module" crossorigin src="/live-onboarding/assets/index-GxbLCVag.js"></script>
<link rel="stylesheet" crossorigin href="/live-onboarding/assets/index-DgUrFQNs.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More