Skip to content

Commit 855f70f

Browse files
committed
Set up api
1 parent 33ba431 commit 855f70f

26 files changed

+798
-171
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
# This workflow is used to build the API lambda
3+
# lambda function bootstrap binary that can be deployed to AWS Lambda.
4+
name: Build API Lambda
5+
6+
on:
7+
workflow_dispatch:
8+
workflow_call:
9+
inputs:
10+
ref:
11+
required: false
12+
type: string
13+
default: ${{ github.ref }}
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
env:
19+
BINARY_PATH: .artifacts/AwsLambda/release_linux-x64/bootstrap
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
ref: ${{ inputs.ref }}
24+
- name: Amazon Linux 2023 build
25+
run: |
26+
docker build . -t api-lambda:latest -f src/api/AwsLambda/DockerFile
27+
- name: Get bootstrap binary
28+
run: |
29+
docker cp $(docker create --name tc api-lambda:latest):/app/.artifacts/publish ./.artifacts && docker rm tc
30+
- name: Inspect bootstrap binary
31+
run: |
32+
tree .artifacts
33+
stat "${BINARY_PATH}"
34+
- name: Archive artifact
35+
id: upload-artifact
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: api-lambda-binary
39+
retention-days: 1
40+
if-no-files-found: error
41+
path: ${{ env.BINARY_PATH }}

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ jobs:
3232
- name: Validate Content Sources
3333
run: dotnet run --project src/tooling/docs-assembler -c release -- content-source validate
3434

35-
build-lambda:
35+
build-link-index-updater-lambda:
3636
uses: ./.github/workflows/build-link-index-updater-lambda.yml
37-
37+
38+
build-api-lambda:
39+
uses: ./.github/workflows/build-api-lambda.yml
40+
3841
npm:
3942
runs-on: ubuntu-latest
4043
defaults:

Directory.Packages.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</ItemGroup>
99
<!-- AWS -->
1010
<ItemGroup>
11+
<PackageVersion Include="Amazon.Lambda.AspNetCoreServer.Hosting" Version="1.9.0" />
1112
<PackageVersion Include="Amazon.Lambda.RuntimeSupport" Version="1.13.0" />
1213
<PackageVersion Include="Amazon.Lambda.Core" Version="2.5.1" />
1314
<PackageVersion Include="Amazon.Lambda.S3Events" Version="3.1.0" />
@@ -18,6 +19,7 @@
1819
<PackageVersion Include="AWSSDK.S3" Version="4.0.0.1" />
1920
<PackageVersion Include="FakeItEasy" Version="8.3.0" />
2021
<PackageVersion Include="Elastic.Ingest.Elasticsearch" Version="0.11.3" />
22+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
2123
<PackageVersion Include="Microsoft.OpenApi" Version="2.0.0-preview9" />
2224
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
2325
</ItemGroup>
@@ -70,4 +72,4 @@
7072
</PackageVersion>
7173
<PackageVersion Include="xunit.v3" Version="2.0.2" />
7274
</ItemGroup>
73-
</Project>
75+
</Project>

docs-builder.sln

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{6FAB56
119119
config\navigation.yml = config\navigation.yml
120120
EndProjectSection
121121
EndProject
122+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{B042CC78-5060-4091-B95A-79C71BA3908A}"
123+
EndProject
124+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\api\Core\Core.csproj", "{F30B90AD-1A01-4A6F-9699-809FA6875B22}"
125+
EndProject
126+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\api\Infrastructure\Infrastructure.csproj", "{AE3FC78E-167F-4B6E-88EC-84743EB748B7}"
127+
EndProject
128+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda", "src\api\AwsLambda\AwsLambda.csproj", "{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}"
129+
EndProject
122130
Global
123131
GlobalSection(SolutionConfigurationPlatforms) = preSolution
124132
Debug|Any CPU = Debug|Any CPU
@@ -204,6 +212,18 @@ Global
204212
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
205213
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
206214
{164F55EC-9412-4CD4-81AD-3598B57632A6}.Release|Any CPU.Build.0 = Release|Any CPU
215+
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
216+
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Debug|Any CPU.Build.0 = Debug|Any CPU
217+
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|Any CPU.ActiveCfg = Release|Any CPU
218+
{F30B90AD-1A01-4A6F-9699-809FA6875B22}.Release|Any CPU.Build.0 = Release|Any CPU
219+
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
220+
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
221+
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
222+
{AE3FC78E-167F-4B6E-88EC-84743EB748B7}.Release|Any CPU.Build.0 = Release|Any CPU
223+
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
224+
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
225+
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
226+
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE}.Release|Any CPU.Build.0 = Release|Any CPU
207227
EndGlobalSection
208228
GlobalSection(NestedProjects) = preSolution
209229
{4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
@@ -234,5 +254,9 @@ Global
234254
{89B83007-71E6-4B57-BA78-2544BFA476DB} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
235255
{111E7029-BB29-4039-9B45-04776798A8DD} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
236256
{164F55EC-9412-4CD4-81AD-3598B57632A6} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
257+
{B042CC78-5060-4091-B95A-79C71BA3908A} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
258+
{F30B90AD-1A01-4A6F-9699-809FA6875B22} = {B042CC78-5060-4091-B95A-79C71BA3908A}
259+
{AE3FC78E-167F-4B6E-88EC-84743EB748B7} = {B042CC78-5060-4091-B95A-79C71BA3908A}
260+
{C6A121C5-DEB1-4FCE-9140-AF144EA98EEE} = {B042CC78-5060-4091-B95A-79C71BA3908A}
237261
EndGlobalSection
238262
EndGlobal

docs/syntax/stepper.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ By default every step title is a link with a generated anchor. You can override
1212
:::::{stepper}
1313

1414
::::{step} Install
15-
First install the dependencies.
15+
First install the {{foo}} dependencies.
1616
```shell
1717
npm install
1818
```

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/useLlmGateway.ts

Lines changed: 69 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,12 @@ import { EventSourceMessage } from '@microsoft/fetch-event-source'
44
import { useEffect, useState, useRef, useCallback } from 'react'
55
import * as z from 'zod'
66

7-
export const LlmGatewayRequestSchema = z.object({
8-
userContext: z.object({
9-
userEmail: z.string(),
10-
}),
11-
platformContext: z.object({
12-
origin: z.literal('support_portal'),
13-
useCase: z.literal('support_assistant'),
14-
metadata: z.any(),
15-
}),
16-
input: z.array(
17-
z.object({
18-
role: z.string(),
19-
message: z.string(),
20-
})
21-
),
22-
threadId: z.string(),
7+
export const AskAiRequestSchema = z.object({
8+
message: z.string(),
9+
threadId: z.string().optional(),
2310
})
2411

25-
export type LlmGatewayRequest = z.infer<typeof LlmGatewayRequestSchema>
12+
export type AskAiRequest = z.infer<typeof AskAiRequestSchema>
2613

2714
const sharedAttributes = {
2815
timestamp: z.number(),
@@ -154,7 +141,7 @@ export const useLlmGateway = (props: Props): UseLlmGatewayResponse => {
154141
[processMessage]
155142
)
156143

157-
const { sendMessage, abort } = useFetchEventSource<LlmGatewayRequest>({
144+
const { sendMessage, abort } = useFetchEventSource<AskAiRequest>({
158145
apiEndpoint: '/chat',
159146
onMessage,
160147
onError: (error) => {
@@ -221,64 +208,70 @@ export const useLlmGateway = (props: Props): UseLlmGatewayResponse => {
221208
}
222209
}
223210

224-
function createLlmGatewayRequest(question: string, threadId?: string) {
225-
// TODO: we should move this to the backend so that the use cannot change this
226-
// Right now, the backend is a pure proxy to the LLM gateway
227-
return LlmGatewayRequestSchema.parse({
228-
userContext: {
229-
userEmail: `elastic-docs-v3@invalid`, // Random email (will be optional in the future)
230-
},
231-
platformContext: {
232-
origin: 'support_portal',
233-
useCase: 'support_assistant',
234-
metadata: {},
235-
},
236-
input: [
237-
{
238-
role: 'user',
239-
message: `
240-
# ROLE AND GOAL
241-
You are an expert AI assistant for the Elastic Stack (Elasticsearch, Kibana, Beats, Logstash, etc.). Your sole purpose is to answer user questions based *exclusively* on the provided context from the official Elastic Documentation.
242-
243-
# CRITICAL INSTRUCTION: SINGLE-SHOT INTERACTION
244-
This is a single-turn interaction. The user cannot reply to your answer for clarification. Therefore, your response MUST be final, self-contained, and as comprehensive as possible based on the provided context.
245-
Also, keep the response as short as possible, but do not truncate the context.
246-
247-
# RULES
248-
1. **Facts** Always do RAG search to find the relevant Elastic documentation.
249-
2. **Strictly Grounded Answers:** You MUST base your answer 100% on the information from the search results. Do not use any of your pre-trained knowledge or any information outside of this context.
250-
3. **Handle Ambiguity Gracefully:** Since you cannot ask clarifying questions, if the question is broad or ambiguous (e.g., "how to improve performance"), structure your answer to cover the different interpretations supported by the context.
251-
* Acknowledge the ambiguity. For example: "Your question about 'performance' can cover several areas. Based on the documentation, here are the key aspects:"
252-
* Organize the answer with clear headings for each aspect (e.g., "Indexing Performance," "Query Performance").
253-
* But if there is a similar or related topic in the docs you can mention it and link to it.
254-
4. **Direct Answer First:** If the context directly and sufficiently answers a specific question, provide a clear, comprehensive, and well-structured answer.
255-
* Use Markdown for formatting (e.g., code blocks for configurations, bullet points for lists).
256-
* Use LaTeX for mathematical or scientific notations where appropriate (e.g., \`$E = mc^2$\`).
257-
* Make the answer as complete as possible, as this is the user's only response.
258-
* Keep the answer short and concise. We want to link users to the Elastic Documentation to find more information.
259-
5. **Handling Incomplete Answers:** If the context contains relevant information but does not fully answer the question, you MUST follow this procedure:
260-
* Start by explicitly stating that you could not find a complete answer.
261-
* Then, summarize the related information you *did* find in the context, explaining how it might be helpful.
262-
6. **Handling No Answer:** If the context is empty or completely irrelevant to the question, you MUST respond with the following, and nothing else:
263-
I was unable to find an answer to your question in the Elastic Documentation.
264-
265-
For further assistance, you may want to:
266-
* Ask the community of experts at **discuss.elastic.co**.
267-
* If you have an Elastic subscription, contact our support engineers at **support.elastic.co**."
268-
7. If you are 100% sure that something is not supported by Elastic, then say so.
269-
8. **Tone:** Your tone should be helpful, professional, and confident. It is better to provide no answer (Rule #5) than an incorrect one.
270-
* Assume that the user is using Elastic for the first time.
271-
* Assume that the user is a beginner.
272-
* Assume that the user has a limited knowledge of Elastic
273-
* Explain unusual terminology, abbreviations, or acronyms.
274-
* Always try to cite relevant Elastic documentation.
275-
`,
276-
},
277-
{
278-
role: 'user',
279-
message: question,
280-
},
281-
],
211+
function createLlmGatewayRequest(
212+
message: string,
213+
threadId?: string
214+
): AskAiRequest {
215+
return AskAiRequestSchema.parse({
216+
message,
282217
threadId,
283218
})
219+
220+
// return LlmGatewayRequestSchema.parse({
221+
// userContext: {
222+
// userEmail: `elastic-docs-v3@invalid`, // Random email (will be optional in the future)
223+
// },
224+
// platformContext: {
225+
// origin: 'support_portal',
226+
// useCase: 'support_assistant',
227+
// metadata: {},
228+
// },
229+
// input: [
230+
// {
231+
// role: 'user',
232+
// message: `
233+
// # ROLE AND GOAL
234+
// You are an expert AI assistant for the Elastic Stack (Elasticsearch, Kibana, Beats, Logstash, etc.). Your sole purpose is to answer user questions based *exclusively* on the provided context from the official Elastic Documentation.
235+
//
236+
// # CRITICAL INSTRUCTION: SINGLE-SHOT INTERACTION
237+
// This is a single-turn interaction. The user cannot reply to your answer for clarification. Therefore, your response MUST be final, self-contained, and as comprehensive as possible based on the provided context.
238+
// Also, keep the response as short as possible, but do not truncate the context.
239+
//
240+
// # RULES
241+
// 1. **Facts** Always do RAG search to find the relevant Elastic documentation.
242+
// 2. **Strictly Grounded Answers:** You MUST base your answer 100% on the information from the search results. Do not use any of your pre-trained knowledge or any information outside of this context.
243+
// 3. **Handle Ambiguity Gracefully:** Since you cannot ask clarifying questions, if the question is broad or ambiguous (e.g., "how to improve performance"), structure your answer to cover the different interpretations supported by the context.
244+
// * Acknowledge the ambiguity. For example: "Your question about 'performance' can cover several areas. Based on the documentation, here are the key aspects:"
245+
// * Organize the answer with clear headings for each aspect (e.g., "Indexing Performance," "Query Performance").
246+
// * But if there is a similar or related topic in the docs you can mention it and link to it.
247+
// 4. **Direct Answer First:** If the context directly and sufficiently answers a specific question, provide a clear, comprehensive, and well-structured answer.
248+
// * Use Markdown for formatting (e.g., code blocks for configurations, bullet points for lists).
249+
// * Use LaTeX for mathematical or scientific notations where appropriate (e.g., \`$E = mc^2$\`).
250+
// * Make the answer as complete as possible, as this is the user's only response.
251+
// * Keep the answer short and concise. We want to link users to the Elastic Documentation to find more information.
252+
// 5. **Handling Incomplete Answers:** If the context contains relevant information but does not fully answer the question, you MUST follow this procedure:
253+
// * Start by explicitly stating that you could not find a complete answer.
254+
// * Then, summarize the related information you *did* find in the context, explaining how it might be helpful.
255+
// 6. **Handling No Answer:** If the context is empty or completely irrelevant to the question, you MUST respond with the following, and nothing else:
256+
// I was unable to find an answer to your question in the Elastic Documentation.
257+
//
258+
// For further assistance, you may want to:
259+
// * Ask the community of experts at **discuss.elastic.co**.
260+
// * If you have an Elastic subscription, contact our support engineers at **support.elastic.co**."
261+
// 7. If you are 100% sure that something is not supported by Elastic, then say so.
262+
// 8. **Tone:** Your tone should be helpful, professional, and confident. It is better to provide no answer (Rule #5) than an incorrect one.
263+
// * Assume that the user is using Elastic for the first time.
264+
// * Assume that the user is a beginner.
265+
// * Assume that the user has a limited knowledge of Elastic
266+
// * Explain unusual terminology, abbreviations, or acronyms.
267+
// * Always try to cite relevant Elastic documentation.
268+
// `,
269+
// },
270+
// {
271+
// role: 'user',
272+
// message: question,
273+
// },
274+
// ],
275+
// threadId,
276+
// })
284277
}

src/api/AwsLambda/AwsLambda.csproj

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<InvariantGlobalization>true</InvariantGlobalization>
9+
10+
<AssemblyName>bootstrap</AssemblyName>
11+
<AWSProjectType>Lambda</AWSProjectType>
12+
13+
<IsPublishable>true</IsPublishable>
14+
<PublishAot>true</PublishAot>
15+
<PublishTrimmed>true</PublishTrimmed>
16+
<EnableSdkContainerSupport>true</EnableSdkContainerSupport>
17+
<TrimmerSingleWarn>false</TrimmerSingleWarn>
18+
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
19+
20+
<RootNamespace>Elastic.Documentation.Lambda.Api</RootNamespace>
21+
</PropertyGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\Core\Core.csproj" />
25+
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
26+
</ItemGroup>
27+
28+
<ItemGroup>
29+
<PackageReference Include="Amazon.Lambda.AspNetCoreServer.Hosting" />
30+
</ItemGroup>
31+
32+
</Project>

src/api/AwsLambda/Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM public.ecr.aws/amazonlinux/amazonlinux:2023 AS base
2+
3+
ARG TARGETARCH
4+
ARG TARGETOS
5+
6+
WORKDIR /app
7+
8+
RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc
9+
10+
RUN curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/39/prod.repo
11+
12+
RUN dnf update -y
13+
RUN dnf install -y dotnet-sdk-9.0
14+
RUN dnf install -y npm
15+
RUN dnf install -y git
16+
RUN dnf install -y clang
17+
18+
COPY . .
19+
20+
ENV DOTNET_NOLOGO=true \
21+
DOTNET_CLI_TELEMETRY_OPTOUT=true
22+
23+
RUN arch=$TARGETARCH \
24+
&& if [ "$arch" = "amd64" ]; then arch="x64"; fi \
25+
&& echo $TARGETOS-$arch > /tmp/rid
26+
27+
RUN dotnet publish src/api/AwsLambda -r linux-x64 -c Release

src/api/AwsLambda/Program.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Text.Json.Serialization;
6+
using Amazon.Lambda.APIGatewayEvents;
7+
using Amazon.Lambda.Serialization.SystemTextJson;
8+
using Api.Core;
9+
using Api.Core.AskAi;
10+
using Api.Infrastructure;
11+
12+
var builder = WebApplication.CreateBuilder(args);
13+
14+
builder.Services.ConfigureHttpJsonOptions(options =>
15+
{
16+
options.SerializerOptions.TypeInfoResolverChain.Insert(0, ApiJsonContext.Default);
17+
});
18+
19+
builder.Services.AddHttpClient();
20+
builder.Services.AddUsecases(Environment.GetEnvironmentVariable("APP_ENVIRONMENT"));
21+
22+
builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi, new SourceGeneratorLambdaJsonSerializer<LambdaJsonSerializerContext>());
23+
24+
var app = builder.Build();
25+
26+
app.MapPost("/ask-ai/stream", async (AskAiRequest askAiRequest, AskAiUsecase askAiUsecase, Cancel ctx) =>
27+
{
28+
var stream = await askAiUsecase.AskAi(askAiRequest, ctx);
29+
return Results.Stream(stream, "text/event-stream");
30+
});
31+
32+
app.Run();
33+
34+
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest), GenerationMode = JsonSourceGenerationMode.Metadata)]
35+
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse), GenerationMode = JsonSourceGenerationMode.Default)]
36+
internal sealed partial class LambdaJsonSerializerContext : JsonSerializerContext { }

0 commit comments

Comments
 (0)