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/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", ] diff --git a/sample-code/sample_code/grounding.py b/sample-code/sample_code/grounding.py new file mode 100644 index 00000000..d32c8569 --- /dev/null +++ b/sample-code/sample_code/grounding.py @@ -0,0 +1,132 @@ +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: + List of available collections. + """ + client = VectorAPIClient() + return client.get_collections() + + +def create_collection(): + """ + Create a new vector collection with a text-embedding model. + + Returns: + The created collection. + """ + client = VectorAPIClient() + return client.create_collection( + CollectionCreateRequest( + title="sample-collection", + embeddingConfig=EmbeddingConfig(modelName="text-embedding-3-small"), + 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: + The created documents. + """ + 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: + List of configured pipelines. + """ + client = PipelineAPIClient() + return client.get_pipelines() + + +def retrieval_documents(): + """ + Retrieve documents across data repositories. + + Returns: + Search results. + """ + client = RetrievalAPIClient() + return client.search( + RetrievalSearchInput( + query="What are the key features of SAP BTP?", + filters=[ + RetrievalSearchFilter( + id="filter-1", + 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 new file mode 100644 index 00000000..28d92564 --- /dev/null +++ b/sample-code/sample_code/langchain_openai.py @@ -0,0 +1,153 @@ +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(): + 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/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, ) diff --git a/sample-code/sample_code/prompt_registry.py b/sample-code/sample_code/prompt_registry.py new file mode 100644 index 00000000..d23df1cc --- /dev/null +++ b/sample-code/sample_code/prompt_registry.py @@ -0,0 +1,119 @@ +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 + +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( + template=[ + PromptTemplate(role="system", content="You are a helpful assistant."), + PromptTemplate(role="user", content="{{?user_input}}"), + ] + ) + return client.create_prompt_template( + scenario="my-scenario", + name="my-template", + version="1.0.0", + prompt_template_spec=spec, + ) + + +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: + The filled prompt response. + """ + client = PromptTemplateClient() + return client.fill_prompt_template( + scenario="my-scenario", + name="my-template", + version="1.0.0", + input_params={"user_input": "What are the main features of SAP BTP?"}, + ) + + +def get_prompt_templates(): + """ + List all prompt templates matching the scenario/name/version filter. + + Returns: + List of matching prompt templates. + """ + client = PromptTemplateClient() + return client.get_prompt_templates(scenario="my-scenario", name="my-template", version="1.0.0") + + +def delete_prompt_template(template_id: str): + """ + 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) + + +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( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template( + template=[ + SystemMessage(content="You are a helpful assistant."), + UserMessage(content="Hello, World!"), + ] + ), + model=LLMModelDetails(name="gpt-5.4-nano"), + ) + ) + ) + return client.create_orchestration_config( + scenario="my-scenario", + name="my-orchestration-config", + version="1.0.0", + spec=spec, + ) + + +def get_orchestration_configs(): + """ + List orchestration configs matching the scenario/name/version filter. + + Returns: + List of matching orchestration configs. + """ + client = OrchestrationConfigClient() + return client.get_orchestration_configs( + 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 new file mode 100644 index 00000000..6e47db0d --- /dev/null +++ b/sample-code/sample_code/sap_rpt.py @@ -0,0 +1,115 @@ +from gen_ai_hub.proxy.native.sap.client import RPTClient +from gen_ai_hub.proxy.native.sap.models import DataType, PredictionConfig, RPTRequest, TargetColumn + +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]"}, +] + +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(): + """ + 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. + + Returns: + The prediction result. + """ + 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="sap-rpt-1-small") + + +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( + 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="sap-rpt-1-small") + + +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( + 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="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 4e011e3b..3e956d1e 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_openai, openai, orchestration, prompt_registry, sap_rpt app = FastAPI(title="SAP AI Core Python SDK Sample Application") @@ -45,6 +45,19 @@ async def health(): # Amazon/Anthropic app.get("/amazon/converse")(amazon.converse) +# LangChain +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) + +# 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) + # Orchestration app.get("/orchestration/completion")(orchestration.completion) app.get("/orchestration/completion-async")(orchestration.completion_async) @@ -68,3 +81,25 @@ 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.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 +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.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) + +# 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) diff --git a/uv.lock b/uv.lock index 5e398786..b2d10fdd 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" }, ]