SuperAPI AI Directory

cap-llm-plugin

Agents & models

View repository

Updated 10-16 09:47

About this skill

CAP LLM Plugin — AI Integration Best Practices

Primary reference: https://cap.cloud.sap/docs/plugins/#ai SAP AI SDK: https://sap.github.io/ai-sdk/

@cap-js/ai — Built-in AI Plugin (recommended for field recommendations)

For UI field recommendations and standard AI Core integration, use the first-party plugin:

npm install @cap-js/ai

Fields with @Common.ValueList or @cds.odata.valuelist annotations automatically get AI-powered recommendations in Fiori draft-enabled UIs — no custom handler needed:

annotate Books with {
  genre @Common.ValueList: {
    CollectionPath: 'Genres',
    Parameters: [{ $Type: 'Common.ValueListParameterInOut',
      ValueListProperty: 'code', LocalDataProperty: genre_code }]
  }
}

For custom LLM integration (RAG, chat completions, orchestration), use the SAP AI SDK described below.

Setup: SAP AI SDK for CAP (recommended, 2025+)

npm install @sap-ai-sdk/langchain @sap-ai-sdk/foundation-models

Bind aicore service in mta.yaml:

- name: my-cap-app-aicore
  type: org.cloudfoundry.managed-service
  parameters:
    service: aicore
    service-plan: extended

Chat completion (OpenAI-compatible via AI Core)

const { AzureOpenAiChatClient } = require('@sap-ai-sdk/foundation-models')

module.exports = class AssistantService extends cds.ApplicationService {
  async init() {
    this.on('askQuestion', this.onAskQuestion)
    return super.init()
  }

  async onAskQuestion(req) {
    const { question, context } = req.data

    const client = new AzureOpenAiChatClient({ modelName: 'gpt-4o' })

    const response = await client.invoke([
      {
        role: 'system',
        content: 'You are a helpful assistant for SAP procurement processes. ' +
                 'Answer based only on the provided context.'
      },
      {
        role: 'user',
        content: `Context:\n${context}\n\nQuestion: ${question}`
      }
    ])

    return { answer: response.content }
  }
}

RAG: vector embeddings with HANA Cloud Vector Engine

Note (April 2026+): The Vector type is now also supported on H2, SQLite, and PostgreSQL for local development — not just HANA Cloud. Use SQLite locally, HANA in production without model changes.

// Schema for document chunks with vector embeddings
entity DocumentChunks : cuid {
  content    : LargeString;
  sourceDoc  : String(500);
  embedding  : Vector(1536);  // dimension depends on embedding model
}
const { AzureOpenAiEmbeddingClient } = require('@sap-ai-sdk/foundation-models')

async function embedAndStore(text, sourceDoc) {
  const client = new AzureOpenAiEmbeddingClient({ modelName: 'text-embedding-3-small' })
  const [vector] = await client.embedDocuments([text])

  await INSERT.into(DocumentChunks).entries({
    content:   text,
    sourceDoc: sourceDoc,
    embedding: vector
  })
}

async function retrieveRelevant(question, topK = 5) {
  const client = new AzureOpenAiEmbeddingClient({ modelName: 'text-embedding-3-small' })
  const [queryVector] = await client.embedDocuments([question])

  // HANA Cloud cosine similarity search
  return cds.db.run(
    `SELECT TOP ${topK} content, sourceDoc,
            COSINE_SIMILARITY(embedding, TO_REAL_VECTOR(?)) AS score
     FROM MY_DOCUMENTCHUNKS
     ORDER BY score DESC`,
    [JSON.stringify(queryVector)]
  )
}

Orchestration with LangChain (via SAP AI SDK)

const { ChatPromptTemplate }       = require('@langchain/core/prompts')
const { StringOutputParser }       = require('@langchain/core/output_parsers')
const { AzureOpenAiChatClient }    = require('@sap-ai-sdk/langchain')

async function runRAGChain(question) {
  const llm = new AzureOpenAiChatClient({ modelName: 'gpt-4o' })

  const chunks = await retrieveRelevant(question)
  const context = chunks.map(c => c.CONTENT).join('\n\n---\n\n')

  const prompt = ChatPromptTemplate.fromMessages([
    ['system', 'Answer the question using only this context:\n{context}'],
    ['human', '{question}']
  ])

  const chain = prompt.pipe(llm).pipe(new StringOutputParser())
  return chain.invoke({ context, question })
}

Streaming responses (SSE)

this.on('streamAnswer', async (req, res) => {
  const client = new AzureOpenAiChatClient({ modelName: 'gpt-4o' })

  // Set SSE headers
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')

  const stream = await client.stream([{ role: 'user', content: req.data.question }])

  for await (const chunk of stream) {
    res.write(`data: ${JSON.stringify({ token: chunk.content })}\n\n`)
  }
  res.write('data: [DONE]\n\n')
  res.end()
})

Token budget management

function buildPrompt(systemPrompt, context, question, maxContextTokens = 3000) {
  // Rough estimate: 1 token ≈ 4 chars
  const budget = maxContextTokens * 4
  const truncated = context.length > budget
    ? context.slice(0, budget) + '\n[...context truncated...]'
    : context

  return [
    { role: 'system', content: systemPrompt },
    { role: 'user',   content: `Context:\n${truncated}\n\nQuestion: ${question}` }
  ]
}

Local development mock

{
  "cds": {
    "requires": {
      "aicore": {
        "[development]": {
          "kind": "aicore",
          "credentials": {
            "clientid":     "...",
            "clientsecret": "...",
            "url":          "https://YOUR_TENANT.authentication.eu10.hana.ondemand.com",
            "serviceurls":  { "AI_API_URL": "https://api.ai.eu10.aws.ml.hana.ondemand.com" }
          }
        }
      }
    }
  }
}

Use a .env file (never commit) with real AI Core credentials for local testing.

Common mistakes to avoid

  • ❌ Sending raw user input directly to the LLM without sanitization (prompt injection risk)
  • ❌ Storing embeddings as JSON strings instead of using HANA Vector type
  • ❌ Re-embedding the same documents on every request — precompute and cache
  • ❌ Not limiting context size — long prompts = slow responses + high cost
  • ❌ Exposing LLM errors verbatim to end users — raw model output or stack traces can reveal internal prompt instructions or configuration details
  • ❌ Forgetting to bind the aicore service instance in mta.yaml before deploy
  • ❌ Using synchronous calls for long-running LLM tasks — use background jobs or streaming