From da4d3d883bae7146cb5aa3f46631a2c331a90d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Fri, 14 Aug 2026 15:51:58 +0200 Subject: [PATCH 1/9] docs: initial draft of sample code server --- .gitignore | 3 + pyproject.toml | 5 +- sample-code/Makefile | 2 + sample-code/README.md | 24 ++++++ sample-code/pyproject.toml | 18 +++++ sample-code/src/sample_code/__init__.py | 0 sample-code/src/sample_code/core.py | 45 +++++++++++ sample-code/src/sample_code/openai.py | 99 +++++++++++++++++++++++++ sample-code/src/sample_code/server.py | 41 ++++++++++ uv.lock | 70 +++++++++++++++++ 10 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 sample-code/Makefile create mode 100644 sample-code/README.md create mode 100644 sample-code/pyproject.toml create mode 100644 sample-code/src/sample_code/__init__.py create mode 100644 sample-code/src/sample_code/core.py create mode 100644 sample-code/src/sample_code/openai.py create mode 100644 sample-code/src/sample_code/server.py diff --git a/.gitignore b/.gitignore index e17a658..b963d03 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,9 @@ dmypy.json # VS Code .vscode/ +# pyright +pyrightconfig.json + # Certificates *.pem diff --git a/pyproject.toml b/pyproject.toml index 4aaa5f8..a43be84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,10 @@ sap-ai-sdk-core = { workspace = true } sap-ai-sdk-gen = { workspace = true } [tool.uv.workspace] -members = ["packages/*"] +members = [ + "packages/*", + "sample-code", +] [tool.pip-licenses] # Blue Oak Council Bronze+ permissive licenses (https://blueoakcouncil.org/list) diff --git a/sample-code/Makefile b/sample-code/Makefile new file mode 100644 index 0000000..c9fe416 --- /dev/null +++ b/sample-code/Makefile @@ -0,0 +1,2 @@ +server: + uv run uvicorn sample_code.server:app --app-dir src --env-file .env --reload diff --git a/sample-code/README.md b/sample-code/README.md new file mode 100644 index 0000000..423723d --- /dev/null +++ b/sample-code/README.md @@ -0,0 +1,24 @@ +# Sample Code - Work in Progress + +Sample code to demonstrate the usage of the SAP Cloud SDK for AI. + +## Local Deployment + +Create a .env file in the sample-code directory with the complete content of your AI core service key by adding the following lines: + +```bash +AICORE_CLIENT_ID="..." +AICORE_CLIENT_SECRET="..." +AICORE_AUTH_URL="..." +AICORE_BASE_URL="..." +``` + +The server can be started with +```bash +uv run uvicorn sample_code.server:app --app-dir src --env-file .env --reload +``` +or by running ```make```. + +## Usage + +TODO: overview diff --git a/sample-code/pyproject.toml b/sample-code/pyproject.toml new file mode 100644 index 0000000..ec0e1a3 --- /dev/null +++ b/sample-code/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "sample-code" +version = "0.1.0" +description = "Sample code for using the AI Core Python SDK" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.141.1", + "sap-ai-sdk-gen", + "uvicorn>=0.52.1", +] + +[build-system] +requires = ["uv_build>=0.12.1,<0.13.0"] +build-backend = "uv_build" + +[tool.uv.sources] +sap-ai-sdk-gen = { workspace = true, editable = true } diff --git a/sample-code/src/sample_code/__init__.py b/sample-code/src/sample_code/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sample-code/src/sample_code/core.py b/sample-code/src/sample_code/core.py new file mode 100644 index 0000000..af4f46c --- /dev/null +++ b/sample-code/src/sample_code/core.py @@ -0,0 +1,45 @@ +from typing import Annotated + +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from fastapi import Body + + +def get_configurations(): + client = AICoreV2Client.from_env() + return client.configuration.query() + + +def create_configuration(): + client = AICoreV2Client.from_env() + parameter_bindings = [ + ParameterBinding.from_dict({"key": "modelName", "value": "gpt-5.4-nano"}), + ParameterBinding.from_dict({"key": "modelVersion", "value": "latest"}), + ] + return client.configuration.create( + name="my-gpt-5.4-nano-config", + scenario_id="foundation-models", + executable_id="azure-openai", + parameter_bindings=parameter_bindings, + input_artifact_bindings=[], + ) + + +def get_deployments(): + client = AICoreV2Client.from_env() + return client.deployment.query() + + +def create_deployment(configuration_id: Annotated[str, Body(embed=True)]): + client = AICoreV2Client.from_env() + return client.deployment.create(configuration_id=configuration_id) + + +def get_scenarios(): + client = AICoreV2Client.from_env() + return client.scenario.query() + + +def get_models(): + client = AICoreV2Client.from_env() + return client.model.query() diff --git a/sample-code/src/sample_code/openai.py b/sample-code/src/sample_code/openai.py new file mode 100644 index 0000000..5c0e832 --- /dev/null +++ b/sample-code/src/sample_code/openai.py @@ -0,0 +1,99 @@ +from fastapi.responses import StreamingResponse +from gen_ai_hub.proxy.native.openai import chat, embeddings, responses +from pydantic import BaseModel + +# TODOs: +# - add async examples + + +def chat_completion(): + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Does Azure OpenAI support customer managed keys?"}, + { + "role": "assistant", + "content": "Yes, customer managed keys are supported by Azure OpenAI.", + }, + { + "role": "user", + "content": "Do other Azure Cognitive Services support this too?", + }, + ] + return chat.completions.create(model_name="gpt-5.4-nano", messages=messages) + + +class Person(BaseModel): + name: str + age: int + + +def chat_completion_structured(): + response = chat.completions.parse( + model_name="gpt-5.4-nano", + messages=[{"role": "user", "content": "Tell me about John Doe, aged 30."}], + response_format=Person, + ) + return response.choices[0].message.parsed + + +def chat_completion_stream(): + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Count from 1 to 10, one number per line."}, + ] + + def generate(): + stream = chat.completions.create( + model_name="gpt-5.4-nano", + messages=messages, + stream=True, + ) + for chunk in stream: + if chunk.choices: + content = chunk.choices[0].delta.content + if content: + yield content + + return StreamingResponse(generate(), media_type="text/event-stream") + + +def chat_completion_structured_stream(): + with chat.completions.with_streaming_response.parse( + model_name="gpt-5.4-nano", + messages=[{"role": "user", "content": "Tell me about John Doe, aged 30."}], + response_format=Person, + ) as stream: + response = stream.parse() + return response.choices[0].message.parsed + + +def responses_simple(): + return responses.create( + model="gpt-5.4-nano", + instructions="You are a helpful assistant.", + input="What is the capital of France?", + ) + + +def responses_structured(): + response = responses.parse( + model="gpt-5.4-nano", + input="Tell me about John Doe aged 30.", + text_format=Person, + ) + return response.output_parsed + + +def embedding(): + result = embeddings.create( + model_name="text-embedding-3-small", + input="The quick brown fox jumps over the lazy dog.", + ) + return { + "model": result.model, + "embedding": result.data[0].embedding, + "usage": { + "prompt_tokens": result.usage.prompt_tokens, + "total_tokens": result.usage.total_tokens, + }, + } diff --git a/sample-code/src/sample_code/server.py b/sample-code/src/sample_code/server.py new file mode 100644 index 0000000..47b696f --- /dev/null +++ b/sample-code/src/sample_code/server.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from sample_code import core, openai + +app = FastAPI(title="SAP AI Core Python SDK Sample Application") + + +# no specific error handling, simply return error message +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + return JSONResponse(status_code=500, content={"error": str(exc)}) + + +# NOTE: /docs contains an auto-generated overview of the routes + + +@app.get("/") +@app.get("/health") +async def health(): + return {"status": "ok"} + + +# AI Core (Configurations/Deployments) +app.get("/core/configurations")(core.get_configurations) +app.post("/core/configuration/create")(core.create_configuration) +app.get("/core/deployments")(core.get_deployments) +app.post("/core/deployment/create")(core.create_deployment) +app.get("/core/scenarios")(core.get_scenarios) +app.get("/core/models")(core.get_models) + +# OpenAI +app.get("/openai/chat-completion")(openai.chat_completion) +app.get("/openai/chat-completion-stream")(openai.chat_completion_stream) +app.get("/openai/chat-completion-structured")(openai.chat_completion_structured) +app.get("/openai/chat-completion-structured-stream")( + openai.chat_completion_structured_stream +) +app.get("/openai/responses")(openai.responses_simple) +app.get("/openai/responses-structured")(openai.responses_structured) +app.get("/openai/embedding")(openai.embedding) diff --git a/uv.lock b/uv.lock index d28b26a..2e9d2de 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ resolution-markers = [ [manifest] members = [ "ai-sdk-python", + "sample-code", "sap-ai-sdk-base", "sap-ai-sdk-core", "sap-ai-sdk-gen", @@ -266,6 +267,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + [[package]] name = "annotated-types" version = "0.8.0" @@ -941,6 +951,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + [[package]] name = "fastjsonschema" version = "2.22.1" @@ -4104,6 +4130,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, ] +[[package]] +name = "sample-code" +version = "0.1.0" +source = { editable = "sample-code" } +dependencies = [ + { name = "fastapi" }, + { name = "sap-ai-sdk-gen" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.141.1" }, + { name = "sap-ai-sdk-gen", editable = "packages/gen" }, + { name = "uvicorn", specifier = ">=0.52.1" }, +] + [[package]] name = "sap-ai-sdk-base" version = "3.4.1" @@ -4529,6 +4572,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + [[package]] name = "tabulate" version = "0.10.0" @@ -4858,6 +4914,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/8e/86ce2c03a1d9674530f6649e49067f7c69929600127077731de590d12132/uuid_utils-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310", size = 178681, upload-time = "2026-07-09T13:49:57.096Z" }, ] +[[package]] +name = "uvicorn" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, +] + [[package]] name = "wcwidth" version = "0.8.2" From a0784d3c821760aa3d8e3d3fb59713dead93b397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Wed, 19 Aug 2026 16:47:11 +0200 Subject: [PATCH 2/9] docs: rewrite server, add orchestration and further native clients --- sample-code/README.md | 23 +- sample-code/src/sample_code/amazon.py | 25 + sample-code/src/sample_code/core.py | 34 + sample-code/src/sample_code/google.py | 69 ++ sample-code/src/sample_code/openai.py | 75 +- sample-code/src/sample_code/orchestration.py | 854 +++++++++++++++++++ sample-code/src/sample_code/server.py | 38 +- 7 files changed, 1085 insertions(+), 33 deletions(-) create mode 100644 sample-code/src/sample_code/amazon.py create mode 100644 sample-code/src/sample_code/google.py create mode 100644 sample-code/src/sample_code/orchestration.py diff --git a/sample-code/README.md b/sample-code/README.md index 423723d..6b9ffe8 100644 --- a/sample-code/README.md +++ b/sample-code/README.md @@ -2,9 +2,22 @@ Sample code to demonstrate the usage of the SAP Cloud SDK for AI. +## Prerequisites + +Before running the application, ensure the following prerequisites are met: + +- Python installation (3.10 or higher) +- uv installation (0.12) +- Credentials for [SAP AI Core](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core) service configured. +- Deployments of the orchestration service as well as the following models in the resource group specified in the `.env` file below: + - `gpt-5.4-nano` + - `text-embedding-3-small` + - `anthropic--claude-4.6-sonnet` + - `gemini-3.5-flash` + ## Local Deployment -Create a .env file in the sample-code directory with the complete content of your AI core service key by adding the following lines: +Create a `.env` file in the sample-code directory with the complete content of your AI core service key by adding the following lines: ```bash AICORE_CLIENT_ID="..." @@ -13,12 +26,16 @@ AICORE_AUTH_URL="..." AICORE_BASE_URL="..." ``` -The server can be started with +Optionally, you can add the `AICORE_RESOURCE_GROUP` environment variable to specify a resource group different from the `default` one. + +The server can be started with + ```bash uv run uvicorn sample_code.server:app --app-dir src --env-file .env --reload ``` + or by running ```make```. ## Usage -TODO: overview +When the server is running, head to `http://localhost:8000/docs` to see all available endpoints. diff --git a/sample-code/src/sample_code/amazon.py b/sample-code/src/sample_code/amazon.py new file mode 100644 index 0000000..8d5e2cf --- /dev/null +++ b/sample-code/src/sample_code/amazon.py @@ -0,0 +1,25 @@ +from gen_ai_hub.proxy.native.amazon import Session + + +def converse(): + """ + Run chat example for Claude 4.6 Sonnet. + + Returns: + JSON object containing the model response as result. + """ + bedrock = Session().client(model_name="anthropic--claude-4.6-sonnet") + conversation = [ + { + "role": "user", + "content": [ + { + "text": "Describe the purpose of a 'Hello World' program in one sentence." + } + ], + } + ] + response = bedrock.converse( + messages=conversation, + ) + return {"result": response["output"]["message"]["content"][0]["text"]} diff --git a/sample-code/src/sample_code/core.py b/sample-code/src/sample_code/core.py index af4f46c..bf03c93 100644 --- a/sample-code/src/sample_code/core.py +++ b/sample-code/src/sample_code/core.py @@ -6,11 +6,22 @@ def get_configurations(): + """ + Get all configurations for the resource group specified in the .env file. + + Returns: + A dict containing the configurations in a ConfigurationQueryResponse object. + """ client = AICoreV2Client.from_env() return client.configuration.query() def create_configuration(): + """ + Create configuration for GPT-5.4-nano. + + The configuration is created for the resource group specified in the .env file. + """ client = AICoreV2Client.from_env() parameter_bindings = [ ParameterBinding.from_dict({"key": "modelName", "value": "gpt-5.4-nano"}), @@ -26,20 +37,43 @@ def create_configuration(): def get_deployments(): + """ + Get all deployments for the resource group specified in the .env file. + + Returns: + A dict containing the deployments in a DeploymentQueryResponse object. + """ client = AICoreV2Client.from_env() return client.deployment.query() def create_deployment(configuration_id: Annotated[str, Body(embed=True)]): + """ + Create deployment for the configuration_id in the request body. + + The deployment is created for the resource group specified in the .env file. + """ client = AICoreV2Client.from_env() return client.deployment.create(configuration_id=configuration_id) def get_scenarios(): + """ + Get all scenarios. + + Returns: + A dict containing the scenarios in a ScenarioQueryResponse object. + """ client = AICoreV2Client.from_env() return client.scenario.query() def get_models(): + """ + Get all available models. + + Returns: + A dict containing the models in a ModelQueryResponse object. + """ client = AICoreV2Client.from_env() return client.model.query() diff --git a/sample-code/src/sample_code/google.py b/sample-code/src/sample_code/google.py new file mode 100644 index 0000000..e991070 --- /dev/null +++ b/sample-code/src/sample_code/google.py @@ -0,0 +1,69 @@ +from fastapi.responses import StreamingResponse +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.proxy.native.google_genai import Client +from google.genai import types + + +def generate(): + """ + Run chat example for Gemini 3.5 Flash. + + Returns: + JSON object containing the model response as result. + """ + proxy_client = get_proxy_client("gen-ai-hub") + client = Client(proxy_client=proxy_client) + response = client.models.generate_content( + model="gemini-3.5-flash", contents="How many paws are there for a dog?" + ) + return {"result": response.candidates[0].content.parts[0].text} + + +def generate_stream(): + """ + Run chat example with streaming response for Gemini 3.5 Flash. + + Returns: + Streaming response emitting the produced text. + """ + proxy_client = get_proxy_client("gen-ai-hub") + + client = Client( + proxy_client=proxy_client, + ) + + def stream(): + stream = client.models.generate_content_stream( + model="gemini-3.5-flash", contents="Explain singularity in short terms." + ) + for chunk in stream: + if chunk.text: + yield chunk.text + + return StreamingResponse(stream(), media_type="text/event-stream") + + +def tool_call(): + """ + Run chat example including a tool call for Gemini 3.5 Flash. + + Returns: + JSON object containing the model response as result. + """ + + # addition tool to call + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + proxy_client = get_proxy_client("gen-ai-hub") + + client = Client( + proxy_client=proxy_client, + ) + response = client.models.generate_content( + model="gemini-3.5-flash", + contents="What is 769 + 348?", + config=types.GenerateContentConfig(tools=[add]), + ) + return {"result": response.candidates[0].content.parts[0].text} diff --git a/sample-code/src/sample_code/openai.py b/sample-code/src/sample_code/openai.py index 5c0e832..e6ad8e2 100644 --- a/sample-code/src/sample_code/openai.py +++ b/sample-code/src/sample_code/openai.py @@ -2,11 +2,14 @@ from gen_ai_hub.proxy.native.openai import chat, embeddings, responses from pydantic import BaseModel -# TODOs: -# - add async examples - def chat_completion(): + """ + Run chat example for GPT-5.4-nano with the ChatCompletions API. + + Returns: + JSON object containing the model response as result. + """ messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Does Azure OpenAI support customer managed keys?"}, @@ -19,15 +22,22 @@ def chat_completion(): "content": "Do other Azure Cognitive Services support this too?", }, ] - return chat.completions.create(model_name="gpt-5.4-nano", messages=messages) + response = chat.completions.create(model_name="gpt-5.4-nano", messages=messages) + return {"result": response.choices[0].message.content} -class Person(BaseModel): - name: str - age: int +def chat_completion_structured(): + """ + Run structured output (JSON) example for GPT-5.4-nano with the ChatCompletions API. + Returns: + JSON object response from the model. + """ + + class Person(BaseModel): + name: str + age: int -def chat_completion_structured(): response = chat.completions.parse( model_name="gpt-5.4-nano", messages=[{"role": "user", "content": "Tell me about John Doe, aged 30."}], @@ -37,6 +47,12 @@ def chat_completion_structured(): def chat_completion_stream(): + """ + Run chat example with streaming response for GPT-5.4-nano with the ChatCompletions API. + + Returns: + Streaming response emitting the produced text. + """ messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Count from 1 to 10, one number per line."}, @@ -57,25 +73,33 @@ def generate(): return StreamingResponse(generate(), media_type="text/event-stream") -def chat_completion_structured_stream(): - with chat.completions.with_streaming_response.parse( - model_name="gpt-5.4-nano", - messages=[{"role": "user", "content": "Tell me about John Doe, aged 30."}], - response_format=Person, - ) as stream: - response = stream.parse() - return response.choices[0].message.parsed - - def responses_simple(): - return responses.create( + """ + Run chat example for GPT-5.4-nano with the Responses API. + + Returns: + JSON object containing the model response as result. + """ + response = responses.create( model="gpt-5.4-nano", instructions="You are a helpful assistant.", input="What is the capital of France?", ) + return {"result": response.output[0].content[0].text} def responses_structured(): + """ + Run structured output (JSON) example for GPT-5.4-nano with the Responses API. + + Returns: + JSON object response from the model. + """ + + class Person(BaseModel): + name: str + age: int + response = responses.parse( model="gpt-5.4-nano", input="Tell me about John Doe aged 30.", @@ -85,15 +109,16 @@ def responses_structured(): def embedding(): + """ + Run embedding example. + + Returns: + JSON object containing the embedding. + """ result = embeddings.create( model_name="text-embedding-3-small", input="The quick brown fox jumps over the lazy dog.", ) return { - "model": result.model, - "embedding": result.data[0].embedding, - "usage": { - "prompt_tokens": result.usage.prompt_tokens, - "total_tokens": result.usage.total_tokens, - }, + "result": result.data[0].embedding, } diff --git a/sample-code/src/sample_code/orchestration.py b/sample-code/src/sample_code/orchestration.py new file mode 100644 index 0000000..a7b4b7a --- /dev/null +++ b/sample-code/src/sample_code/orchestration.py @@ -0,0 +1,854 @@ +from fastapi.responses import StreamingResponse +from gen_ai_hub.orchestration_v2 import ( + AzureContentSafetyInput, + AzureContentSafetyInputFilterConfig, + AzureContentSafetyOutput, + AzureContentSafetyOutputFilterConfig, + AzureThreshold, + DPICustomEntity, + DPIMethodConstant, + DPIStandardEntity, + EmbeddingsInput, + EmbeddingsModelConfig, + EmbeddingsModelDetails, + EmbeddingsModuleConfigs, + EmbeddingsOrchestrationConfig, + FilteringModuleConfig, + FunctionObject, + FunctionTool, + GlobalStreamOptions, + ImageItem, + InputFiltering, + InputTranslationConfig, + JSONResponseSchema, + LlamaGuard38bFilter, + LlamaGuard38bFilterConfig, + LLMModelDetails, + MaskingMethod, + MaskingModuleConfig, + MaskingProviderConfig, + ModuleConfig, + OrchestrationConfig, + OrchestrationError, + OrchestrationService, + OutputFiltering, + OutputTranslationConfig, + ProfileEntity, + PromptTemplatingModuleConfig, + ResponseFormatJsonSchema, + SAPDocumentTranslationInput, + SAPDocumentTranslationOutput, + SystemMessage, + Template, + ToolChatMessage, + TranslationModuleConfig, + UserMessage, + function_tool, +) + + +def completion(): + """ + Run chat example through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What is the longest river on planet earth?" + ) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +async def completion_async(): + """ + Run async chat example through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What is the longest river on planet earth?" + ) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + service = OrchestrationService(config=config) + result = await service.arun() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def completion_stream(): + """ + Run chat example with a streaming response through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What is the longest river on planet earth?" + ) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ), + stream=GlobalStreamOptions(enabled=True), + ) + service = OrchestrationService(config=config) + + def generate(): + stream = service.stream() + for chunk in stream: + if chunk.final_result: + content = chunk.final_result.choices[0].delta.content + if content: + yield content + service.close_http_connection() + + return StreamingResponse(generate(), media_type="text/event-stream") + + +def completion_json(): + """ + Run chat example with structured output (JSON) through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + json_schema = { + "title": "Person", + "type": "object", + "properties": { + "firstName": {"type": "string", "description": "The person's first name."}, + "lastName": {"type": "string", "description": "The person's last name."}, + }, + } + + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage(content="Format the response as json."), + UserMessage(content="Who was the first person on the moon?"), + ], + # setting ResponseFormatJsonObject() enables JSON responses without a fixed schema + response_format=ResponseFormatJsonSchema( + json_schema=JSONResponseSchema( + name="person", + description="person mapping", + schema=json_schema, + ) + ), + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def completion_template(): + """ + Run chat example with a template including placeholders through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + # add placeholder by wrapping it with {{?...}} + UserMessage(content="What is the capital of {{?country}}?") + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + service = OrchestrationService(config=config) + # provide placeholder values + result = service.run(placeholder_values={"country": "Denmark"}) + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def completion_with_fallback(): + """ + Run chat example with fallback configurations through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + config = OrchestrationConfig( + modules=[ + # Trigger fallback with non-orchestration model + ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What is the longest river on planet earth?" + ) + ] + ), + model=LLMModelDetails(name="sap-rpt-1-small"), + ) + ), + # Second configuration will succeed + ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What is the longest river on planet earth?" + ) + ] + ), + model=LLMModelDetails(name="anthropic--claude-4.5-haiku"), + ) + ), + ] + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def completion_abap(): + """ + Run chat example with SAP ABAP through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="Explain the concept of internal tables in ABAP" + ) + ] + ), + model=LLMModelDetails(name="sap-abap-1"), + ) + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def message_history(): + """ + Run chat example with message history through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + # the service can also be started without providing a default config + # in this case, each call to service.run has to pass a config to use + service = OrchestrationService() + first_config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[UserMessage(content="What is the capital of France?")] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + + first_response = service.run(config=first_config) + # first_response.intermediate_results.templating contains the history including the one that was passed in (here this part is still empty) + history = first_response.intermediate_results.templating or [] + history.append(first_response.final_result.choices[0].message) + + second_config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[UserMessage(content="What is the typical food there?")] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + second_response = service.run(config=second_config, history=history) + service.close_http_connection() + return {"result": second_response.final_result.choices[0].message.content} + + +def completion_image(): + """ + Run multimodal example with image input through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + # First option: load image from a standard, publicly accessible url + image = ImageItem(url="https://picsum.photos/id/1/200/300") + # Second option: pass the image content as base64-encoded data url + # with the format "data:[][;base64]," + # image = ImageItem( + # url="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAE0lEQVR4nGP8z4APMOGVZRip0gBBLAETee26JgAAAABJRU5ErkJggg==" + # ) + # Third option: load the image from a local file path + # try: + # image = ImageItem.from_file("path/to/your/local/image.jpeg") + # except FileNotFoundError: + # print("Error: The specified image file was not found.") + # except Exception as e: + # print(f"An error occurred while loading the image: {e}") + multimodal_content = [image, "What objects are prominent in this image?"] + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + # add placeholder by wrapping it with {{?...}} + UserMessage(content=multimodal_content) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def input_filtering(): + """ + Run input filtering example through the Orchestration Service API. + + Returns: + JSON object containing a message confirming successful filtering. + Raises: + RuntimeError: Raised if the filtering is unsuccesful. + """ + content_filter_config = FilteringModuleConfig( + input=InputFiltering( + filters=[ + AzureContentSafetyInputFilterConfig( + # only safe content allowed for hate and violence + config=AzureContentSafetyInput( + hate=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + ) + ), + # category 'privacy' enabled + LlamaGuard38bFilterConfig(config=LlamaGuard38bFilter(privacy=True)), + ] + ) + ) + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + # should be filtered by Llama Guard + content="My social insurance number is ABC123456789." + ) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ), + filtering=content_filter_config, + ) + ) + service = OrchestrationService(config=config) + try: + service.run() + raise RuntimeError("Input was not filtered as expected") + except OrchestrationError as e: + if e.code == 400: + service.close_http_connection() + return {"result": "Input was filtered as expected."} + else: + raise + + +def output_filtering(): + """ + Run output filtering example through the Orchestration Service API. + + Returns: + JSON object containing a message confirming successful filtering. + Raises: + RuntimeError: Raised if the filtering is unsuccesful. + """ + content_filter_config = FilteringModuleConfig( + output=OutputFiltering( + filters=[ + AzureContentSafetyOutputFilterConfig( + # only safe content allowed for hate and violence + config=AzureContentSafetyOutput( + hate=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + ) + ), + # category 'privacy' enabled + LlamaGuard38bFilterConfig(config=LlamaGuard38bFilter(privacy=True)), + ] + ) + ) + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + # should be filtered by Azure content filter + content="Reparaphrase the sentence in 30 ways with strong feelings: 'I hate you!'." + ) + ] + ), + model=LLMModelDetails(name="anthropic--claude-4.5-haiku"), + ), + filtering=content_filter_config, + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + # should be filtered by the Azure content filter, hence content should be empty + if result.final_result.choices[0].message.content: + raise RuntimeError("Output was not filtered as expected") + else: + return {"result": "Output was filtered as expected"} + + +def completion_masking(): + """ + Run masked (pseudonymized) chat example through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + data_masking_config = MaskingModuleConfig( + providers=[ + MaskingProviderConfig( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[ + DPIStandardEntity(type=ProfileEntity.ADDRESS), + DPIStandardEntity(type=ProfileEntity.EMAIL), + DPIStandardEntity(type=ProfileEntity.PHONE), + DPIStandardEntity(type=ProfileEntity.PERSON), + DPICustomEntity( + regex="[0-9]{4}[-/][0-9]{2}[-/][0-9]{2}", + replacement_strategy=DPIMethodConstant(value="MASKED_DATE"), + ), + ], + ) + ] + ) + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="Generate HTML that shows the contact info for Jane Doe, born on 1975-03-05, living at 10 Downing Street, London UK with email 'jane.doe@mailprovider.com' and phone number +4902044123221." + ) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ), + masking=data_masking_config, + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def translation(): + """ + Run chat example with prompt and output translation through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + translation_config = TranslationModuleConfig( + input=SAPDocumentTranslationInput( + config=InputTranslationConfig( + source_language="en-US", target_language="de-DE" + ) + ), + output=SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="de-DE", target_language="fr-FR" + ) + ), + ) + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What is the longest river on planet earth?" + ) + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ), + translation=translation_config, + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return {"result": result.final_result.choices[0].message.content} + + +def sonar_with_citations(): + """ + Run chat example with citations (Sonar model) through the Orchestration Service API. + + Returns: + JSON object containing the model response (text and citations) as result. + """ + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="What are the latest developments in quantum computing?" + ) + ] + ), + model=LLMModelDetails(name="sonar"), + ) + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return { + "result": { + "text": result.final_result.choices[0].message.content, + "citations": result.final_result.citations, + } + } + + +def embedding(): + """ + Run embedding example through the Orchestration Service API. + + Returns: + JSON object containing the embedding as result. + """ + embdding_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ) + ) + ) + + service = OrchestrationService() + response = service.embed( + config=embdding_config, input=EmbeddingsInput(text="Hello World!") + ) + return {"result": response.final_result.data[0].embedding} + + +def embedding_batched(): + """ + Run masked (anonymized )embedding example through the Orchestration Service API. + + Returns: + JSON object containing the embedding as result. + """ + embdding_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ) + ) + ) + + input_list = ["Hello World!", "This is your captain speaking"] + + service = OrchestrationService() + response = service.embed( + config=embdding_config, input=EmbeddingsInput(text=input_list) + ) + return {"result": response.final_result.data} + + +def embedding_masked(): + embdding_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ), + masking=MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[ + DPIStandardEntity(type=ProfileEntity.PERSON), + DPIStandardEntity(type=ProfileEntity.EMAIL), + DPIStandardEntity(type=ProfileEntity.PHONE), + ], + ) + ] + ), + ) + ) + + service = OrchestrationService() + response = service.embed( + config=embdding_config, + input=EmbeddingsInput( + text="Contact John Smith at john.smith@example.com or call 555-123-4567." + ), + ) + return {"result": response.final_result.data[0].embedding} + + +def tool_call_decorator(): + """ + Run chat example with tool calls using the `function_tool` decorator through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + + @function_tool + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + tools = [add] + + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage( + content="You are a helpful AI that performs the addition of two numbers." + ), + UserMessage(content="What is 279 + 929?"), + ], + tools=tools, + ), + model=LLMModelDetails(name="gpt-4o"), + ) + ) + ) + + service = OrchestrationService() + result = service.run(config=config) + tool_calls = result.final_result.choices[0].message.tool_calls + if tool_calls is None: + raise RuntimeError("Unexpectedly no tool calls in response") + + history = list(result.intermediate_results.templating or []) + history.append(result.final_result.choices[0].message) + for tool_call in tool_calls: + if tool_call.function.name != "add": + raise RuntimeError( + f"Unexpectedly called '{tool_call.function.name}' instead of 'add'" + ) + result = add.execute(**tool_call.function.parse_arguments()) + tool_message = ToolChatMessage(content=str(result), tool_call_id=tool_call.id) + history.append(tool_message) + + result = service.run(config=config, history=history) + return {"result": result.final_result.choices[0].message.content} + + +def tool_call_function_tool(): + """ + Run chat example with tool calls using the `FunctionTool` class through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + add_tool = FunctionTool( + function=FunctionObject( + name="add", + description="Add two numbers.", + parameters={ + "type": "object", + "properties": { + "a": { + "type": "number", + "description": "First operand of the addition function", + }, + "b": { + "type": "number", + "description": "Second operand of the addition function", + }, + }, + "required": ["a", "b"], + "additionalProperties": False, + }, + strict=True, + function=add, + ) + ) + + tools = [add_tool] + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage( + content="You are a helpful AI that performs the addition of two numbers." + ), + UserMessage(content="What is 279 + 929?"), + ], + tools=tools, + ), + model=LLMModelDetails(name="gpt-4o"), + ) + ) + ) + + service = OrchestrationService() + result = service.run(config=config) + tool_calls = result.final_result.choices[0].message.tool_calls + if tool_calls is None: + raise RuntimeError("Unexpectedly no tool calls in response") + + history = list(result.intermediate_results.templating or []) + history.append(result.final_result.choices[0].message) + for tool_call in tool_calls: + if tool_call.function.name != "add": + raise RuntimeError( + f"Unexpectedly called '{tool_call.function.name}' instead of 'add'" + ) + result = add_tool.execute(**tool_call.function.parse_arguments()) + tool_message = ToolChatMessage(content=str(result), tool_call_id=tool_call.id) + history.append(tool_message) + + result = service.run(config=config, history=history) + return {"result": result.final_result.choices[0].message.content} + + +def tool_call_json(): + """ + Run chat example with tool calls using a JSON schema dictionary through the Orchestration Service API. + + Returns: + JSON object containing the model response as result. + """ + # this is helpful if the tool call doesn't map to a Python function + tools = [ + { + "type": "function", + "function": { + "name": "add", + "description": "Add two numbers.", + "parameters": { + "type": "object", + "properties": { + "a": { + "type": "number", + "description": "First operand of the addition function", + }, + "b": { + "type": "number", + "description": "Second operand of the addition function", + }, + }, + "required": ["a", "b"], + "additionalProperties": False, + }, + "strict": True, + }, + } + ] + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage( + content="You are a helpful AI that performs the addition of two numbers." + ), + UserMessage(content="What is 279 + 929?"), + ], + tools=tools, + ), + model=LLMModelDetails(name="gpt-4o"), + ) + ) + ) + + service = OrchestrationService() + result = service.run(config=config) + tool_calls = result.final_result.choices[0].message.tool_calls + if tool_calls is None: + raise RuntimeError("Unexpectedly no tool calls in response") + + history = list(result.intermediate_results.templating or []) + history.append(result.final_result.choices[0].message) + for tool_call in tool_calls: + if tool_call.function.name != "add": + raise RuntimeError( + f"Unexpectedly called '{tool_call.function.name}' instead of 'add'" + ) + result = sum(tool_call.function.parse_arguments().values()) + tool_message = ToolChatMessage(content=str(result), tool_call_id=tool_call.id) + history.append(tool_message) + + result = service.run(config=config, history=history) + return {"result": result.final_result.choices[0].message.content} diff --git a/sample-code/src/sample_code/server.py b/sample-code/src/sample_code/server.py index 47b696f..f898387 100644 --- a/sample-code/src/sample_code/server.py +++ b/sample-code/src/sample_code/server.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -from sample_code import core, openai +from sample_code import amazon, core, google, openai, orchestration app = FastAPI(title="SAP AI Core Python SDK Sample Application") @@ -29,13 +29,41 @@ async def health(): app.get("/core/scenarios")(core.get_scenarios) app.get("/core/models")(core.get_models) -# OpenAI +# Azure/OpenAI app.get("/openai/chat-completion")(openai.chat_completion) app.get("/openai/chat-completion-stream")(openai.chat_completion_stream) app.get("/openai/chat-completion-structured")(openai.chat_completion_structured) -app.get("/openai/chat-completion-structured-stream")( - openai.chat_completion_structured_stream -) app.get("/openai/responses")(openai.responses_simple) app.get("/openai/responses-structured")(openai.responses_structured) app.get("/openai/embedding")(openai.embedding) + +# Google +app.get("/google/generate")(google.generate) +app.get("/google/generate-stream")(google.generate_stream) +app.get("/google/tool-call")(google.tool_call) + +# Amazon/Anthropic +app.get("/amazon/converse")(amazon.converse) + +# Orchestration +app.get("/orchestration/completion")(orchestration.completion) +app.get("/orchestration/completion-stream")(orchestration.completion_stream) +app.get("/orchestration/completion-template")(orchestration.completion_template) +app.get("/orchestration/completion-json")(orchestration.completion_json) +app.get("/orchestration/completion-with-fallback")( + orchestration.completion_with_fallback +) +app.get("/orchestration/completion-abap")(orchestration.completion_abap) +app.get("/orchestration/message-history")(orchestration.message_history) +app.get("/orchestration/completion-image")(orchestration.completion_image) +app.get("/orchestration/input-filtering")(orchestration.input_filtering) +app.get("/orchestration/output-filtering")(orchestration.output_filtering) +app.get("/orchestration/completion-masking")(orchestration.completion_masking) +app.get("/orchestration/translation")(orchestration.translation) +app.get("/orchestration/citations")(orchestration.sonar_with_citations) +app.get("/orchestration/embedding")(orchestration.embedding) +app.get("/orchestration/embedding-batched")(orchestration.embedding_batched) +app.get("/orchestration/embedding-masked")(orchestration.embedding_masked) +app.get("/orchestration/tool_call_decorator")(orchestration.tool_call_decorator) +app.get("/orchestration/tool_call_function_tool")(orchestration.tool_call_function_tool) +app.get("/orchestration/tool_call_json")(orchestration.tool_call_json) From a3e2306485d21c1bd5ae4392fc26284e90e20d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Tue, 25 Aug 2026 13:55:41 +0200 Subject: [PATCH 3/9] docs: polish code --- sample-code/src/sample_code/core.py | 1 + sample-code/src/sample_code/google.py | 2 +- sample-code/src/sample_code/openai.py | 2 +- sample-code/src/sample_code/orchestration.py | 39 +++++++++++++------- sample-code/src/sample_code/server.py | 7 ++-- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/sample-code/src/sample_code/core.py b/sample-code/src/sample_code/core.py index bf03c93..e92f1af 100644 --- a/sample-code/src/sample_code/core.py +++ b/sample-code/src/sample_code/core.py @@ -23,6 +23,7 @@ def create_configuration(): The configuration is created for the resource group specified in the .env file. """ client = AICoreV2Client.from_env() + # for illustrative purposes the example config is hardcoded parameter_bindings = [ ParameterBinding.from_dict({"key": "modelName", "value": "gpt-5.4-nano"}), ParameterBinding.from_dict({"key": "modelVersion", "value": "latest"}), diff --git a/sample-code/src/sample_code/google.py b/sample-code/src/sample_code/google.py index e991070..bb02697 100644 --- a/sample-code/src/sample_code/google.py +++ b/sample-code/src/sample_code/google.py @@ -40,7 +40,7 @@ def stream(): if chunk.text: yield chunk.text - return StreamingResponse(stream(), media_type="text/event-stream") + return StreamingResponse(stream(), media_type="text/plain") def tool_call(): diff --git a/sample-code/src/sample_code/openai.py b/sample-code/src/sample_code/openai.py index e6ad8e2..e0b195e 100644 --- a/sample-code/src/sample_code/openai.py +++ b/sample-code/src/sample_code/openai.py @@ -70,7 +70,7 @@ def generate(): if content: yield content - return StreamingResponse(generate(), media_type="text/event-stream") + return StreamingResponse(generate(), media_type="text/plain") def responses_simple(): diff --git a/sample-code/src/sample_code/orchestration.py b/sample-code/src/sample_code/orchestration.py index a7b4b7a..7088744 100644 --- a/sample-code/src/sample_code/orchestration.py +++ b/sample-code/src/sample_code/orchestration.py @@ -97,7 +97,7 @@ async def completion_async(): ) service = OrchestrationService(config=config) result = await service.arun() - service.close_http_connection() + await service.aclose_http_connection() return {"result": result.final_result.choices[0].message.content} @@ -134,7 +134,7 @@ def generate(): yield content service.close_http_connection() - return StreamingResponse(generate(), media_type="text/event-stream") + return StreamingResponse(generate(), media_type="text/plain") def completion_json(): @@ -299,7 +299,7 @@ def message_history(): ) first_response = service.run(config=first_config) - # first_response.intermediate_results.templating contains the history including the one that was passed in (here this part is still empty) + # first_response.intermediate_results.templating contains the history history = first_response.intermediate_results.templating or [] history.append(first_response.final_result.choices[0].message) @@ -405,10 +405,11 @@ def input_filtering(): raise RuntimeError("Input was not filtered as expected") except OrchestrationError as e: if e.code == 400: - service.close_http_connection() return {"result": "Input was filtered as expected."} else: raise + finally: + service.close_http_connection() def output_filtering(): @@ -585,7 +586,7 @@ def embedding(): Returns: JSON object containing the embedding as result. """ - embdding_config = EmbeddingsOrchestrationConfig( + embedding_config = EmbeddingsOrchestrationConfig( modules=EmbeddingsModuleConfigs( embeddings=EmbeddingsModelConfig( model=EmbeddingsModelDetails(name="text-embedding-3-small") @@ -595,19 +596,20 @@ def embedding(): service = OrchestrationService() response = service.embed( - config=embdding_config, input=EmbeddingsInput(text="Hello World!") + config=embedding_config, input=EmbeddingsInput(text="Hello World!") ) + service.close_http_connection() return {"result": response.final_result.data[0].embedding} def embedding_batched(): """ - Run masked (anonymized )embedding example through the Orchestration Service API. + Run batched embedding example through the Orchestration Service API. Returns: JSON object containing the embedding as result. """ - embdding_config = EmbeddingsOrchestrationConfig( + embedding_config = EmbeddingsOrchestrationConfig( modules=EmbeddingsModuleConfigs( embeddings=EmbeddingsModelConfig( model=EmbeddingsModelDetails(name="text-embedding-3-small") @@ -619,19 +621,26 @@ def embedding_batched(): service = OrchestrationService() response = service.embed( - config=embdding_config, input=EmbeddingsInput(text=input_list) + config=embedding_config, input=EmbeddingsInput(text=input_list) ) + service.close_http_connection() return {"result": response.final_result.data} def embedding_masked(): - embdding_config = EmbeddingsOrchestrationConfig( + """ + Run masked (anonymized )embedding example through the Orchestration Service API. + + Returns: + JSON object containing the embedding as result. + """ + embedding_config = EmbeddingsOrchestrationConfig( modules=EmbeddingsModuleConfigs( embeddings=EmbeddingsModelConfig( model=EmbeddingsModelDetails(name="text-embedding-3-small") ), masking=MaskingModuleConfig( - masking_providers=[ + providers=[ MaskingProviderConfig( method=MaskingMethod.ANONYMIZATION, entities=[ @@ -647,11 +656,12 @@ def embedding_masked(): service = OrchestrationService() response = service.embed( - config=embdding_config, + config=embedding_config, input=EmbeddingsInput( text="Contact John Smith at john.smith@example.com or call 555-123-4567." ), ) + service.close_http_connection() return {"result": response.final_result.data[0].embedding} @@ -690,7 +700,7 @@ def add(a: int, b: int) -> int: service = OrchestrationService() result = service.run(config=config) tool_calls = result.final_result.choices[0].message.tool_calls - if tool_calls is None: + if not tool_calls: raise RuntimeError("Unexpectedly no tool calls in response") history = list(result.intermediate_results.templating or []) @@ -705,6 +715,7 @@ def add(a: int, b: int) -> int: history.append(tool_message) result = service.run(config=config, history=history) + service.close_http_connection() return {"result": result.final_result.choices[0].message.content} @@ -780,6 +791,7 @@ def add(a: int, b: int) -> int: history.append(tool_message) result = service.run(config=config, history=history) + service.close_http_connection() return {"result": result.final_result.choices[0].message.content} @@ -851,4 +863,5 @@ def tool_call_json(): history.append(tool_message) result = service.run(config=config, history=history) + service.close_http_connection() return {"result": result.final_result.choices[0].message.content} diff --git a/sample-code/src/sample_code/server.py b/sample-code/src/sample_code/server.py index f898387..4e011e3 100644 --- a/sample-code/src/sample_code/server.py +++ b/sample-code/src/sample_code/server.py @@ -47,6 +47,7 @@ async def health(): # Orchestration app.get("/orchestration/completion")(orchestration.completion) +app.get("/orchestration/completion-async")(orchestration.completion_async) app.get("/orchestration/completion-stream")(orchestration.completion_stream) app.get("/orchestration/completion-template")(orchestration.completion_template) app.get("/orchestration/completion-json")(orchestration.completion_json) @@ -64,6 +65,6 @@ async def health(): app.get("/orchestration/embedding")(orchestration.embedding) app.get("/orchestration/embedding-batched")(orchestration.embedding_batched) app.get("/orchestration/embedding-masked")(orchestration.embedding_masked) -app.get("/orchestration/tool_call_decorator")(orchestration.tool_call_decorator) -app.get("/orchestration/tool_call_function_tool")(orchestration.tool_call_function_tool) -app.get("/orchestration/tool_call_json")(orchestration.tool_call_json) +app.get("/orchestration/tool-call-decorator")(orchestration.tool_call_decorator) +app.get("/orchestration/tool-call-function-tool")(orchestration.tool_call_function_tool) +app.get("/orchestration/tool-call-json")(orchestration.tool_call_json) From 9e7427e7a4bdf3d4341b626e7f7cd36da535f860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Tue, 25 Aug 2026 15:03:20 +0200 Subject: [PATCH 4/9] check in pyrightconfig.json to link to the other packages --- .gitignore | 3 --- sample-code/pyrightconfig.json | 9 +++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 sample-code/pyrightconfig.json diff --git a/.gitignore b/.gitignore index b963d03..e17a658 100644 --- a/.gitignore +++ b/.gitignore @@ -133,9 +133,6 @@ dmypy.json # VS Code .vscode/ -# pyright -pyrightconfig.json - # Certificates *.pem diff --git a/sample-code/pyrightconfig.json b/sample-code/pyrightconfig.json new file mode 100644 index 0000000..bec92e0 --- /dev/null +++ b/sample-code/pyrightconfig.json @@ -0,0 +1,9 @@ +{ + "venvPath": "..", + "venv": ".venv", + "extraPaths": [ + "../packages/base", + "../packages/core", + "../packages/gen" + ] +} From 2bcfe32413cb366db3f20928e2bb3bb1985a9b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Tue, 25 Aug 2026 15:03:52 +0200 Subject: [PATCH 5/9] add async openai examples --- sample-code/src/sample_code/openai.py | 47 ++++++++++++++++++++++++++- sample-code/src/sample_code/server.py | 2 ++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/sample-code/src/sample_code/openai.py b/sample-code/src/sample_code/openai.py index e0b195e..934acf9 100644 --- a/sample-code/src/sample_code/openai.py +++ b/sample-code/src/sample_code/openai.py @@ -1,5 +1,6 @@ from fastapi.responses import StreamingResponse -from gen_ai_hub.proxy.native.openai import chat, embeddings, responses +from gen_ai_hub.proxy.native.openai import AsyncOpenAI, chat, embeddings, responses +from openai.types.responses import Response from pydantic import BaseModel @@ -26,6 +27,33 @@ def chat_completion(): return {"result": response.choices[0].message.content} +async def chat_completion_async(): + """ + Run async chat example for GPT-5.4-nano with the ChatCompletions API. + + Returns: + JSON object containing the model response as result. + """ + client = AsyncOpenAI() + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Does Azure OpenAI support customer managed keys?"}, + { + "role": "assistant", + "content": "Yes, customer managed keys are supported by Azure OpenAI.", + }, + { + "role": "user", + "content": "Do other Azure Cognitive Services support this too?", + }, + ] + response = await client.chat.completions.create( + model_name="gpt-5.4-nano", messages=messages + ) + await client.close() + return {"result": response.choices[0].message.content} + + def chat_completion_structured(): """ Run structured output (JSON) example for GPT-5.4-nano with the ChatCompletions API. @@ -88,6 +116,23 @@ def responses_simple(): return {"result": response.output[0].content[0].text} +async def responses_simple_async(): + """ + Run async chat example for GPT-5.4-nano with the Responses API. + + Returns: + JSON object containing the model response as result. + """ + client = AsyncOpenAI() + response: Response = await client.responses.create( # type: ignore[assignment] + model="gpt-5.4-nano", + instructions="You are a helpful assistant.", + input="What is the capital of France?", + ) + await client.close() + return {"result": response.output[0].content[0].text} # type: ignore[union-attr] + + def responses_structured(): """ Run structured output (JSON) example for GPT-5.4-nano with the Responses API. diff --git a/sample-code/src/sample_code/server.py b/sample-code/src/sample_code/server.py index 4e011e3..931da8a 100644 --- a/sample-code/src/sample_code/server.py +++ b/sample-code/src/sample_code/server.py @@ -33,8 +33,10 @@ async def health(): app.get("/openai/chat-completion")(openai.chat_completion) app.get("/openai/chat-completion-stream")(openai.chat_completion_stream) app.get("/openai/chat-completion-structured")(openai.chat_completion_structured) +app.get("/openai/chat-completion-async")(openai.chat_completion_async) app.get("/openai/responses")(openai.responses_simple) app.get("/openai/responses-structured")(openai.responses_structured) +app.get("/openai/responses-async")(openai.responses_simple_async) app.get("/openai/embedding")(openai.embedding) # Google From 79cab8ca32c5171b8a478cc7b62584e7f6cff599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Wed, 26 Aug 2026 08:31:50 +0200 Subject: [PATCH 6/9] add license file to sample_code folder --- sample-code/LICENSE | 201 +++++++++++++++++++++++++++++++++++++ sample-code/pyproject.toml | 2 + 2 files changed, 203 insertions(+) create mode 100644 sample-code/LICENSE diff --git a/sample-code/LICENSE b/sample-code/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/sample-code/LICENSE @@ -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. diff --git a/sample-code/pyproject.toml b/sample-code/pyproject.toml index ec0e1a3..9eeac18 100644 --- a/sample-code/pyproject.toml +++ b/sample-code/pyproject.toml @@ -3,6 +3,8 @@ name = "sample-code" version = "0.1.0" description = "Sample code for using the AI Core Python SDK" readme = "README.md" +license = "Apache-2.0" +license-files = ["LICENSE"] requires-python = ">=3.10" dependencies = [ "fastapi>=0.141.1", From e7fb91cf3d5c2686f9a5eccb7783892bdf721955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Wed, 26 Aug 2026 09:49:15 +0200 Subject: [PATCH 7/9] fix: remove not fully supported native OpenAI async examples --- sample-code/src/sample_code/openai.py | 45 +-------------------------- sample-code/src/sample_code/server.py | 2 -- 2 files changed, 1 insertion(+), 46 deletions(-) diff --git a/sample-code/src/sample_code/openai.py b/sample-code/src/sample_code/openai.py index 934acf9..8560853 100644 --- a/sample-code/src/sample_code/openai.py +++ b/sample-code/src/sample_code/openai.py @@ -1,4 +1,5 @@ from fastapi.responses import StreamingResponse +from gen_ai_hub.proxy.core import get_proxy_client from gen_ai_hub.proxy.native.openai import AsyncOpenAI, chat, embeddings, responses from openai.types.responses import Response from pydantic import BaseModel @@ -27,33 +28,6 @@ def chat_completion(): return {"result": response.choices[0].message.content} -async def chat_completion_async(): - """ - Run async chat example for GPT-5.4-nano with the ChatCompletions API. - - Returns: - JSON object containing the model response as result. - """ - client = AsyncOpenAI() - messages = [ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Does Azure OpenAI support customer managed keys?"}, - { - "role": "assistant", - "content": "Yes, customer managed keys are supported by Azure OpenAI.", - }, - { - "role": "user", - "content": "Do other Azure Cognitive Services support this too?", - }, - ] - response = await client.chat.completions.create( - model_name="gpt-5.4-nano", messages=messages - ) - await client.close() - return {"result": response.choices[0].message.content} - - def chat_completion_structured(): """ Run structured output (JSON) example for GPT-5.4-nano with the ChatCompletions API. @@ -116,23 +90,6 @@ def responses_simple(): return {"result": response.output[0].content[0].text} -async def responses_simple_async(): - """ - Run async chat example for GPT-5.4-nano with the Responses API. - - Returns: - JSON object containing the model response as result. - """ - client = AsyncOpenAI() - response: Response = await client.responses.create( # type: ignore[assignment] - model="gpt-5.4-nano", - instructions="You are a helpful assistant.", - input="What is the capital of France?", - ) - await client.close() - return {"result": response.output[0].content[0].text} # type: ignore[union-attr] - - def responses_structured(): """ Run structured output (JSON) example for GPT-5.4-nano with the Responses API. diff --git a/sample-code/src/sample_code/server.py b/sample-code/src/sample_code/server.py index 931da8a..4e011e3 100644 --- a/sample-code/src/sample_code/server.py +++ b/sample-code/src/sample_code/server.py @@ -33,10 +33,8 @@ async def health(): app.get("/openai/chat-completion")(openai.chat_completion) app.get("/openai/chat-completion-stream")(openai.chat_completion_stream) app.get("/openai/chat-completion-structured")(openai.chat_completion_structured) -app.get("/openai/chat-completion-async")(openai.chat_completion_async) app.get("/openai/responses")(openai.responses_simple) app.get("/openai/responses-structured")(openai.responses_structured) -app.get("/openai/responses-async")(openai.responses_simple_async) app.get("/openai/embedding")(openai.embedding) # Google From 3aceb639ebd6d1b8e54f597fefb9b321af7f0eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Thu, 27 Aug 2026 14:33:28 +0200 Subject: [PATCH 8/9] switch to flat layout --- sample-code/pyproject.toml | 3 +++ sample-code/{src => }/sample_code/__init__.py | 0 sample-code/{src => }/sample_code/amazon.py | 0 sample-code/{src => }/sample_code/core.py | 0 sample-code/{src => }/sample_code/google.py | 0 sample-code/{src => }/sample_code/openai.py | 0 sample-code/{src => }/sample_code/orchestration.py | 0 sample-code/{src => }/sample_code/server.py | 0 8 files changed, 3 insertions(+) rename sample-code/{src => }/sample_code/__init__.py (100%) rename sample-code/{src => }/sample_code/amazon.py (100%) rename sample-code/{src => }/sample_code/core.py (100%) rename sample-code/{src => }/sample_code/google.py (100%) rename sample-code/{src => }/sample_code/openai.py (100%) rename sample-code/{src => }/sample_code/orchestration.py (100%) rename sample-code/{src => }/sample_code/server.py (100%) diff --git a/sample-code/pyproject.toml b/sample-code/pyproject.toml index 9eeac18..9d1d37f 100644 --- a/sample-code/pyproject.toml +++ b/sample-code/pyproject.toml @@ -16,5 +16,8 @@ dependencies = [ requires = ["uv_build>=0.12.1,<0.13.0"] build-backend = "uv_build" +[tool.uv.build-backend] +module-root = "" + [tool.uv.sources] sap-ai-sdk-gen = { workspace = true, editable = true } diff --git a/sample-code/src/sample_code/__init__.py b/sample-code/sample_code/__init__.py similarity index 100% rename from sample-code/src/sample_code/__init__.py rename to sample-code/sample_code/__init__.py diff --git a/sample-code/src/sample_code/amazon.py b/sample-code/sample_code/amazon.py similarity index 100% rename from sample-code/src/sample_code/amazon.py rename to sample-code/sample_code/amazon.py diff --git a/sample-code/src/sample_code/core.py b/sample-code/sample_code/core.py similarity index 100% rename from sample-code/src/sample_code/core.py rename to sample-code/sample_code/core.py diff --git a/sample-code/src/sample_code/google.py b/sample-code/sample_code/google.py similarity index 100% rename from sample-code/src/sample_code/google.py rename to sample-code/sample_code/google.py diff --git a/sample-code/src/sample_code/openai.py b/sample-code/sample_code/openai.py similarity index 100% rename from sample-code/src/sample_code/openai.py rename to sample-code/sample_code/openai.py diff --git a/sample-code/src/sample_code/orchestration.py b/sample-code/sample_code/orchestration.py similarity index 100% rename from sample-code/src/sample_code/orchestration.py rename to sample-code/sample_code/orchestration.py diff --git a/sample-code/src/sample_code/server.py b/sample-code/sample_code/server.py similarity index 100% rename from sample-code/src/sample_code/server.py rename to sample-code/sample_code/server.py From 7d62d4a86116f5f1130e25457bca39714aad5475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Wien=C3=B6bst?= Date: Thu, 27 Aug 2026 14:40:46 +0200 Subject: [PATCH 9/9] move to root level Makefile --- Makefile | 3 +++ sample-code/Makefile | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 sample-code/Makefile diff --git a/Makefile b/Makefile index 04a4a0c..231787b 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,9 @@ lint: license-check: uv run pip-licenses +sample-code-server: + uv run uvicorn sample_code.server:app --env-file sample-code/.env --reload + test: uv run pytest packages/base/tests uv run pytest packages/core/tests diff --git a/sample-code/Makefile b/sample-code/Makefile deleted file mode 100644 index c9fe416..0000000 --- a/sample-code/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -server: - uv run uvicorn sample_code.server:app --app-dir src --env-file .env --reload