From 1ab4b807b6288c8113bd52aefee6049478a7f71e Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Mon, 14 Sep 2026 11:19:29 +0200 Subject: [PATCH 01/14] Add example codes for grounding, langchain, prompt registry and rpt modules. --- sample-code/sample_code/grounding.py | 143 +++++++++++++ .../sample_code/langchain_orchestration.py | 193 ++++++++++++++++++ sample-code/sample_code/prompt_registry.py | 83 ++++++++ sample-code/sample_code/sap_rpt.py | 98 +++++++++ sample-code/sample_code/server.py | 37 +++- 5 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 sample-code/sample_code/grounding.py create mode 100644 sample-code/sample_code/langchain_orchestration.py create mode 100644 sample-code/sample_code/prompt_registry.py create mode 100644 sample-code/sample_code/sap_rpt.py diff --git a/sample-code/sample_code/grounding.py b/sample-code/sample_code/grounding.py new file mode 100644 index 00000000..e9f496df --- /dev/null +++ b/sample-code/sample_code/grounding.py @@ -0,0 +1,143 @@ +from fastapi import Query + +from gen_ai_hub.document_grounding.client import ( + PipelineAPIClient, + RetrievalAPIClient, + VectorAPIClient, +) +from gen_ai_hub.document_grounding.models.retrieval import ( + RetrievalSearchConfiguration, + RetrievalSearchFilter, + RetrievalSearchInput, +) +from gen_ai_hub.document_grounding.models.vector import ( + BaseDocument, + CollectionCreateRequest, + DocumentsCreateRequest, + EmbeddingConfig, + TextOnlyBaseChunk, + VectorKeyValueListPair, +) + + +def get_collections(): + """ + List all vector collections available to the tenant. + + Returns: + JSON object containing the list of collections. + """ + client = VectorAPIClient() + return client.get_collections() + + +def create_collection(): + """ + Create a new vector collection with a text-embedding model. + + Returns: + JSON object containing the ID of the created collection. + """ + client = VectorAPIClient() + return client.create_collection( + CollectionCreateRequest( + title="sample-collection", + embeddingConfig=EmbeddingConfig(modelName="text-embedding-3-large"), + metadata=[VectorKeyValueListPair(key="source", value=["sample-code"])], + ) + ) + + +def delete_collection(collection_id: str): + """ + Delete a collection by its ID. + + Args: + collection_id: The ID of the collection to delete. + + Returns: + HTTP response confirming deletion with HTTP status code 204. + """ + client = VectorAPIClient() + return client.delete_collection(collection_id) + + +def create_documents(collection_id: str): + """ + Add two sample documents with text chunks to an existing collection. + + Args: + collection_id: The ID of the collection to add documents to. + + Returns: + JSON object containing the created document IDs. + """ + client = VectorAPIClient() + return client.create_documents( + collection_id, + DocumentsCreateRequest( + documents=[ + BaseDocument( + chunks=[ + TextOnlyBaseChunk( + content="SAP BTP provides cloud-native platform services for building enterprise applications.", + metadata=[VectorKeyValueListPair(key="language", value=["en"])], + ) + ], + metadata=[VectorKeyValueListPair(key="topic", value=["BTP"])], + ), + BaseDocument( + chunks=[ + TextOnlyBaseChunk( + content="HANA Vector Store enables semantic search over large document collections using embeddings.", + metadata=[VectorKeyValueListPair(key="language", value=["en"])], + ) + ], + metadata=[VectorKeyValueListPair(key="topic", value=["HANA"])], + ), + ] + ), + ) + + +def get_pipelines(): + """ + List all document vectorization pipelines configured for the tenant. + + Returns: + JSON object containing the list of pipelines with their IDs and types. + """ + client = PipelineAPIClient() + return client.get_pipelines() + + +def retrieval_documents( + query: str = "What are the key features of SAP BTP?", + data_repository_type: str = "vector", + data_repositories: list[str] = Query(default=["*"]), +): + """ + Retrieve documents across data repositories. + + Args: + query: Search query. + data_repository_type: Type of data repository. + data_repositories: List of data repository IDs to search in. + + Returns: + Search results. + """ + client = RetrievalAPIClient() + return client.search( + RetrievalSearchInput( + query=query, + filters=[ + RetrievalSearchFilter( + id="filter-1", + dataRepositoryType=data_repository_type, + dataRepositories=data_repositories, + searchConfiguration=RetrievalSearchConfiguration(maxChunkCount=1), + ) + ], + ) + ) diff --git a/sample-code/sample_code/langchain_orchestration.py b/sample-code/sample_code/langchain_orchestration.py new file mode 100644 index 00000000..ef5987a6 --- /dev/null +++ b/sample-code/sample_code/langchain_orchestration.py @@ -0,0 +1,193 @@ +import uuid + +from fastapi.responses import StreamingResponse +from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, ToolMessage +from langchain_core.output_parsers import StrOutputParser +from langchain_core.runnables import RunnableConfig +from langchain_core.tools import tool +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import END, START, MessagesState, StateGraph +from pydantic import BaseModel + +from gen_ai_hub.proxy.core import get_proxy_client +from gen_ai_hub.proxy.langchain.init_models import init_embedding_model, init_llm +from gen_ai_hub.proxy.langchain.openai import ChatOpenAI + +def _build_langgraph_app(model_name: str = "gpt-5.4-nano"): + """Build a simple single-node LangGraph app with in-memory checkpointing.""" + llm = ChatOpenAI(proxy_model_name=model_name) + + async def call_model(state: MessagesState): + response = await llm.ainvoke(state["messages"]) + return {"messages": [response]} + + workflow = ( + StateGraph(MessagesState) + .add_node("model", call_model) + .add_edge(START, "model") + .add_edge("model", END) + ) + return workflow.compile(checkpointer=MemorySaver()) + + +async def langgraph_chat_completion(): + """ + Invoke the model twice within the same thread to demonstrate memory across turns. + + Returns: + JSON object containing both responses. + """ + app = _build_langgraph_app() + config: RunnableConfig = {"configurable": {"thread_id": str(uuid.uuid4())}} + + output1 = await app.ainvoke( + {"messages": [HumanMessage(content="Tell me something about the SAP AI SDK")]}, + config=config, + ) + output2 = await app.ainvoke( + {"messages": [HumanMessage(content="What is special about it? Tell me in 3 sentences!")]}, + config=config, + ) + + first = output1["messages"][-1].content + second = output2["messages"][-1].content + return {"result": f"{first}\n\n{second}"} + + +async def langgraph_chat_completion_stream(): + """ + Stream two sequential turns through a LangGraph workflow. + + Returns: + A StreamingResponse that yields both turns separated by a blank line. + """ + app = _build_langgraph_app() + thread_config: RunnableConfig = {"configurable": {"thread_id": str(uuid.uuid4())}} + + async def generate(): + async for chunk, _ in app.astream( + {"messages": [HumanMessage(content="Tell me something about the SAP AI SDK")]}, + config=thread_config, + stream_mode="messages", + ): + content = chunk.content # type: ignore[union-attr] + if isinstance(content, str) and content: + yield content + + yield "\n\n" + + async for chunk, _ in app.astream( + {"messages": [HumanMessage(content="What is special about it? Tell me in 3 sentences!")]}, + config=thread_config, + stream_mode="messages", + ): + content = chunk.content # type: ignore[union-attr] + if isinstance(content, str) and content: + yield content + + return StreamingResponse(generate(), media_type="text/plain") + + +def tool_chain(): + """ + Demonstrate tool calling: the model calls a custom Python function and + the result is fed back for a final natural-language response. + + Returns: + JSON object containing the final model response. + """ + llm = ChatOpenAI(proxy_model_name="gpt-5.4") + + @tool + def shareholder_value(value: float) -> str: + """Multiplies the shareholder value.""" + return f"The shareholder value has been increased to {value * 2}" + + messages: list[BaseMessage] = [HumanMessage(content="Increase the shareholder value, it is currently at 10")] + + response = llm.bind_tools([shareholder_value]).invoke(messages) + messages.append(response) + + if response.tool_calls and response.tool_calls[0]["name"] == "shareholder_value": + tool_call = response.tool_calls[0] + tool_result = shareholder_value.invoke(tool_call["args"]) + messages.append( + ToolMessage(content=tool_result, tool_call_id=tool_call["id"] or "default") + ) + else: + messages.append(SystemMessage(content="No tool calls were made")) + + final = llm.invoke(messages) + return {"result": StrOutputParser().invoke(final)} + + +class SampleSchema(BaseModel): + """A sample structured output schema.""" + + setup: str + punchline: str + rating: int + + +def structured_output(): + """ + Ask the model for a structured response conforming to a Pydantic schema. + + Returns: + JSON object containing the structured output. + """ + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + structured_llm = llm.with_structured_output(SampleSchema) + result = structured_llm.invoke("Tell me a joke about cats") + if not isinstance(result, SampleSchema): + raise RuntimeError("Unexpected structured output type") + return {"result": result.model_dump()} + +def invoke_chain_with_fallback_configs(): + """ + Invoke a chain with fallback model configurations for resilience. + + If the primary model fails, LangChain automatically retries with each + fallback in order until one succeeds. + + Returns: + JSON object containing the model response. + """ + primary_llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + client = get_proxy_client() + fallback_llms = [ + ChatOpenAI(proxy_model_name="anthropic--claude-4.6-sonnet"), + init_llm("anthropic--claude-4.6-sonnet", proxy_client=client), + ] + llm = primary_llm.with_fallbacks(fallback_llms) + chain = llm | StrOutputParser() + result = chain.invoke([HumanMessage(content="Tell me about SAP AI SDK")]) + return {"result": result} + + +def invoke_dynamic_model_agent(): + """ + Select a model dynamically based on input complexity. + + Short or simple prompts are routed to a lightweight model; longer or more + complex prompts are routed to a more capable model. The routing decision is + made at invocation time via a custom selector function. + + Returns: + JSON object containing the model response. + """ + simple_chain = ChatOpenAI(proxy_model_name="gpt-5.4-nano") | StrOutputParser() + complex_chain = ChatOpenAI(proxy_model_name="anthropic--claude-4.6-sonnet") | StrOutputParser() + + def select_chain(messages: list): + total_words = sum(len(str(m.content).split()) for m in messages) + return complex_chain if total_words > 20 else simple_chain + + message = ( + "Explain the key architectural differences between microservices and monolithic " + "applications, covering scalability, maintainability, deployment complexity, and " + "data management strategies." + ) + messages = [HumanMessage(content=message)] + result = select_chain(messages).invoke(messages) + return {"result": result} \ No newline at end of file diff --git a/sample-code/sample_code/prompt_registry.py b/sample-code/sample_code/prompt_registry.py new file mode 100644 index 00000000..c266014c --- /dev/null +++ b/sample-code/sample_code/prompt_registry.py @@ -0,0 +1,83 @@ +from gen_ai_hub.prompt_registry.client import OrchestrationConfigClient, PromptTemplateClient +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplate, PromptTemplateSpec +from gen_ai_hub.orchestration_v2.models.config import ModuleConfig, OrchestrationConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.template import PromptTemplatingModuleConfig, Template + +SCENARIO = "my-scenario" +TEMPLATE_NAME = "my-template" +VERSION = "1.0.0" +CONFIG_NAME = "my-orchestration-config" + + +def create_prompt_template(): + """ + Create a prompt template with a user-input placeholder. + + The placeholder {{ ?user_input }} will be substituted at runtime via fill_prompt_template. + """ + client = PromptTemplateClient() + spec = PromptTemplateSpec( + template=[ + PromptTemplate(role="system", content="You are a helpful assistant."), + PromptTemplate(role="user", content="Hello World!"), + ] + ) + return client.create_prompt_template( + scenario=SCENARIO, + name=TEMPLATE_NAME, + version=VERSION, + prompt_template_spec=spec, + ) + + +def get_prompt_templates(): + """List all prompt templates matching the scenario/name/version filter.""" + client = PromptTemplateClient() + return client.get_prompt_templates(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION) + + +def delete_prompt_template(template_id: str): + """Delete a prompt template by its ID.""" + client = PromptTemplateClient() + return client.delete_prompt_template_by_id(template_id) + + +def create_orchestration_config(): + """ + Create an orchestration config that bundles an LLM and a prompt template. + + The config references gpt-4o-mini and a static Hello World prompt. + """ + client = OrchestrationConfigClient() + spec = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage(content="You are a helpful assistant."), + UserMessage(content="Hello, World!"), + ] + ), + model=LLMModelDetails(name="gpt-4o-mini"), + ) + ) + ) + return client.create_orchestration_config( + scenario=SCENARIO, + name=CONFIG_NAME, + version=VERSION, + spec=spec, + ) + + +def get_orchestration_configs(): + """List orchestration configs matching the scenario/name/version filter.""" + client = OrchestrationConfigClient() + return client.get_orchestration_configs( + scenario=SCENARIO, + name=CONFIG_NAME, + version=VERSION, + include_spec=True, + ) diff --git a/sample-code/sample_code/sap_rpt.py b/sample-code/sample_code/sap_rpt.py new file mode 100644 index 00000000..2b3a3cdd --- /dev/null +++ b/sample-code/sample_code/sap_rpt.py @@ -0,0 +1,98 @@ +from gen_ai_hub.proxy.native.sap.client import RPTClient +from gen_ai_hub.proxy.native.sap.models import DataType, PredictionConfig, RPTRequest, TargetColumn + +MODEL_NAME = "sap-rpt-1-small" + +CLASSIFICATION_SCHEMA = { + "PRODUCT": DataType(dtype="string"), + "PRICE": DataType(dtype="numeric"), + "ORDERDATE": DataType(dtype="date"), + "ID": DataType(dtype="string"), + "COSTCENTER": DataType(dtype="string"), +} + +CLASSIFICATION_ROWS = [ + {"PRODUCT": "Couch", "PRICE": 999.99, "ORDERDATE": "28-11-2025", "ID": "35", "COSTCENTER": "[PREDICT]"}, + {"PRODUCT": "Office Chair", "PRICE": 150.8, "ORDERDATE": "02-11-2025", "ID": "44", "COSTCENTER": "Office Furniture"}, + {"PRODUCT": "Server Rack", "PRICE": 2200.00, "ORDERDATE": "01-11-2025", "ID": "104", "COSTCENTER": "Data Infrastructure"}, +] + +CLASSIFICATION_COLUMNS = { + "PRODUCT": ["Couch", "Office Chair", "Server Rack"], + "PRICE": [999.99, 150.8, 2200.00], + "ORDERDATE": ["28-11-2025", "02-11-2025", "01-11-2025"], + "ID": ["35", "44", "104"], + "COSTCENTER": ["[PREDICT]", "Office Furniture", "Data Infrastructure"], +} + +REGRESSION_ROWS = [ + {"PRODUCT": "Couch", "PRICE": 999.99, "ORDERDATE": "28-11-2025", "ID": "35", "DISCOUNT_RATE": "[PREDICT]"}, + {"PRODUCT": "Office Chair", "PRICE": 150.80, "ORDERDATE": "02-11-2025", "ID": "44", "DISCOUNT_RATE": 0.12}, + {"PRODUCT": "Server Rack", "PRICE": 2200.00, "ORDERDATE": "01-11-2025", "ID": "104", "DISCOUNT_RATE": 0.05}, + {"PRODUCT": "Standing Desk", "PRICE": 640.00, "ORDERDATE": "05-11-2025", "ID": "205", "DISCOUNT_RATE": 0.10}, + {"PRODUCT": "Monitor 27 inch", "PRICE": 289.99, "ORDERDATE": "08-11-2025", "ID": "306", "DISCOUNT_RATE": "[PREDICT]"}, +] + + +def predict_by_rows(): + """ + Classify a target column using row-oriented input data. + + Context rows supply known COSTCENTER values; the query row marked + with "[PREDICT]" receives a predicted classification. + """ + client = RPTClient() + body = RPTRequest( + prediction_config=PredictionConfig( + target_columns=[ + TargetColumn( + name="COSTCENTER", + prediction_placeholder="[PREDICT]", + task_type="classification", + ) + ] + ), + index_column="ID", + rows=CLASSIFICATION_ROWS, + data_schema=CLASSIFICATION_SCHEMA, + ) + return client.predict(body=body, model_name=MODEL_NAME) + + +def predict_by_columns(): + """ + Classify a target column using column-oriented input data. + + Equivalent to predict_by_rows but uses the columns format instead of rows. + """ + client = RPTClient() + body = RPTRequest( + prediction_config=PredictionConfig( + target_columns=[ + TargetColumn( + name="COSTCENTER", + prediction_placeholder="[PREDICT]", + task_type="classification", + ) + ] + ), + columns=CLASSIFICATION_COLUMNS, + data_schema=CLASSIFICATION_SCHEMA, + ) + return client.predict(body=body, model_name=MODEL_NAME) + + +def regression(): + """ + Predict a numeric target column (regression). + + Rows with "[PREDICT]" in DISCOUNT_RATE receive a predicted numeric value. + """ + client = RPTClient() + body = RPTRequest( + prediction_config=PredictionConfig( + target_columns=[TargetColumn(name="DISCOUNT_RATE", task_type="regression")] + ), + rows=REGRESSION_ROWS, + ) + return client.predict(body=body, model_name=MODEL_NAME) \ No newline at end of file diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 4e011e3b..8d6892c4 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -from sample_code import amazon, core, google, openai, orchestration +from sample_code import amazon, core, google, grounding, langchain_orchestration, openai, orchestration, prompt_registry, sap_rpt app = FastAPI(title="SAP AI Core Python SDK Sample Application") @@ -45,6 +45,21 @@ async def health(): # Amazon/Anthropic app.get("/amazon/converse")(amazon.converse) +# LangChain +app.get("/langchain/chat-completion")(langchain_orchestration.init_llm_chat_completion) +app.get("/langchain/embedding")(langchain_orchestration.init_embedding) +app.get("/langchain/chat-completion-with-fallback")(langchain_orchestration.invoke_chain_with_fallback_configs) +app.get("/langchain/dynamic-model-agent")(langchain_orchestration.invoke_dynamic_model_agent) +app.get("/langchain/tool-chain")(langchain_orchestration.tool_chain) +app.get("/langchain/structured-output")(langchain_orchestration.structured_output) +app.get("/langchain/langgraph/chat-completion")(langchain_orchestration.langgraph_chat_completion) +app.get("/langchain/langgraph/chat-completion-stream")(langchain_orchestration.langgraph_chat_completion_stream) + +# SAP RPT-1 +app.get("/sap-rpt/predict-by-rows")(sap_rpt.predict_by_rows) +app.get("/sap-rpt/predict-by-columns")(sap_rpt.predict_by_columns) +app.get("/sap-rpt/predict-regression")(sap_rpt.regression) + # Orchestration app.get("/orchestration/completion")(orchestration.completion) app.get("/orchestration/completion-async")(orchestration.completion_async) @@ -68,3 +83,23 @@ async def health(): 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) + +# Prompt Registry - Prompt Templates +app.post("/prompt-registry/template/create")(prompt_registry.create_prompt_template) +app.get("/prompt-registry/templates")(prompt_registry.get_prompt_templates) +app.delete("/prompt-registry/template/{template_id}")(prompt_registry.delete_prompt_template) + +# Prompt Registry - Orchestration Configs +app.post("/prompt-registry/config/create")(prompt_registry.create_orchestration_config) +app.get("/prompt-registry/configs")(prompt_registry.get_orchestration_configs) + +# Document Grounding - Vector API +app.post("/document-grounding/vector/create-collection")(grounding.create_collection) +app.delete("/document-grounding/vector/delete-collection/{collection_id}")(grounding.delete_collection) +app.post("/document-grounding/vector/add-documents/{collection_id}")(grounding.create_documents) + +# Document Grounding - Pipeline API +app.get("/document-grounding/pipeline/get-pipelines")(grounding.get_pipelines) + +# Document Grounding - Retrieval API +app.get("/document-grounding/retrieval/search")(grounding.retrieval_documents) From fb30450111cce3537fd43699f100c50b30675cba Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Tue, 15 Sep 2026 17:27:50 +0200 Subject: [PATCH 02/14] add more sample code --- .../orchestration_v2/models/message.py | 2 +- .../sample_code/langchain_orchestration.py | 26 ++++++++++++++++++- sample-code/sample_code/prompt_registry.py | 23 ++++++++++++++-- sample-code/sample_code/sap_rpt.py | 10 +++++++ sample-code/sample_code/server.py | 2 ++ 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/message.py b/packages/gen/gen_ai_hub/orchestration_v2/models/message.py index f098c775..6885849e 100644 --- a/packages/gen/gen_ai_hub/orchestration_v2/models/message.py +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/message.py @@ -184,7 +184,7 @@ class ResponseChatMessage(BaseModel): tool_calls: A list of tool call objects. """ role: Role = Role.ASSISTANT - content: str + content: Optional[str] = None refusal: Optional[str] = None tool_calls: Optional[List[MessageToolCall]] = None diff --git a/sample-code/sample_code/langchain_orchestration.py b/sample-code/sample_code/langchain_orchestration.py index ef5987a6..9b2e38fe 100644 --- a/sample-code/sample_code/langchain_orchestration.py +++ b/sample-code/sample_code/langchain_orchestration.py @@ -13,6 +13,30 @@ from gen_ai_hub.proxy.langchain.init_models import init_embedding_model, init_llm from gen_ai_hub.proxy.langchain.openai import ChatOpenAI +def init_llm_chat_completion(): + """ + Run a basic chat completion using the init_llm helper. + + Returns: + JSON object containing the model response. + """ + llm = init_llm("gpt-5.4-nano") + result = llm.invoke("Tell me something about the SAP AI SDK") + return {"result": StrOutputParser().invoke(result)} + + +def init_embedding(): + """ + Generate an embedding vector using the init_embedding_model helper. + + Returns: + JSON object containing the embedding vector. + """ + embedding_model = init_embedding_model("text-embedding-3-small") + result = embedding_model.embed_query("SAP AI SDK") + return {"result": result} + + def _build_langgraph_app(model_name: str = "gpt-5.4-nano"): """Build a simple single-node LangGraph app with in-memory checkpointing.""" llm = ChatOpenAI(proxy_model_name=model_name) @@ -96,7 +120,7 @@ def tool_chain(): Returns: JSON object containing the final model response. """ - llm = ChatOpenAI(proxy_model_name="gpt-5.4") + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") @tool def shareholder_value(value: float) -> str: diff --git a/sample-code/sample_code/prompt_registry.py b/sample-code/sample_code/prompt_registry.py index c266014c..396a9209 100644 --- a/sample-code/sample_code/prompt_registry.py +++ b/sample-code/sample_code/prompt_registry.py @@ -15,13 +15,13 @@ def create_prompt_template(): """ Create a prompt template with a user-input placeholder. - The placeholder {{ ?user_input }} will be substituted at runtime via fill_prompt_template. + The placeholder {{?user_input}} will be substituted at runtime via fill_prompt_template. """ client = PromptTemplateClient() spec = PromptTemplateSpec( template=[ PromptTemplate(role="system", content="You are a helpful assistant."), - PromptTemplate(role="user", content="Hello World!"), + PromptTemplate(role="user", content="{{?user_input}}"), ] ) return client.create_prompt_template( @@ -32,6 +32,25 @@ def create_prompt_template(): ) +def fill_prompt_template(): + """ + Fill the prompt template placeholders with concrete values. + + Replaces the {{?user_input}} placeholder in the template with a concrete question. + + Returns: + JSON object containing the filled prompt messages. + """ + client = PromptTemplateClient() + response = client.fill_prompt_template( + scenario=SCENARIO, + name=TEMPLATE_NAME, + version=VERSION, + input_params={"user_input": "What are the main features of SAP BTP?"}, + ) + return {"result": [msg.model_dump() for msg in response.parsed_prompt]} + + def get_prompt_templates(): """List all prompt templates matching the scenario/name/version filter.""" client = PromptTemplateClient() diff --git a/sample-code/sample_code/sap_rpt.py b/sample-code/sample_code/sap_rpt.py index 2b3a3cdd..565fcfb7 100644 --- a/sample-code/sample_code/sap_rpt.py +++ b/sample-code/sample_code/sap_rpt.py @@ -33,6 +33,14 @@ {"PRODUCT": "Monitor 27 inch", "PRICE": 289.99, "ORDERDATE": "08-11-2025", "ID": "306", "DISCOUNT_RATE": "[PREDICT]"}, ] +REGRESSION_SCHEMA = { + "PRODUCT": DataType(dtype="string"), + "PRICE": DataType(dtype="numeric"), + "ORDERDATE": DataType(dtype="date"), + "ID": DataType(dtype="string"), + "DISCOUNT_RATE": DataType(dtype="numeric"), +} + def predict_by_rows(): """ @@ -93,6 +101,8 @@ def regression(): prediction_config=PredictionConfig( target_columns=[TargetColumn(name="DISCOUNT_RATE", task_type="regression")] ), + index_column="ID", rows=REGRESSION_ROWS, + data_schema=REGRESSION_SCHEMA, ) return client.predict(body=body, model_name=MODEL_NAME) \ No newline at end of file diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 8d6892c4..27e3cbcf 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -87,6 +87,7 @@ async def health(): # Prompt Registry - Prompt Templates app.post("/prompt-registry/template/create")(prompt_registry.create_prompt_template) app.get("/prompt-registry/templates")(prompt_registry.get_prompt_templates) +app.post("/prompt-registry/template/fill")(prompt_registry.fill_prompt_template) app.delete("/prompt-registry/template/{template_id}")(prompt_registry.delete_prompt_template) # Prompt Registry - Orchestration Configs @@ -94,6 +95,7 @@ async def health(): app.get("/prompt-registry/configs")(prompt_registry.get_orchestration_configs) # Document Grounding - Vector API +app.get("/document-grounding/vector/get-collections")(grounding.get_collections) app.post("/document-grounding/vector/create-collection")(grounding.create_collection) app.delete("/document-grounding/vector/delete-collection/{collection_id}")(grounding.delete_collection) app.post("/document-grounding/vector/add-documents/{collection_id}")(grounding.create_documents) From 8f51cc718427eaa754d7e55b7afcbb6c110f5e69 Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Thu, 17 Sep 2026 15:44:16 +0200 Subject: [PATCH 03/14] Add more sample codes --- sample-code/sample_code/grounding.py | 2 +- sample-code/sample_code/langchain_openai.py | 153 ++++++ .../sample_code/langchain_orchestration.py | 440 +++++++++++------- sample-code/sample_code/prompt_registry.py | 4 +- sample-code/sample_code/server.py | 31 +- 5 files changed, 455 insertions(+), 175 deletions(-) create mode 100644 sample-code/sample_code/langchain_openai.py diff --git a/sample-code/sample_code/grounding.py b/sample-code/sample_code/grounding.py index e9f496df..3a131890 100644 --- a/sample-code/sample_code/grounding.py +++ b/sample-code/sample_code/grounding.py @@ -42,7 +42,7 @@ def create_collection(): return client.create_collection( CollectionCreateRequest( title="sample-collection", - embeddingConfig=EmbeddingConfig(modelName="text-embedding-3-large"), + embeddingConfig=EmbeddingConfig(modelName="text-embedding-3-small"), metadata=[VectorKeyValueListPair(key="source", value=["sample-code"])], ) ) diff --git a/sample-code/sample_code/langchain_openai.py b/sample-code/sample_code/langchain_openai.py new file mode 100644 index 00000000..8db645e3 --- /dev/null +++ b/sample-code/sample_code/langchain_openai.py @@ -0,0 +1,153 @@ +from collections.abc import AsyncGenerator + +from fastapi.responses import StreamingResponse +from langchain_core.messages import HumanMessage, ToolMessage +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnablePassthrough +from langchain_core.tools import tool +from langchain_core.vectorstores import InMemoryVectorStore +from pydantic import BaseModel + +from gen_ai_hub.proxy.langchain.openai import ChatOpenAI, OpenAIEmbeddings + + +class SampleSchema(BaseModel): + """A sample structured output schema.""" + + content: str + language: str + +def invoke(): + """ + Ask GPT about the capital of Germany. + + Returns: + The answer from the GPT + """ + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + response = llm.invoke("Where is the capital of Germany?") + parser = StrOutputParser() + return parser.invoke(response) + +def invoke_chain(): + """ + Chain a prompt template, LLM, and output parser to answer in German. + + Returns: + The model response as a string. + """ + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + prompt_template = ChatPromptTemplate.from_messages([ + ("system", "Answer the following in {language}:"), + ("user", "{text}"), + ]) + chain = prompt_template | llm | StrOutputParser() + return chain.invoke({"language": "german", "text": "What is the capital of Germany?"}) + + +def invoke_with_structured_output_json_schema(): + """ + Invoke the LLM with structured output using JSON schema (Pydantic model). + + Returns: + SampleSchema instance with content and language fields. + """ + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + structured_llm = llm.with_structured_output(method="json_schema", schema=SampleSchema, strict=True) + prompt_template = ChatPromptTemplate.from_messages([ + ("system", "Answer the following question. Respond with the answer text and the language you answered in."), + ("user", "{text}"), + ]) + chain = prompt_template | structured_llm + return chain.invoke({"text": "What is the capital of France?"}) + + +def invoke_tool_chain(): + """ + Invoke a tool chain: bind an add tool to the LLM, execute any tool calls, + then return the final answer. + + Returns: + The final model response as a string after tool execution. + """ + @tool + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + llm_with_tools = llm.bind_tools([add]) + + messages = [HumanMessage(content="What is 279 + 929?")] + response = llm_with_tools.invoke(messages) + messages.append(response) + + for tool_call in response.tool_calls: + if tool_call["name"] == "add": + result = add.invoke(tool_call["args"]) + messages.append(ToolMessage(content=str(result), tool_call_id=tool_call["id"])) + + final_response = llm_with_tools.invoke(messages) + parser = StrOutputParser() + return parser.invoke(final_response) + + +def invoke_rag_chain(): + """ + Build a RAG chain using SAP AI SDK embeddings and chat model with LangChain. + + Documents are embedded with OpenAIEmbeddings from gen_ai_hub and stored in an + in-memory vector store. A retriever feeds relevant context into a prompt template + that is then answered by ChatOpenAI. + + Returns: + The answer to "What is the best sdk in the world?" grounded in the provided documents. + """ + documents = [ + "SAP BTP provides cloud-native platform services for building enterprise applications.", + "The SAP AI Core service lets you train and deploy machine learning models at scale.", + "The SAP Cloud SDK for Python is the best SDK in the world.", + "SAP Joule is the AI copilot embedded across the SAP portfolio of business applications.", + ] + + embedding_model = OpenAIEmbeddings(proxy_model_name="text-embedding-3-small") + vectorstore = InMemoryVectorStore.from_texts(documents, embedding=embedding_model) + retriever = vectorstore.as_retriever() + + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + prompt = ChatPromptTemplate.from_messages([ + ("system", "Answer the question using only the context below.\n\nContext:\n{context}"), + ("user", "{question}"), + ]) + + def format_docs(docs): + return "\n".join(doc.page_content for doc in docs) + + chain = ( + {"context": retriever | format_docs, "question": RunnablePassthrough()} + | prompt + | llm + | StrOutputParser() + ) + + return chain.invoke("What is the best sdk in the world?") + + +def stream_chain() -> StreamingResponse: + """ + Stream chunks from the LLM about SAP Cloud SDK. + + Returns: + StreamingResponse yielding text chunks. + """ + llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") + messages = [HumanMessage(content="Write a 1000 word explanation about SAP AI SDK and its capabilities")] + + async def generate() -> AsyncGenerator[str, None]: + async for chunk in llm.astream(messages): + if chunk.content: + yield chunk.content + + return StreamingResponse(generate(), media_type="text/plain") + diff --git a/sample-code/sample_code/langchain_orchestration.py b/sample-code/sample_code/langchain_orchestration.py index 9b2e38fe..a4aa9bd4 100644 --- a/sample-code/sample_code/langchain_orchestration.py +++ b/sample-code/sample_code/langchain_orchestration.py @@ -1,217 +1,331 @@ -import uuid - +from gen_ai_hub.orchestration_v2 import ( + AzureContentSafetyInput, + AzureContentSafetyInputFilterConfig, + AzureContentSafetyOutput, + AzureContentSafetyOutputFilterConfig, + AzureThreshold, + DPICustomEntity, + DPIMethodConstant, + DPIStandardEntity, + FilteringModuleConfig, + FunctionObject, + FunctionTool, + GlobalStreamOptions, + InputFiltering, + LLMModelDetails, + MaskingMethod, + MaskingModuleConfig, + MaskingProviderConfig, + ModuleConfig, + OrchestrationConfig, + OrchestrationService, + OutputFiltering, + ProfileEntity, + PromptTemplatingModuleConfig, + SystemMessage, + Template, + ToolChatMessage, + UserMessage, + function_tool, +) from fastapi.responses import StreamingResponse -from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, ToolMessage -from langchain_core.output_parsers import StrOutputParser -from langchain_core.runnables import RunnableConfig -from langchain_core.tools import tool +from gen_ai_hub.proxy.langchain.openai import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, START, MessagesState, StateGraph -from pydantic import BaseModel - -from gen_ai_hub.proxy.core import get_proxy_client -from gen_ai_hub.proxy.langchain.init_models import init_embedding_model, init_llm -from gen_ai_hub.proxy.langchain.openai import ChatOpenAI -def init_llm_chat_completion(): +def invoke_chain() -> str: """ - Run a basic chat completion using the init_llm helper. + Invoke the Orchestration Service with gpt-5.4-nano and return the response as a string. Returns: - JSON object containing the model response. + The model response as a string. """ - llm = init_llm("gpt-5.4-nano") - result = llm.invoke("Tell me something about the SAP AI SDK") - return {"result": StrOutputParser().invoke(result)} + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[UserMessage(content="Tell me about SAP AI SDK")] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return result.final_result.choices[0].message.content -def init_embedding(): +def invoke_chain_with_input_filter() -> str: """ - Generate an embedding vector using the init_embedding_model helper. + Invoke the Orchestration Service with an Azure content safety input filter. Returns: - JSON object containing the embedding vector. + The model response as a string. """ - embedding_model = init_embedding_model("text-embedding-3-small") - result = embedding_model.embed_query("SAP AI SDK") - return {"result": result} + filtering = FilteringModuleConfig( + input=InputFiltering( + filters=[ + AzureContentSafetyInputFilterConfig( + config=AzureContentSafetyInput( + hate=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE, + ) + ) + ] + ) + ) + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Tell me about the way to kill myself.")]), + model=LLMModelDetails(name="gpt-5.4-nano"), + ), + filtering=filtering, + ) + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return result.final_result.choices[0].message.content -def _build_langgraph_app(model_name: str = "gpt-5.4-nano"): - """Build a simple single-node LangGraph app with in-memory checkpointing.""" - llm = ChatOpenAI(proxy_model_name=model_name) +def invoke_chain_with_output_filter() -> str: + """ + Invoke the Orchestration Service with an Azure content safety output filter. - async def call_model(state: MessagesState): - response = await llm.ainvoke(state["messages"]) - return {"messages": [response]} + Uses gpt-5.4-nano which will comply with the prompt and generate + content that the output filter then blocks, leaving choices[0].message.content empty. + Output filtering does NOT raise an error — it silently empties the response content. - workflow = ( - StateGraph(MessagesState) - .add_node("model", call_model) - .add_edge(START, "model") - .add_edge("model", END) + Returns: + A message confirming the output was filtered. + Raises: + RuntimeError: If the output was not filtered as expected. + """ + filtering = FilteringModuleConfig( + output=OutputFiltering( + filters=[ + AzureContentSafetyOutputFilterConfig( + config=AzureContentSafetyOutput( + hate=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE, + ) + ) + ] + ) + ) + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[UserMessage(content="Please tell me 5 ways to kill myself.")] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ), + filtering=filtering, + ) ) - return workflow.compile(checkpointer=MemorySaver()) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return result.final_result.choices[0].message.content -async def langgraph_chat_completion(): +def invoke_chain_with_masking() -> str: """ - Invoke the model twice within the same thread to demonstrate memory across turns. + Invoke the Orchestration Service with DPI pseudonymization masking. + + PII (name, address, email, phone, date) in the prompt is replaced with + pseudonyms before being sent to the model. Returns both the masked prompt + (from intermediate results) and the final model response so masking is visible. Returns: - JSON object containing both responses. + Dict with 'masked_input' (pseudonymized prompt) and 'result' (model response). """ - app = _build_langgraph_app() - config: RunnableConfig = {"configurable": {"thread_id": str(uuid.uuid4())}} - - output1 = await app.ainvoke( - {"messages": [HumanMessage(content="Tell me something about the SAP AI SDK")]}, - config=config, + masking = MaskingModuleConfig( + providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + 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"), + ), + ], + ) + ] ) - output2 = await app.ainvoke( - {"messages": [HumanMessage(content="What is special about it? Tell me in 3 sentences!")]}, - config=config, + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + UserMessage( + content="Generate email 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=masking, + ) ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return result.final_result.choices[0].message.content - first = output1["messages"][-1].content - second = output2["messages"][-1].content - return {"result": f"{first}\n\n{second}"} - - -async def langgraph_chat_completion_stream(): +def invoke_chain_with_fallback() -> str: """ - Stream two sequential turns through a LangGraph workflow. - - Returns: - A StreamingResponse that yields both turns separated by a blank line. - """ - app = _build_langgraph_app() - thread_config: RunnableConfig = {"configurable": {"thread_id": str(uuid.uuid4())}} - - async def generate(): - async for chunk, _ in app.astream( - {"messages": [HumanMessage(content="Tell me something about the SAP AI SDK")]}, - config=thread_config, - stream_mode="messages", - ): - content = chunk.content # type: ignore[union-attr] - if isinstance(content, str) and content: - yield content - - yield "\n\n" - - async for chunk, _ in app.astream( - {"messages": [HumanMessage(content="What is special about it? Tell me in 3 sentences!")]}, - config=thread_config, - stream_mode="messages", - ): - content = chunk.content # type: ignore[union-attr] - if isinstance(content, str) and content: - yield content + Invoke the Orchestration Service with a fallback model. - return StreamingResponse(generate(), media_type="text/plain") + The first ModuleConfig uses a non-existent model to trigger fallback; + the second uses anthropic--claude-4.6-sonnet as the backup. + Returns: + The model response as a string. + """ + config = OrchestrationConfig( + modules=[ + ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), + model=LLMModelDetails(name="dummy-model"), + ) + ), + ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), + model=LLMModelDetails(name="anthropic--claude-4.6-sonnet"), + ) + ), + ] + ) + service = OrchestrationService(config=config) + result = service.run() + service.close_http_connection() + return result.final_result.choices[0].message.content -def tool_chain(): +def stream_chain() -> StreamingResponse: """ - Demonstrate tool calling: the model calls a custom Python function and - the result is fed back for a final natural-language response. + Stream a response from the Orchestration Service token by token. Returns: - JSON object containing the final model response. + StreamingResponse yielding text chunks. """ - llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") - - @tool - def shareholder_value(value: float) -> str: - """Multiplies the shareholder value.""" - return f"The shareholder value has been increased to {value * 2}" - - messages: list[BaseMessage] = [HumanMessage(content="Increase the shareholder value, it is currently at 10")] - - response = llm.bind_tools([shareholder_value]).invoke(messages) - messages.append(response) - - if response.tool_calls and response.tool_calls[0]["name"] == "shareholder_value": - tool_call = response.tool_calls[0] - tool_result = shareholder_value.invoke(tool_call["args"]) - messages.append( - ToolMessage(content=tool_result, tool_call_id=tool_call["id"] or "default") - ) - else: - messages.append(SystemMessage(content="No tool calls were made")) - - final = llm.invoke(messages) - return {"result": StrOutputParser().invoke(final)} - + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[UserMessage(content="Tell me about SAP AI SDK with 1000 words.")] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ), + stream=GlobalStreamOptions(enabled=True), + ) + service = OrchestrationService(config=config) -class SampleSchema(BaseModel): - """A sample structured output schema.""" + def generate(): + for chunk in service.stream(): + if chunk.final_result: + content = chunk.final_result.choices[0].delta.content + if content: + yield content + service.close_http_connection() - setup: str - punchline: str - rating: int + return StreamingResponse(generate(), media_type="text/plain") -def structured_output(): +def stream_chain_with_fallback() -> StreamingResponse: """ - Ask the model for a structured response conforming to a Pydantic schema. + Stream a response from the Orchestration Service with a fallback model. Returns: - JSON object containing the structured output. - """ - llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") - structured_llm = llm.with_structured_output(SampleSchema) - result = structured_llm.invoke("Tell me a joke about cats") - if not isinstance(result, SampleSchema): - raise RuntimeError("Unexpected structured output type") - return {"result": result.model_dump()} - -def invoke_chain_with_fallback_configs(): + StreamingResponse yielding text chunks from the fallback model. """ - Invoke a chain with fallback model configurations for resilience. + config = OrchestrationConfig( + modules=[ + ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), + model=LLMModelDetails(name="dummy-model"), + ) + ), + ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), + model=LLMModelDetails(name="anthropic--claude-4.6-sonnet"), + ) + ), + ], + stream=GlobalStreamOptions(enabled=True), + ) + service = OrchestrationService(config=config) - If the primary model fails, LangChain automatically retries with each - fallback in order until one succeeds. + def generate(): + for chunk in service.stream(): + if chunk.final_result: + content = chunk.final_result.choices[0].delta.content + if content: + yield content + service.close_http_connection() - Returns: - JSON object containing the model response. - """ - primary_llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") - client = get_proxy_client() - fallback_llms = [ - ChatOpenAI(proxy_model_name="anthropic--claude-4.6-sonnet"), - init_llm("anthropic--claude-4.6-sonnet", proxy_client=client), - ] - llm = primary_llm.with_fallbacks(fallback_llms) - chain = llm | StrOutputParser() - result = chain.invoke([HumanMessage(content="Tell me about SAP AI SDK")]) - return {"result": result} + return StreamingResponse(generate(), media_type="text/plain") -def invoke_dynamic_model_agent(): +def invoke_tool_chain() -> str: """ - Select a model dynamically based on input complexity. + Invoke a tool chain via the Orchestration Service. - Short or simple prompts are routed to a lightweight model; longer or more - complex prompts are routed to a more capable model. The routing decision is - made at invocation time via a custom selector function. + Binds a celsius_to_fahrenheit tool, executes the tool call triggered + by the model, then returns the final model response. Returns: - JSON object containing the model response. + The final model response as a string after tool execution. """ - simple_chain = ChatOpenAI(proxy_model_name="gpt-5.4-nano") | StrOutputParser() - complex_chain = ChatOpenAI(proxy_model_name="anthropic--claude-4.6-sonnet") | StrOutputParser() - - def select_chain(messages: list): - total_words = sum(len(str(m.content).split()) for m in messages) - return complex_chain if total_words > 20 else simple_chain - - message = ( - "Explain the key architectural differences between microservices and monolithic " - "applications, covering scalability, maintainability, deployment complexity, and " - "data management strategies." + @function_tool + def celsius_to_fahrenheit(celsius: float) -> str: + """Converts a temperature from Celsius to Fahrenheit.""" + fahrenheit = celsius * 9 / 5 + 32 + return f"{celsius}°C is {fahrenheit}°F" + + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage(content="You are a helpful assistant that converts temperatures."), + UserMessage(content="What is 100 degrees Celsius in Fahrenheit?"), + ], + tools=[celsius_to_fahrenheit], + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) ) - messages = [HumanMessage(content=message)] - result = select_chain(messages).invoke(messages) - return {"result": result} \ No newline at end of file + + service = OrchestrationService() + result = service.run(config=config) + tool_calls = result.final_result.choices[0].message.tool_calls + if not tool_calls: + raise RuntimeError("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: + tool_result = celsius_to_fahrenheit.execute(**tool_call.function.parse_arguments()) + history.append(ToolChatMessage(content=str(tool_result), tool_call_id=tool_call.id)) + + result = service.run(config=config, history=history) + service.close_http_connection() + return result.final_result.choices[0].message.content \ No newline at end of file diff --git a/sample-code/sample_code/prompt_registry.py b/sample-code/sample_code/prompt_registry.py index 396a9209..688ec939 100644 --- a/sample-code/sample_code/prompt_registry.py +++ b/sample-code/sample_code/prompt_registry.py @@ -67,7 +67,7 @@ def create_orchestration_config(): """ Create an orchestration config that bundles an LLM and a prompt template. - The config references gpt-4o-mini and a static Hello World prompt. + The config references gpt-5.4-nano and a static Hello World prompt. """ client = OrchestrationConfigClient() spec = OrchestrationConfig( @@ -79,7 +79,7 @@ def create_orchestration_config(): UserMessage(content="Hello, World!"), ] ), - model=LLMModelDetails(name="gpt-4o-mini"), + model=LLMModelDetails(name="gpt-5.4-nano"), ) ) ) diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 27e3cbcf..0ea17680 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -1,7 +1,12 @@ +from pathlib import Path + +from dotenv import load_dotenv from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -from sample_code import amazon, core, google, grounding, langchain_orchestration, openai, orchestration, prompt_registry, sap_rpt +load_dotenv(Path(__file__).parent / ".env", override=True) + +from sample_code import amazon, core, google, grounding, langchain_openai, langchain_orchestration, openai, orchestration, prompt_registry, sap_rpt app = FastAPI(title="SAP AI Core Python SDK Sample Application") @@ -46,14 +51,22 @@ async def health(): app.get("/amazon/converse")(amazon.converse) # LangChain -app.get("/langchain/chat-completion")(langchain_orchestration.init_llm_chat_completion) -app.get("/langchain/embedding")(langchain_orchestration.init_embedding) -app.get("/langchain/chat-completion-with-fallback")(langchain_orchestration.invoke_chain_with_fallback_configs) -app.get("/langchain/dynamic-model-agent")(langchain_orchestration.invoke_dynamic_model_agent) -app.get("/langchain/tool-chain")(langchain_orchestration.tool_chain) -app.get("/langchain/structured-output")(langchain_orchestration.structured_output) -app.get("/langchain/langgraph/chat-completion")(langchain_orchestration.langgraph_chat_completion) -app.get("/langchain/langgraph/chat-completion-stream")(langchain_orchestration.langgraph_chat_completion_stream) +app.get("/langchain/invoke")(langchain_openai.invoke) +app.get("/langchain/invoke_chain")(langchain_openai.invoke_chain) +app.get("/langchain/structured-output-json-schema")(langchain_openai.invoke_with_structured_output_json_schema) +app.get("/langchain/tool-chain")(langchain_openai.invoke_tool_chain) +app.get("/langchain/rag-chain")(langchain_openai.invoke_rag_chain) +app.get("/langchain/stream-chain")(langchain_openai.stream_chain) + +# LangChain Orchestration +app.get("/langchain-orchestration/invoke-chain")(langchain_orchestration.invoke_chain) +app.get("/langchain-orchestration/invoke-chain-input-filter")(langchain_orchestration.invoke_chain_with_input_filter) +app.get("/langchain-orchestration/invoke-chain-output-filter")(langchain_orchestration.invoke_chain_with_output_filter) +app.get("/langchain-orchestration/invoke-chain-masking")(langchain_orchestration.invoke_chain_with_masking) +app.get("/langchain-orchestration/stream-chain")(langchain_orchestration.stream_chain) +app.get("/langchain-orchestration/invoke-chain-fallback")(langchain_orchestration.invoke_chain_with_fallback) +app.get("/langchain-orchestration/stream-chain-fallback")(langchain_orchestration.stream_chain_with_fallback) +app.get("/langchain-orchestration/tool-chain")(langchain_orchestration.invoke_tool_chain) # SAP RPT-1 app.get("/sap-rpt/predict-by-rows")(sap_rpt.predict_by_rows) From 990501356f21545eaa4f3f654fdd2d2a78571a0d Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Thu, 17 Sep 2026 16:03:34 +0200 Subject: [PATCH 04/14] cleaning --- sample-code/sample_code/grounding.py | 8 ++--- .../sample_code/langchain_orchestration.py | 20 +++-------- sample-code/sample_code/prompt_registry.py | 35 +++++++++++++++---- sample-code/sample_code/sap_rpt.py | 9 +++++ 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/sample-code/sample_code/grounding.py b/sample-code/sample_code/grounding.py index 3a131890..3d1361e4 100644 --- a/sample-code/sample_code/grounding.py +++ b/sample-code/sample_code/grounding.py @@ -25,7 +25,7 @@ def get_collections(): List all vector collections available to the tenant. Returns: - JSON object containing the list of collections. + List of available collections. """ client = VectorAPIClient() return client.get_collections() @@ -36,7 +36,7 @@ def create_collection(): Create a new vector collection with a text-embedding model. Returns: - JSON object containing the ID of the created collection. + The created collection. """ client = VectorAPIClient() return client.create_collection( @@ -70,7 +70,7 @@ def create_documents(collection_id: str): collection_id: The ID of the collection to add documents to. Returns: - JSON object containing the created document IDs. + The created documents. """ client = VectorAPIClient() return client.create_documents( @@ -105,7 +105,7 @@ def get_pipelines(): List all document vectorization pipelines configured for the tenant. Returns: - JSON object containing the list of pipelines with their IDs and types. + List of configured pipelines. """ client = PipelineAPIClient() return client.get_pipelines() diff --git a/sample-code/sample_code/langchain_orchestration.py b/sample-code/sample_code/langchain_orchestration.py index a4aa9bd4..72f8b4b8 100644 --- a/sample-code/sample_code/langchain_orchestration.py +++ b/sample-code/sample_code/langchain_orchestration.py @@ -8,8 +8,6 @@ DPIMethodConstant, DPIStandardEntity, FilteringModuleConfig, - FunctionObject, - FunctionTool, GlobalStreamOptions, InputFiltering, LLMModelDetails, @@ -29,9 +27,6 @@ function_tool, ) from fastapi.responses import StreamingResponse -from gen_ai_hub.proxy.langchain.openai import ChatOpenAI -from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import END, START, MessagesState, StateGraph def invoke_chain() -> str: """ @@ -96,14 +91,8 @@ def invoke_chain_with_output_filter() -> str: """ Invoke the Orchestration Service with an Azure content safety output filter. - Uses gpt-5.4-nano which will comply with the prompt and generate - content that the output filter then blocks, leaving choices[0].message.content empty. - Output filtering does NOT raise an error — it silently empties the response content. - Returns: - A message confirming the output was filtered. - Raises: - RuntimeError: If the output was not filtered as expected. + The model response as a string, empty if the output was filtered. """ filtering = FilteringModuleConfig( output=OutputFiltering( @@ -141,11 +130,10 @@ def invoke_chain_with_masking() -> str: Invoke the Orchestration Service with DPI pseudonymization masking. PII (name, address, email, phone, date) in the prompt is replaced with - pseudonyms before being sent to the model. Returns both the masked prompt - (from intermediate results) and the final model response so masking is visible. + pseudonyms before being sent to the model. Returns: - Dict with 'masked_input' (pseudonymized prompt) and 'result' (model response). + The model response as a string. """ masking = MaskingModuleConfig( providers=[ @@ -170,7 +158,7 @@ def invoke_chain_with_masking() -> str: prompt=Template( template=[ UserMessage( - content="Generate email 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." + content="Generate email that shows the contact info for Jane Doe, born on 1975-03-05, living at 10 Downing Street, with email 'jane.doe@mailprovider.com' and phone number +4902044123221." ) ] ), diff --git a/sample-code/sample_code/prompt_registry.py b/sample-code/sample_code/prompt_registry.py index 688ec939..b7cba8fa 100644 --- a/sample-code/sample_code/prompt_registry.py +++ b/sample-code/sample_code/prompt_registry.py @@ -16,6 +16,9 @@ def create_prompt_template(): Create a prompt template with a user-input placeholder. The placeholder {{?user_input}} will be substituted at runtime via fill_prompt_template. + + Returns: + The created prompt template. """ client = PromptTemplateClient() spec = PromptTemplateSpec( @@ -39,26 +42,38 @@ def fill_prompt_template(): Replaces the {{?user_input}} placeholder in the template with a concrete question. Returns: - JSON object containing the filled prompt messages. + The filled prompt response. """ client = PromptTemplateClient() - response = client.fill_prompt_template( + return client.fill_prompt_template( scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION, input_params={"user_input": "What are the main features of SAP BTP?"}, ) - return {"result": [msg.model_dump() for msg in response.parsed_prompt]} def get_prompt_templates(): - """List all prompt templates matching the scenario/name/version filter.""" + """ + List all prompt templates matching the scenario/name/version filter. + + Returns: + List of matching prompt templates. + """ client = PromptTemplateClient() return client.get_prompt_templates(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION) def delete_prompt_template(template_id: str): - """Delete a prompt template by its ID.""" + """ + Delete a prompt template by its ID. + + Args: + template_id: The ID of the prompt template to delete. + + Returns: + HTTP response confirming deletion with HTTP status code 204. + """ client = PromptTemplateClient() return client.delete_prompt_template_by_id(template_id) @@ -68,6 +83,9 @@ def create_orchestration_config(): Create an orchestration config that bundles an LLM and a prompt template. The config references gpt-5.4-nano and a static Hello World prompt. + + Returns: + The created orchestration config. """ client = OrchestrationConfigClient() spec = OrchestrationConfig( @@ -92,7 +110,12 @@ def create_orchestration_config(): def get_orchestration_configs(): - """List orchestration configs matching the scenario/name/version filter.""" + """ + List orchestration configs matching the scenario/name/version filter. + + Returns: + List of matching orchestration configs. + """ client = OrchestrationConfigClient() return client.get_orchestration_configs( scenario=SCENARIO, diff --git a/sample-code/sample_code/sap_rpt.py b/sample-code/sample_code/sap_rpt.py index 565fcfb7..74ea8f08 100644 --- a/sample-code/sample_code/sap_rpt.py +++ b/sample-code/sample_code/sap_rpt.py @@ -48,6 +48,9 @@ def predict_by_rows(): Context rows supply known COSTCENTER values; the query row marked with "[PREDICT]" receives a predicted classification. + + Returns: + The prediction result. """ client = RPTClient() body = RPTRequest( @@ -72,6 +75,9 @@ def predict_by_columns(): Classify a target column using column-oriented input data. Equivalent to predict_by_rows but uses the columns format instead of rows. + + Returns: + The prediction result. """ client = RPTClient() body = RPTRequest( @@ -95,6 +101,9 @@ def regression(): Predict a numeric target column (regression). Rows with "[PREDICT]" in DISCOUNT_RATE receive a predicted numeric value. + + Returns: + The prediction result. """ client = RPTClient() body = RPTRequest( From 6a6c180cd9979393ecff8abfe07878c1593edfab Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Thu, 17 Sep 2026 16:25:41 +0200 Subject: [PATCH 05/14] cleaning --- packages/gen/gen_ai_hub/orchestration_v2/models/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/message.py b/packages/gen/gen_ai_hub/orchestration_v2/models/message.py index be5c03be..7a7fbe35 100644 --- a/packages/gen/gen_ai_hub/orchestration_v2/models/message.py +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/message.py @@ -197,7 +197,7 @@ class ResponseChatMessage(BaseModel): reasoning_content: A list of reasoning content blocks. """ role: Role = Role.ASSISTANT - content: Optional[str] = None + content: str refusal: Optional[str] = None tool_calls: Optional[List[MessageToolCall]] = None reasoning_content: Optional[List[ReasoningBlock]] = None From 8e07b9a440ac797d0c7ffecb7995baf0b5ebd44a Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Thu, 17 Sep 2026 17:12:00 +0200 Subject: [PATCH 06/14] cleaning --- sample-code/sample_code/langchain_openai.py | 2 ++ sample-code/sample_code/langchain_orchestration.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/sample-code/sample_code/langchain_openai.py b/sample-code/sample_code/langchain_openai.py index 8db645e3..002912a3 100644 --- a/sample-code/sample_code/langchain_openai.py +++ b/sample-code/sample_code/langchain_openai.py @@ -18,6 +18,7 @@ class SampleSchema(BaseModel): content: str language: str + def invoke(): """ Ask GPT about the capital of Germany. @@ -30,6 +31,7 @@ def invoke(): parser = StrOutputParser() return parser.invoke(response) + def invoke_chain(): """ Chain a prompt template, LLM, and output parser to answer in German. diff --git a/sample-code/sample_code/langchain_orchestration.py b/sample-code/sample_code/langchain_orchestration.py index 72f8b4b8..2baa8484 100644 --- a/sample-code/sample_code/langchain_orchestration.py +++ b/sample-code/sample_code/langchain_orchestration.py @@ -28,6 +28,7 @@ ) from fastapi.responses import StreamingResponse + def invoke_chain() -> str: """ Invoke the Orchestration Service with gpt-5.4-nano and return the response as a string. @@ -172,6 +173,7 @@ def invoke_chain_with_masking() -> str: service.close_http_connection() return result.final_result.choices[0].message.content + def invoke_chain_with_fallback() -> str: """ Invoke the Orchestration Service with a fallback model. @@ -203,6 +205,7 @@ def invoke_chain_with_fallback() -> str: service.close_http_connection() return result.final_result.choices[0].message.content + def stream_chain() -> StreamingResponse: """ Stream a response from the Orchestration Service token by token. From 8687d18ec2a0c7761534eb81f11c93d23d3b5ebb Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Thu, 17 Sep 2026 18:05:46 +0200 Subject: [PATCH 07/14] keep consistancy of the model --- sample-code/sample_code/orchestration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-code/sample_code/orchestration.py b/sample-code/sample_code/orchestration.py index a5766a47..377e1d08 100644 --- a/sample-code/sample_code/orchestration.py +++ b/sample-code/sample_code/orchestration.py @@ -239,7 +239,7 @@ def completion_with_fallback(): ) ] ), - model=LLMModelDetails(name="anthropic--claude-4.5-haiku"), + model=LLMModelDetails(name="anthropic--claude-4.6-sonnet"), ) ), ] @@ -447,7 +447,7 @@ def output_filtering(): ) ] ), - model=LLMModelDetails(name="anthropic--claude-4.5-haiku"), + model=LLMModelDetails(name="anthropic--claude-4.6-sonnet"), ), filtering=content_filter_config, ) From e3586166743173944428291896fbe9f263daeec3 Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Fri, 18 Sep 2026 15:26:09 +0200 Subject: [PATCH 08/14] delete the langchain_orchestration file --- .../sample_code/langchain_orchestration.py | 322 ------------------ 1 file changed, 322 deletions(-) delete mode 100644 sample-code/sample_code/langchain_orchestration.py diff --git a/sample-code/sample_code/langchain_orchestration.py b/sample-code/sample_code/langchain_orchestration.py deleted file mode 100644 index 2baa8484..00000000 --- a/sample-code/sample_code/langchain_orchestration.py +++ /dev/null @@ -1,322 +0,0 @@ -from gen_ai_hub.orchestration_v2 import ( - AzureContentSafetyInput, - AzureContentSafetyInputFilterConfig, - AzureContentSafetyOutput, - AzureContentSafetyOutputFilterConfig, - AzureThreshold, - DPICustomEntity, - DPIMethodConstant, - DPIStandardEntity, - FilteringModuleConfig, - GlobalStreamOptions, - InputFiltering, - LLMModelDetails, - MaskingMethod, - MaskingModuleConfig, - MaskingProviderConfig, - ModuleConfig, - OrchestrationConfig, - OrchestrationService, - OutputFiltering, - ProfileEntity, - PromptTemplatingModuleConfig, - SystemMessage, - Template, - ToolChatMessage, - UserMessage, - function_tool, -) -from fastapi.responses import StreamingResponse - - -def invoke_chain() -> str: - """ - Invoke the Orchestration Service with gpt-5.4-nano and return the response as a string. - - Returns: - The model response as a string. - """ - config = OrchestrationConfig( - modules=ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template( - template=[UserMessage(content="Tell me about SAP AI SDK")] - ), - model=LLMModelDetails(name="gpt-5.4-nano"), - ) - ) - ) - service = OrchestrationService(config=config) - result = service.run() - service.close_http_connection() - return result.final_result.choices[0].message.content - - -def invoke_chain_with_input_filter() -> str: - """ - Invoke the Orchestration Service with an Azure content safety input filter. - - Returns: - The model response as a string. - """ - filtering = FilteringModuleConfig( - input=InputFiltering( - filters=[ - AzureContentSafetyInputFilterConfig( - config=AzureContentSafetyInput( - hate=AzureThreshold.ALLOW_SAFE, - violence=AzureThreshold.ALLOW_SAFE, - self_harm=AzureThreshold.ALLOW_SAFE, - sexual=AzureThreshold.ALLOW_SAFE, - ) - ) - ] - ) - ) - config = OrchestrationConfig( - modules=ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template(template=[UserMessage(content="Tell me about the way to kill myself.")]), - model=LLMModelDetails(name="gpt-5.4-nano"), - ), - filtering=filtering, - ) - ) - service = OrchestrationService(config=config) - result = service.run() - service.close_http_connection() - return result.final_result.choices[0].message.content - - -def invoke_chain_with_output_filter() -> str: - """ - Invoke the Orchestration Service with an Azure content safety output filter. - - Returns: - The model response as a string, empty if the output was filtered. - """ - filtering = FilteringModuleConfig( - output=OutputFiltering( - filters=[ - AzureContentSafetyOutputFilterConfig( - config=AzureContentSafetyOutput( - hate=AzureThreshold.ALLOW_SAFE, - violence=AzureThreshold.ALLOW_SAFE, - self_harm=AzureThreshold.ALLOW_SAFE, - sexual=AzureThreshold.ALLOW_SAFE, - ) - ) - ] - ) - ) - config = OrchestrationConfig( - modules=ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template( - template=[UserMessage(content="Please tell me 5 ways to kill myself.")] - ), - model=LLMModelDetails(name="gpt-5.4-nano"), - ), - filtering=filtering, - ) - ) - service = OrchestrationService(config=config) - result = service.run() - service.close_http_connection() - return result.final_result.choices[0].message.content - - -def invoke_chain_with_masking() -> str: - """ - Invoke the Orchestration Service with DPI pseudonymization masking. - - PII (name, address, email, phone, date) in the prompt is replaced with - pseudonyms before being sent to the model. - - Returns: - The model response as a string. - """ - masking = MaskingModuleConfig( - providers=[ - MaskingProviderConfig( - method=MaskingMethod.ANONYMIZATION, - 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 email that shows the contact info for Jane Doe, born on 1975-03-05, living at 10 Downing Street, with email 'jane.doe@mailprovider.com' and phone number +4902044123221." - ) - ] - ), - model=LLMModelDetails(name="gpt-5.4-nano"), - ), - masking=masking, - ) - ) - service = OrchestrationService(config=config) - result = service.run() - service.close_http_connection() - return result.final_result.choices[0].message.content - - -def invoke_chain_with_fallback() -> str: - """ - Invoke the Orchestration Service with a fallback model. - - The first ModuleConfig uses a non-existent model to trigger fallback; - the second uses anthropic--claude-4.6-sonnet as the backup. - - Returns: - The model response as a string. - """ - config = OrchestrationConfig( - modules=[ - ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), - model=LLMModelDetails(name="dummy-model"), - ) - ), - ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), - model=LLMModelDetails(name="anthropic--claude-4.6-sonnet"), - ) - ), - ] - ) - service = OrchestrationService(config=config) - result = service.run() - service.close_http_connection() - return result.final_result.choices[0].message.content - - -def stream_chain() -> StreamingResponse: - """ - Stream a response from the Orchestration Service token by token. - - Returns: - StreamingResponse yielding text chunks. - """ - config = OrchestrationConfig( - modules=ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template( - template=[UserMessage(content="Tell me about SAP AI SDK with 1000 words.")] - ), - model=LLMModelDetails(name="gpt-5.4-nano"), - ) - ), - stream=GlobalStreamOptions(enabled=True), - ) - service = OrchestrationService(config=config) - - def generate(): - for chunk in service.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 stream_chain_with_fallback() -> StreamingResponse: - """ - Stream a response from the Orchestration Service with a fallback model. - - Returns: - StreamingResponse yielding text chunks from the fallback model. - """ - config = OrchestrationConfig( - modules=[ - ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), - model=LLMModelDetails(name="dummy-model"), - ) - ), - ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template(template=[UserMessage(content="Tell me about SAP AI SDK")]), - model=LLMModelDetails(name="anthropic--claude-4.6-sonnet"), - ) - ), - ], - stream=GlobalStreamOptions(enabled=True), - ) - service = OrchestrationService(config=config) - - def generate(): - for chunk in service.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 invoke_tool_chain() -> str: - """ - Invoke a tool chain via the Orchestration Service. - - Binds a celsius_to_fahrenheit tool, executes the tool call triggered - by the model, then returns the final model response. - - Returns: - The final model response as a string after tool execution. - """ - @function_tool - def celsius_to_fahrenheit(celsius: float) -> str: - """Converts a temperature from Celsius to Fahrenheit.""" - fahrenheit = celsius * 9 / 5 + 32 - return f"{celsius}°C is {fahrenheit}°F" - - config = OrchestrationConfig( - modules=ModuleConfig( - prompt_templating=PromptTemplatingModuleConfig( - prompt=Template( - template=[ - SystemMessage(content="You are a helpful assistant that converts temperatures."), - UserMessage(content="What is 100 degrees Celsius in Fahrenheit?"), - ], - tools=[celsius_to_fahrenheit], - ), - model=LLMModelDetails(name="gpt-5.4-nano"), - ) - ) - ) - - service = OrchestrationService() - result = service.run(config=config) - tool_calls = result.final_result.choices[0].message.tool_calls - if not tool_calls: - raise RuntimeError("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: - tool_result = celsius_to_fahrenheit.execute(**tool_call.function.parse_arguments()) - history.append(ToolChatMessage(content=str(tool_result), tool_call_id=tool_call.id)) - - result = service.run(config=config, history=history) - service.close_http_connection() - return result.final_result.choices[0].message.content \ No newline at end of file From 72bc07091db39febb4f21a07ab38e4c8ff469ad6 Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Fri, 18 Sep 2026 15:30:19 +0200 Subject: [PATCH 09/14] delete the langchain_orchestration fastapi endpoints --- sample-code/sample_code/server.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 0ea17680..0990c79f 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -58,16 +58,6 @@ async def health(): app.get("/langchain/rag-chain")(langchain_openai.invoke_rag_chain) app.get("/langchain/stream-chain")(langchain_openai.stream_chain) -# LangChain Orchestration -app.get("/langchain-orchestration/invoke-chain")(langchain_orchestration.invoke_chain) -app.get("/langchain-orchestration/invoke-chain-input-filter")(langchain_orchestration.invoke_chain_with_input_filter) -app.get("/langchain-orchestration/invoke-chain-output-filter")(langchain_orchestration.invoke_chain_with_output_filter) -app.get("/langchain-orchestration/invoke-chain-masking")(langchain_orchestration.invoke_chain_with_masking) -app.get("/langchain-orchestration/stream-chain")(langchain_orchestration.stream_chain) -app.get("/langchain-orchestration/invoke-chain-fallback")(langchain_orchestration.invoke_chain_with_fallback) -app.get("/langchain-orchestration/stream-chain-fallback")(langchain_orchestration.stream_chain_with_fallback) -app.get("/langchain-orchestration/tool-chain")(langchain_orchestration.invoke_tool_chain) - # SAP RPT-1 app.get("/sap-rpt/predict-by-rows")(sap_rpt.predict_by_rows) app.get("/sap-rpt/predict-by-columns")(sap_rpt.predict_by_columns) From c2abe3f158c8914ad3517747c5ebca2832fb9736 Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Fri, 18 Sep 2026 15:40:16 +0200 Subject: [PATCH 10/14] remove langchain_orchestration from server --- sample-code/sample_code/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 0990c79f..18ae25a0 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -6,7 +6,7 @@ load_dotenv(Path(__file__).parent / ".env", override=True) -from sample_code import amazon, core, google, grounding, langchain_openai, langchain_orchestration, openai, orchestration, prompt_registry, sap_rpt +from sample_code import amazon, core, google, grounding, langchain_openai, openai, orchestration, prompt_registry, sap_rpt app = FastAPI(title="SAP AI Core Python SDK Sample Application") From 6e151127c1a6964f1854ea90c4525ae2a299f3e7 Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Mon, 21 Sep 2026 17:29:44 +0200 Subject: [PATCH 11/14] fix the sample code --- sample-code/sample_code/grounding.py | 19 +++--------- sample-code/sample_code/langchain_openai.py | 4 +-- sample-code/sample_code/prompt_registry.py | 32 +++++++++------------ sample-code/sample_code/sap_rpt.py | 8 ++---- sample-code/sample_code/server.py | 2 +- 5 files changed, 22 insertions(+), 43 deletions(-) diff --git a/sample-code/sample_code/grounding.py b/sample-code/sample_code/grounding.py index 3d1361e4..d32c8569 100644 --- a/sample-code/sample_code/grounding.py +++ b/sample-code/sample_code/grounding.py @@ -1,5 +1,3 @@ -from fastapi import Query - from gen_ai_hub.document_grounding.client import ( PipelineAPIClient, RetrievalAPIClient, @@ -111,31 +109,22 @@ def get_pipelines(): return client.get_pipelines() -def retrieval_documents( - query: str = "What are the key features of SAP BTP?", - data_repository_type: str = "vector", - data_repositories: list[str] = Query(default=["*"]), -): +def retrieval_documents(): """ Retrieve documents across data repositories. - Args: - query: Search query. - data_repository_type: Type of data repository. - data_repositories: List of data repository IDs to search in. - Returns: Search results. """ client = RetrievalAPIClient() return client.search( RetrievalSearchInput( - query=query, + query="What are the key features of SAP BTP?", filters=[ RetrievalSearchFilter( id="filter-1", - dataRepositoryType=data_repository_type, - dataRepositories=data_repositories, + dataRepositoryType="vector", + dataRepositories=["*"], searchConfiguration=RetrievalSearchConfiguration(maxChunkCount=1), ) ], diff --git a/sample-code/sample_code/langchain_openai.py b/sample-code/sample_code/langchain_openai.py index 002912a3..28d92564 100644 --- a/sample-code/sample_code/langchain_openai.py +++ b/sample-code/sample_code/langchain_openai.py @@ -1,5 +1,3 @@ -from collections.abc import AsyncGenerator - from fastapi.responses import StreamingResponse from langchain_core.messages import HumanMessage, ToolMessage from langchain_core.output_parsers import StrOutputParser @@ -146,7 +144,7 @@ def stream_chain() -> StreamingResponse: llm = ChatOpenAI(proxy_model_name="gpt-5.4-nano") messages = [HumanMessage(content="Write a 1000 word explanation about SAP AI SDK and its capabilities")] - async def generate() -> AsyncGenerator[str, None]: + async def generate(): async for chunk in llm.astream(messages): if chunk.content: yield chunk.content diff --git a/sample-code/sample_code/prompt_registry.py b/sample-code/sample_code/prompt_registry.py index b7cba8fa..d23df1cc 100644 --- a/sample-code/sample_code/prompt_registry.py +++ b/sample-code/sample_code/prompt_registry.py @@ -5,12 +5,6 @@ from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage from gen_ai_hub.orchestration_v2.models.template import PromptTemplatingModuleConfig, Template -SCENARIO = "my-scenario" -TEMPLATE_NAME = "my-template" -VERSION = "1.0.0" -CONFIG_NAME = "my-orchestration-config" - - def create_prompt_template(): """ Create a prompt template with a user-input placeholder. @@ -28,9 +22,9 @@ def create_prompt_template(): ] ) return client.create_prompt_template( - scenario=SCENARIO, - name=TEMPLATE_NAME, - version=VERSION, + scenario="my-scenario", + name="my-template", + version="1.0.0", prompt_template_spec=spec, ) @@ -46,9 +40,9 @@ def fill_prompt_template(): """ client = PromptTemplateClient() return client.fill_prompt_template( - scenario=SCENARIO, - name=TEMPLATE_NAME, - version=VERSION, + scenario="my-scenario", + name="my-template", + version="1.0.0", input_params={"user_input": "What are the main features of SAP BTP?"}, ) @@ -61,7 +55,7 @@ def get_prompt_templates(): List of matching prompt templates. """ client = PromptTemplateClient() - return client.get_prompt_templates(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION) + return client.get_prompt_templates(scenario="my-scenario", name="my-template", version="1.0.0") def delete_prompt_template(template_id: str): @@ -102,9 +96,9 @@ def create_orchestration_config(): ) ) return client.create_orchestration_config( - scenario=SCENARIO, - name=CONFIG_NAME, - version=VERSION, + scenario="my-scenario", + name="my-orchestration-config", + version="1.0.0", spec=spec, ) @@ -118,8 +112,8 @@ def get_orchestration_configs(): """ client = OrchestrationConfigClient() return client.get_orchestration_configs( - scenario=SCENARIO, - name=CONFIG_NAME, - version=VERSION, + scenario="my-scenario", + name="my-orchestration-config", + version="1.0.0", include_spec=True, ) diff --git a/sample-code/sample_code/sap_rpt.py b/sample-code/sample_code/sap_rpt.py index 74ea8f08..6e47db0d 100644 --- a/sample-code/sample_code/sap_rpt.py +++ b/sample-code/sample_code/sap_rpt.py @@ -1,8 +1,6 @@ from gen_ai_hub.proxy.native.sap.client import RPTClient from gen_ai_hub.proxy.native.sap.models import DataType, PredictionConfig, RPTRequest, TargetColumn -MODEL_NAME = "sap-rpt-1-small" - CLASSIFICATION_SCHEMA = { "PRODUCT": DataType(dtype="string"), "PRICE": DataType(dtype="numeric"), @@ -67,7 +65,7 @@ def predict_by_rows(): rows=CLASSIFICATION_ROWS, data_schema=CLASSIFICATION_SCHEMA, ) - return client.predict(body=body, model_name=MODEL_NAME) + return client.predict(body=body, model_name="sap-rpt-1-small") def predict_by_columns(): @@ -93,7 +91,7 @@ def predict_by_columns(): columns=CLASSIFICATION_COLUMNS, data_schema=CLASSIFICATION_SCHEMA, ) - return client.predict(body=body, model_name=MODEL_NAME) + return client.predict(body=body, model_name="sap-rpt-1-small") def regression(): @@ -114,4 +112,4 @@ def regression(): rows=REGRESSION_ROWS, data_schema=REGRESSION_SCHEMA, ) - return client.predict(body=body, model_name=MODEL_NAME) \ No newline at end of file + return client.predict(body=body, model_name="sap-rpt-1-small") \ No newline at end of file diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 18ae25a0..6e7cf9d6 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -58,7 +58,7 @@ async def health(): app.get("/langchain/rag-chain")(langchain_openai.invoke_rag_chain) app.get("/langchain/stream-chain")(langchain_openai.stream_chain) -# SAP RPT-1 +# SAP RPT app.get("/sap-rpt/predict-by-rows")(sap_rpt.predict_by_rows) app.get("/sap-rpt/predict-by-columns")(sap_rpt.predict_by_columns) app.get("/sap-rpt/predict-regression")(sap_rpt.regression) From 160dcce39f5dc9f48859773de2d88b6e0165761e Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Tue, 22 Sep 2026 08:06:37 +0200 Subject: [PATCH 12/14] update sample code --- sample-code/README.md | 1 + sample-code/sample_code/server.py | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/sample-code/README.md b/sample-code/README.md index 6b9ffe89..fc34764f 100644 --- a/sample-code/README.md +++ b/sample-code/README.md @@ -14,6 +14,7 @@ Before running the application, ensure the following prerequisites are met: - `text-embedding-3-small` - `anthropic--claude-4.6-sonnet` - `gemini-3.5-flash` + - `sap-rpt-1-small` ## Local Deployment diff --git a/sample-code/sample_code/server.py b/sample-code/sample_code/server.py index 6e7cf9d6..3e956d1e 100644 --- a/sample-code/sample_code/server.py +++ b/sample-code/sample_code/server.py @@ -1,11 +1,6 @@ -from pathlib import Path - -from dotenv import load_dotenv from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -load_dotenv(Path(__file__).parent / ".env", override=True) - from sample_code import amazon, core, google, grounding, langchain_openai, openai, orchestration, prompt_registry, sap_rpt app = FastAPI(title="SAP AI Core Python SDK Sample Application") From eda034c953461407144e9d2bf460ca80131ad857 Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Wed, 23 Sep 2026 09:31:34 +0200 Subject: [PATCH 13/14] update the sample code pyproject file --- sample-code/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-code/pyproject.toml b/sample-code/pyproject.toml index 9d1d37f6..22fd8edd 100644 --- a/sample-code/pyproject.toml +++ b/sample-code/pyproject.toml @@ -8,7 +8,7 @@ license-files = ["LICENSE"] requires-python = ">=3.10" dependencies = [ "fastapi>=0.141.1", - "sap-ai-sdk-gen", + "sap-ai-sdk-gen[all]", "uvicorn>=0.52.1", ] From bfa414e21aad857c932b3550f6f09c9459262f5a Mon Sep 17 00:00:00 2001 From: Sicheng Dong Date: Wed, 23 Sep 2026 09:39:02 +0200 Subject: [PATCH 14/14] update uv lock --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 86122e41..e8cf162d 100644 --- a/uv.lock +++ b/uv.lock @@ -4089,14 +4089,14 @@ version = "0.1.0" source = { editable = "sample-code" } dependencies = [ { name = "fastapi" }, - { name = "sap-ai-sdk-gen" }, + { name = "sap-ai-sdk-gen", extra = ["all"] }, { name = "uvicorn" }, ] [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.141.1" }, - { name = "sap-ai-sdk-gen", editable = "packages/gen" }, + { name = "sap-ai-sdk-gen", extras = ["all"], editable = "packages/gen" }, { name = "uvicorn", specifier = ">=0.52.1" }, ]