chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[*.sh]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,2 @@
|
||||
code_review:
|
||||
comment_severity_threshold: LOW
|
||||
@@ -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-*`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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> 🦕
|
||||
@@ -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.
|
||||
@@ -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-----
|
||||
@@ -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
|
||||
(\\?['"]|")[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]+
|
||||
@@ -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]
|
||||
@@ -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,}
|
||||
@@ -0,0 +1,11 @@
|
||||
^attache$
|
||||
^bellow$
|
||||
benefitting
|
||||
occurences?
|
||||
^dependan.*
|
||||
^oer$
|
||||
Sorce
|
||||
^[Ss]pae.*
|
||||
^untill$
|
||||
^untilling$
|
||||
^wether.*
|
||||
@@ -0,0 +1,2 @@
|
||||
[flake8]
|
||||
extend-ignore = E501
|
||||
@@ -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"
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id-class-value": false,
|
||||
"attr-lowercase": false
|
||||
}
|
||||
@@ -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/**"
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,2 @@
|
||||
no-inline-html: false
|
||||
line-length: false
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=E0401,C0301,R0903,R1710,C0114,R0915,W1514,W1203,I1101
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"rules": {
|
||||
"terminology": {
|
||||
"defaultTerms": true,
|
||||
"exclude": [
|
||||
"README",
|
||||
"VS Code",
|
||||
"website"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"bracketSameLine": true
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
Vendored
+23
@@ -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
|
||||
]
|
||||
}
|
||||
@@ -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/)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
[](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.
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`GoogleCloudPlatform/generative-ai`
|
||||
- 原始仓库:https://github.com/GoogleCloudPlatform/generative-ai
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+109
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
+88
@@ -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)
|
||||
}
|
||||
+107
@@ -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),
|
||||
}
|
||||
}
|
||||
+117
@@ -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)
|
||||
}
|
||||
}
|
||||
+473
@@ -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)
|
||||
}
|
||||
+168
@@ -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)
|
||||
}
|
||||
+10
@@ -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",
|
||||
)
|
||||
+1
@@ -0,0 +1 @@
|
||||
# Shared utilities for telemetry and tracing context propagation
|
||||
+67
@@ -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 {}
|
||||
+205
@@ -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)
|
||||
+682
@@ -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
|
||||
|
||||
+2982
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
+132
@@ -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."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+73
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+155
@@ -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"]
|
||||
+106
@@ -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
+19
@@ -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.
|
||||
+19
@@ -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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📖 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**.
|
||||
@@ -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"]
|
||||
@@ -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
Reference in New Issue
Block a user