diff --git a/pyproject.toml b/pyproject.toml index 55a508d..4485824 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/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/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..6b9ffe8 --- /dev/null +++ b/sample-code/README.md @@ -0,0 +1,41 @@ +# Sample Code - Work in Progress + +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: + +```bash +AICORE_CLIENT_ID="..." +AICORE_CLIENT_SECRET="..." +AICORE_AUTH_URL="..." +AICORE_BASE_URL="..." +``` + +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 + +When the server is running, head to `http://localhost:8000/docs` to see all available endpoints. diff --git a/sample-code/pyproject.toml b/sample-code/pyproject.toml new file mode 100644 index 0000000..9eeac18 --- /dev/null +++ b/sample-code/pyproject.toml @@ -0,0 +1,20 @@ +[project] +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", + "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/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" + ] +} 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/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 new file mode 100644 index 0000000..e92f1af --- /dev/null +++ b/sample-code/src/sample_code/core.py @@ -0,0 +1,80 @@ +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(): + """ + 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() + # 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"}), + ] + 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(): + """ + 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..bb02697 --- /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/plain") + + +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 new file mode 100644 index 0000000..8560853 --- /dev/null +++ b/sample-code/src/sample_code/openai.py @@ -0,0 +1,126 @@ +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 + + +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?"}, + { + "role": "assistant", + "content": "Yes, customer managed keys are supported by Azure OpenAI.", + }, + { + "role": "user", + "content": "Do other Azure Cognitive Services support this too?", + }, + ] + response = chat.completions.create(model_name="gpt-5.4-nano", messages=messages) + 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. + + Returns: + JSON object response from the model. + """ + + class Person(BaseModel): + name: str + age: int + + 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(): + """ + 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."}, + ] + + 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/plain") + + +def responses_simple(): + """ + 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.", + text_format=Person, + ) + return response.output_parsed + + +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 { + "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..7088744 --- /dev/null +++ b/sample-code/src/sample_code/orchestration.py @@ -0,0 +1,867 @@ +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() + await service.aclose_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/plain") + + +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 + 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: + return {"result": "Input was filtered as expected."} + else: + raise + finally: + service.close_http_connection() + + +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. + """ + embedding_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ) + ) + ) + + service = OrchestrationService() + response = service.embed( + config=embedding_config, input=EmbeddingsInput(text="Hello World!") + ) + service.close_http_connection() + return {"result": response.final_result.data[0].embedding} + + +def embedding_batched(): + """ + Run batched 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") + ) + ) + ) + + input_list = ["Hello World!", "This is your captain speaking"] + + service = OrchestrationService() + response = service.embed( + config=embedding_config, input=EmbeddingsInput(text=input_list) + ) + service.close_http_connection() + return {"result": response.final_result.data} + + +def embedding_masked(): + """ + 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( + providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[ + DPIStandardEntity(type=ProfileEntity.PERSON), + DPIStandardEntity(type=ProfileEntity.EMAIL), + DPIStandardEntity(type=ProfileEntity.PHONE), + ], + ) + ] + ), + ) + ) + + service = OrchestrationService() + response = service.embed( + 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} + + +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 not tool_calls: + 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) + service.close_http_connection() + 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) + service.close_http_connection() + 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) + 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 new file mode 100644 index 0000000..4e011e3 --- /dev/null +++ b/sample-code/src/sample_code/server.py @@ -0,0 +1,70 @@ +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +from sample_code import amazon, core, google, openai, orchestration + +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) + +# 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/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-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) +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) diff --git a/uv.lock b/uv.lock index 461ae24..d887a3c 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", @@ -249,6 +250,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" @@ -924,6 +934,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" @@ -4068,6 +4094,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" @@ -4480,6 +4523,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" @@ -4809,6 +4865,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"