├── .dockerignore
├── .gitattributes
├── .gitignore
├── .idea
└── .idea.FsOpenAI
│ └── .idea
│ ├── .gitignore
│ ├── indexLayout.xml
│ └── vcs.xml
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── Dockerfile
├── ExampleSettings.json
├── FsOpenAI.sln
├── LICENSE
├── codecheck
└── decompile.fsx
├── global.json
├── readme.md
└── src
├── FsOpenAI.Client
├── App
│ ├── App.fs
│ ├── ClientHub.fs
│ └── Subscription.fs
├── FsOpenAI.Client.fsproj
├── Model
│ ├── Auth.fs
│ ├── Graph.fs
│ ├── IO.fs
│ ├── Initialization.fs
│ ├── Model.fs
│ ├── Submission.fs
│ ├── TmpState.fs
│ └── Update.fs
├── Properties
│ └── launchSettings.json
├── Startup.fs
├── Views
│ ├── AssistantMessageView.fs
│ ├── AsyncExts.fs
│ ├── AuthenticationViews.fs
│ ├── Binding.fs
│ ├── ChatHistoryView.fs
│ ├── ChatSettingsView.fs
│ ├── CodeEvalView.fs
│ ├── DocView.fs
│ ├── FeedbackView.fs
│ ├── FooterView.fs
│ ├── HeaderView.fs
│ ├── MainLayout.fs
│ ├── MainSettingsView.fs
│ ├── PromptView.fs
│ ├── QuestionView.fs
│ ├── SearchResultsView.fs
│ ├── SidebarView.fs
│ ├── SourcesView.fs
│ └── UserMessageView.fs
└── wwwroot
│ ├── app
│ └── imgs
│ │ ├── Persona.png
│ │ ├── favicon.png
│ │ └── logo.png
│ ├── appsettings.json.template
│ ├── css
│ ├── index.css
│ └── theme-override.css
│ ├── favicon.png
│ ├── imgs
│ └── person.png
│ ├── scripts
│ └── utils.js
│ └── theme-override.css
├── FsOpenAI.CodeEvaluator
├── CodeEval.fs
├── FsOpenAI.CodeEvaluator.fsproj
├── Valdiate.fs
└── scripts
│ └── Sandbox.fsx
├── FsOpenAI.GenAI
├── AsyncExts.fs
├── Connection.fs
├── Env.fs
├── FsOpenAI.GenAI.fsproj
├── Gen
│ ├── ChatUtils.fs
│ ├── Completions.fs
│ ├── DocQnA.fs
│ ├── Endpoints.fs
│ ├── GenUtils.fs
│ ├── IndexQnA.fs
│ ├── Indexes.fs
│ ├── Models.fs
│ ├── Prompts.fs
│ ├── SKernel.fs
│ ├── SemanticVectorSearch.fs
│ ├── StreamParser.fs
│ ├── TemplateParser.fs
│ ├── Tokens.fs
│ └── WebCompletion.fs
├── Monitoring.fs
└── Sessions.fs
├── FsOpenAI.Server
├── .config
│ └── dotnet-tools.json
├── BackgroundTasks.fs
├── FsOpenAI.Server.fsproj
├── Index.fs
├── Properties
│ └── launchSettings.json
├── Samples.fs
├── ServerHub.fs
├── Startup.fs
├── Templates.fs
├── appsettings.json.template
└── wwwroot
│ └── app
│ ├── AppConfig.json
│ └── Templates
│ └── default
│ ├── DocQuerySkill
│ ├── Contextual
│ │ ├── config.json
│ │ ├── question.txt
│ │ └── skprompt.txt
│ └── ContextualBrief
│ │ ├── config.json
│ │ ├── question.txt
│ │ └── skprompt.txt
│ ├── ExtractionSkill
│ └── Concepts
│ │ ├── config.json
│ │ └── skprompt.txt
│ └── Samples.json
├── FsOpenAI.Shared
├── AppConfig.fs
├── Constants.fs
├── FsOpenAI.Shared.fsproj
├── Interactions.CodeEval.fs
├── Interactions.Core.fs
├── Interactions.fs
├── Settings.fs
├── Types.fs
└── Utils.fs
├── FsOpenAI.Tasks
├── FsOpenAI.Tasks.fsproj
├── Library.fs
├── deployments
│ └── default
│ │ ├── client
│ │ ├── app
│ │ │ └── imgs
│ │ │ │ ├── Persona.png
│ │ │ │ ├── favicon.png
│ │ │ │ └── logo.png
│ │ ├── appsettings.json.template
│ │ └── theme-override.css
│ │ ├── config_default.fsx
│ │ ├── indexed_config.fsx
│ │ ├── indexed_create.fsx
│ │ ├── server
│ │ └── appsettings.json.template
│ │ └── templates
│ │ └── default
│ │ ├── DocQuerySkill
│ │ ├── Contextual
│ │ │ ├── config.json
│ │ │ ├── question.txt
│ │ │ └── skprompt.txt
│ │ └── ContextualBrief
│ │ │ ├── config.json
│ │ │ ├── question.txt
│ │ │ └── skprompt.txt
│ │ ├── ExtractionSkill
│ │ └── Concepts
│ │ │ ├── config.json
│ │ │ └── skprompt.txt
│ │ └── Samples.json
└── scripts
│ ├── CopyAppSettings.fsx
│ ├── CosmoDB.fsx
│ ├── CreateSettings.fsx
│ ├── ListOpenAIModels.fsx
│ ├── LoadIndex.fsx
│ ├── Sandbox.fsx
│ ├── ScriptEnv.fsx
│ ├── SerializeSettings.fsx
│ ├── TemplateParsingSandbox.fsx
│ ├── TestOpenAIClient.fsx
│ └── packages.fsx
└── FsOpenAI.Vision
├── FsOpenAI.Vision.fsproj
├── Image.fs
├── Video.fs
└── VisionApi.fs
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/bin
15 | **/charts
16 | **/docker-compose*
17 | **/compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
26 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.idea/.idea.FsOpenAI/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /modules.xml
6 | /projectSettingsUpdater.xml
7 | /.idea.FsOpenAI.iml
8 | /contentModel.xml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/.idea/.idea.FsOpenAI/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/.idea.FsOpenAI/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Fast Client - Debug Server",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | "launchSettingsFilePath": "${workspaceFolder}/src/FsOpenAI.Server/Properties/launchSettings.json",
13 | "launchSettingsProfile": "https",
14 | "program": "${workspaceFolder}/src/FsOpenAI.Server/bin/Debug/net9.0/FsOpenAI.Server.dll",
15 | "args": [],
16 | "cwd": "${workspaceFolder}/src/FsOpenAI.Server",
17 | "stopAtEntry": false,
18 | "serverReadyAction": {
19 | "action": "openExternally",
20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)",
21 | },
22 | "env": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | },
25 | "sourceFileMap": {
26 | "/Views": "${workspaceFolder}/Views"
27 | }
28 | },
29 | {
30 | "name": "UNAUTHENTICATED",
31 | "type": "coreclr",
32 | "request": "launch",
33 | "preLaunchTask": "build UNAUTHENTICATED",
34 | "launchSettingsFilePath": "${workspaceFolder}/src/FsOpenAI.Server/Properties/launchSettings.json",
35 | "launchSettingsProfile": "https",
36 | "program": "${workspaceFolder}/src/FsOpenAI.Server/bin/Debug/net9.0/FsOpenAI.Server.dll",
37 | "args": [],
38 | "cwd": "${workspaceFolder}/src/FsOpenAI.Server",
39 | "stopAtEntry": false,
40 | "serverReadyAction": {
41 | "action": "openExternally",
42 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)",
43 | },
44 | "env": {
45 | "ASPNETCORE_ENVIRONMENT": "Development"
46 | },
47 | "sourceFileMap": {
48 | "/Views": "${workspaceFolder}/Views"
49 | }
50 | },
51 | {
52 | "name": "WASM Debug",
53 | "type": "blazorwasm",
54 | "hosted": true,
55 | "request": "launch",
56 | "preLaunchTask": "build",//this does not seem to work; run build task separately
57 | "launchSettingsFilePath": "${workspaceFolder}/src/FsOpenAI.Server/Properties/launchSettings.json",
58 | "launchSettingsProfile": "https",
59 | "program": "${workspaceFolder}/src/FsOpenAI.Server/bin/Debug/net9.0/FsOpenAI.Server.dll",
60 | "args": [],
61 | "cwd": "${workspaceFolder}/src/FsOpenAI.Server",
62 | "stopAtEntry": false,
63 | "serverReadyAction": {
64 | "action": "openExternally",
65 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)",
66 | },
67 | "env": {
68 | "ASPNETCORE_ENVIRONMENT": "Development"
69 | },
70 | "sourceFileMap": {
71 | "/Views": "${workspaceFolder}/Views"
72 | }
73 | },
74 | {
75 | "name": "WASM UNAUTHENTICATED",
76 | "type": "blazorwasm",
77 | "hosted": true,
78 | "request": "launch",
79 | "preLaunchTask": "build UNAUTHENTICATED", //this does not seem to work; run build task separately
80 | "launchSettingsFilePath": "${workspaceFolder}/src/FsOpenAI.Server/Properties/launchSettings.json",
81 | "launchSettingsProfile": "https",
82 | "program": "${workspaceFolder}/src/FsOpenAI.Server/bin/Debug/net9.0/FsOpenAI.Server.dll",
83 | "args": [],
84 | "cwd": "${workspaceFolder}/src/FsOpenAI.Server",
85 | "stopAtEntry": false,
86 | "serverReadyAction": {
87 | "action": "openExternally",
88 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)",
89 | },
90 | "env": {
91 | "ASPNETCORE_ENVIRONMENT": "Development"
92 | },
93 | "sourceFileMap": {
94 | "/Views": "${workspaceFolder}/Views"
95 | }
96 | }
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "FSharp.inlayHints.typeAnnotations": false,
3 | "FSharp.inlayHints.parameterNames": false,
4 | "FSharp.codeLenses.signature.enabled": false,
5 | "FSharp.codeLenses.references.enabled": false,
6 | "FSharp.enableMSBuildProjectGraph": true,
7 | "FSharp.fsac.parallelReferenceResolution": true,
8 | "editor.inlayHints.enabled": "off",
9 | "FSharp.inlineValues.enabled": false,
10 | "FSharp.fsac.gc.conserveMemory": 0
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "command": "dotnet",
6 | "type": "process",
7 | "args": [
8 | "build",
9 | "FsOpenAI.sln"
10 | ],
11 | "problemMatcher": [
12 | "$msCompile"
13 | ],
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | },
18 | "label": "build",
19 | "detail": "Build the FsOpenAI.sln solution using dotnet build"
20 | },
21 | {
22 | "command": "dotnet",
23 | "type": "process",
24 | "args": [
25 | "build",
26 | "FsOpenAI.sln",
27 | "-p:DefineConstants=UNAUTHENTICATED"
28 | ],
29 | "problemMatcher": [
30 | "$msCompile"
31 | ],
32 | "group": {
33 | "kind": "build",
34 | "isDefault": false
35 | },
36 | "label": "build UNAUTHENTICATED"
37 | },
38 | {
39 | "command": "dotnet",
40 | "type": "process",
41 | "args": [
42 | "publish",
43 | "src/FsOpenAI.Server/FsOpenAI.Server.fsproj",
44 | "-c:Release",
45 | "-o:../appunauth",
46 | "-p:DefineConstants=UNAUTHENTICATED"
47 | ],
48 | "problemMatcher": [
49 | "$msCompile"
50 | ],
51 | "group": {
52 | "kind": "build",
53 | "isDefault": false
54 | },
55 | "label": "publish UNAUTHENTICATED"
56 | },
57 | {
58 | "command": "dotnet",
59 | "type": "process",
60 | "args": [
61 | "publish",
62 | "src/FsOpenAI.Server/FsOpenAI.Server.fsproj",
63 | "-c:Release",
64 | "-o:../app"
65 | ],
66 | "problemMatcher": [
67 | "$msCompile"
68 | ],
69 | "group": {
70 | "kind": "build",
71 | "isDefault": false
72 | },
73 | "label": "publish"
74 | }
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:8.0.404-bookworm-slim AS base
2 | EXPOSE 52037
3 | #EXPOSE 52035
4 |
5 | ENV ASPNETCORE_URLS=http://+:52037
6 | #ENV ASPNETCORE_URLS=https://+:52035
7 | #ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/usr/local/share/ca-certificates/localhost-tm1.pfx
8 | #ENV ASPNETCORE_Kestrel__Certificates__Default__Password=tm1
9 | #need 'docker_extra' folder in the same directory as the Dockerfile
10 | #the contents of this folder are not in the repo
11 | # WORKDIR /usr/local/share/ca-certificates
12 | # COPY ./certs .
13 | # COPY ./docker_extra/localhost-tm1.pfx .
14 | # RUN update-ca-certificates
15 |
16 | FROM base AS prepped
17 |
18 | RUN cd /tmp \
19 | && apt-get update \
20 | && apt-get install -y wget \
21 | && apt-get install -y gnupg \
22 | && apt-get install -y unzip \
23 | && apt-get update \
24 | && wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \
25 | | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null \
26 | && echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" \
27 | | tee /etc/apt/sources.list.d/oneAPI.list \
28 | && apt-get update \
29 | && apt install -y intel-basekit \
30 | && apt-get install -y libsasl2-modules-gssapi-mit \
31 | && apt-get install -y libsasl2-dev \
32 | && apt-get install -y krb5-user \
33 | && apt-get install -y librdkafka-dev \
34 | && find /opt -name "libiomp5.so" \
35 | && ldconfig /opt/intel/compilers_and_libraries_2020.0.166/linux/compiler/lib/intel64_lin
36 | # && wget https://databricks-bi-artifacts.s3.us-east-2.amazonaws.com/simbaspark-drivers/odbc/2.8.0/SimbaSparkODBC-2.8.0.1002-Debian-64bit.zip \
37 | # && unzip SimbaSparkODBC-2.8.0.1002-Debian-64bit.zip \
38 | # && dpkg -i simbaspark_2.8.0.1002-2_amd64.deb \
39 | # && apt-get install -y unixodbc-dev
40 |
41 | FROM base AS build
42 | WORKDIR /src
43 | COPY src .
44 | WORKDIR /src/FsOpenAI.Server
45 | RUN dotnet workload update
46 |
47 | # RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
48 | # --mount=type=secret,id=nugetconfig \
49 | # dotnet restore -r linux-x64
50 | # RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
51 | # --mount=type=secret,id=nugetconfig \
52 | # dotnet publish -c Release -o /app --no-restore
53 |
54 |
55 | RUN dotnet restore -r linux-x64
56 | RUN dotnet publish -c:Release -p:DefineConstants=UNAUTHENTICATED -o:/app --no-restore
57 |
58 | FROM prepped AS final
59 | WORKDIR /app
60 | COPY --from=build /app .
61 | #COPY ./docker_extra .
62 | #ENV ODBCINI=/app/odbc.ini
63 | ENTRYPOINT ["dotnet", "FsOpenAI.Server.dll"]
64 |
--------------------------------------------------------------------------------
/ExampleSettings.json:
--------------------------------------------------------------------------------
1 | //multiple endpoints (if supplied) will be used in a random fashion to distribute load for scaling
2 | {
3 | "AZURE_OPENAI_ENDPOINTS": [
4 | {
5 | "API_KEY": "key1",
6 | "RESOURCE_GROUP": "resoruce group 1",
7 | "API_VERSION": "2023-03-15-preview"
8 | },
9 | {
10 | "API_KEY": "key2",
11 | "RESOURCE_GROUP": "resoruce group 2",
12 | "API_VERSION": "2023-03-15-preview"
13 | }
14 | ],
15 |
16 | //separate embedding endpoints for better scaling
17 | "EMBEDDING_ENDPOINTS": [
18 | {
19 | "API_KEY": "key1",
20 | "RESOURCE_GROUP": "resoruce group 1",
21 | "API_VERSION": "2023-03-15-preview"
22 | },
23 | {
24 | "API_KEY": "key2",
25 | "RESOURCE_GROUP": "resoruce group 2",
26 | "API_VERSION": "2023-03-15-preview"
27 | }
28 | ],
29 |
30 | // OpenAI API key. Can be overridden in the client app settings
31 | "OPENAI_KEY": null,
32 |
33 | //cosmos db connection string for storing persistent sessions and chat logs
34 | "LOG_CONN_STR": null,
35 |
36 | //bing search endpoint for RAG with web search
37 | "BING_ENDPOINT": {
38 | "API_KEY": "search key 1",
39 | "ENDPOINT": "https://api.bing.microsoft.com/"
40 | },
41 |
42 | //Azure AI Search endpoints for RAG with custom knowledge base
43 | "AZURE_SEARCH_ENDPOINTS": [
44 | {
45 | "API_KEY": "search key 1",
46 | "ENDPOINT": "https://[search service 1].search.windows.net"
47 | },
48 | {
49 | "API_KEY": "search key 2",
50 | "ENDPOINT": "https://[search service 2].search.windows.net"
51 | }
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 fwaris
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | This license does not cover any 3rd party packages used in building the software.
24 |
25 |
--------------------------------------------------------------------------------
/codecheck/decompile.fsx:
--------------------------------------------------------------------------------
1 | //need ILSpy tool installed 'dotnet tool install -g ilspycmd'
2 | //decompiles FsOpenAI.*.dll files into equivalent C# code
3 | open System.IO
4 |
5 | let outputDir =
6 | let dir = __SOURCE_DIRECTORY__ + "/../../FsOpenAI.CodeCheck"
7 | Path.GetFullPath(dir)
8 |
9 | if Directory.Exists(outputDir) then
10 | Directory.Delete(outputDir, true) |> ignore
11 |
12 | if not (Directory.Exists(outputDir)) then
13 | Directory.CreateDirectory(outputDir) |> ignore
14 |
15 | let inputDir =
16 | let dir = __SOURCE_DIRECTORY__ + @"/../src/FsOpenAI.Server/bin/Debug/net8.0"
17 | Path.GetFullPath dir
18 |
19 | let fsFiles = Directory.GetFiles(inputDir,"FsOpenAI.*.dll")
20 |
21 | let decompile (fsFile:string) =
22 | printfn $"Converting {fsFile} to C#"
23 | let output = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(fsFile) + ".fs")
24 | let psi = new System.Diagnostics.ProcessStartInfo()
25 | psi.FileName <- "ilspycmd"
26 | psi.Arguments <- $"--disable-updatecheck --nested-directories -p -o {outputDir} {fsFile}"
27 | psi.UseShellExecute <- false
28 | psi.RedirectStandardOutput <- true
29 | let p = System.Diagnostics.Process.Start(psi)
30 | p.WaitForExit()
31 | ;;
32 | fsFiles |> Array.iter decompile
33 |
34 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | { "sdk": { "version": "9.0.201", "rollForward": "latestFeature" }}
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/App/App.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client
2 | open System.Net.Http
3 | open Microsoft.AspNetCore.Components
4 | open Microsoft.Extensions.Logging
5 | open Microsoft.AspNetCore.Components.Authorization
6 | open Microsoft.AspNetCore.Components.WebAssembly.Authentication
7 | open Microsoft.AspNetCore.SignalR.Client
8 | open Elmish
9 | open Bolero
10 | open Bolero.Html
11 | open Bolero.Remoting.Client
12 | open Bolero.Templating.Client
13 | open FsOpenAI.Client.Views
14 | open FsOpenAI.Shared
15 |
16 | module App =
17 | open Radzen
18 | let router = Router.infer SetPage (fun model -> model.page)
19 |
20 | let view model dispatch =
21 | ecomp model dispatch {attr.empty()}
22 | //MainLayout.view model dispatch //homePage model dispatch
23 |
24 | type MyApp() =
25 | inherit ProgramComponent()
26 |
27 | []
28 | member val LocalStore : Blazored.LocalStorage.ILocalStorageService = Unchecked.defaultof<_> with get, set
29 |
30 | []
31 | member val logger:ILoggerProvider = Unchecked.defaultof<_> with get, set
32 |
33 | []
34 | member val Auth : AuthenticationStateProvider = Unchecked.defaultof<_> with get, set
35 |
36 | []
37 | member val HttpFac : IHttpClientFactory = Unchecked.defaultof<_> with get, set
38 |
39 | []
40 | member val TokenProvider : IAccessTokenProvider = Unchecked.defaultof<_> with get, set
41 |
42 | []
43 | member val NotificationService : NotificationService = Unchecked.defaultof<_> with get, set
44 |
45 | []
46 | member val DialogService : DialogService = Unchecked.defaultof<_> with get, set
47 |
48 |
49 | member val hubConn = ref None
50 |
51 | member this.Connect() =
52 | task {
53 | let! hc = ClientHub.connection this.TokenProvider this.logger this.NavigationManager
54 | this.hubConn.Value <- Some hc
55 | let clientDispatch msg = this.Dispatch (FromServer msg)
56 | hc.On(C.ClientHub.fromServer,clientDispatch) |> ignore
57 | }
58 |
59 | override this.Program =
60 |
61 | //authentication
62 | let handler = new AuthenticationStateChangedHandler(fun t ->
63 | task {
64 | let! s = t
65 | this.Dispatch (SetAuth (Some s.User))
66 | } |> ignore)
67 | this.Auth.add_AuthenticationStateChanged(handler)
68 |
69 | let getInitConfig() =
70 | task {
71 | let! v = this.JSRuntime.InvokeAsync("inputValue", [|C.LOAD_CONFIG_ID|])
72 | let cfg =
73 | try
74 | let str = v |> System.Convert.FromBase64String |> System.Text.Encoding.UTF8.GetString
75 | System.Text.Json.JsonSerializer.Deserialize(str,Utils.serOptions())
76 | with ex ->
77 | LoadConfig.Default
78 | return cfg
79 | }
80 |
81 | let uparms =
82 | {
83 | localStore = this.LocalStore
84 | notificationService = this.NotificationService
85 | dialogService = this.DialogService
86 | serverConnect = this.Connect
87 | serverDispatch = ClientHub.send this.Dispatch this.hubConn
88 | serverCall = ClientHub.call this.hubConn
89 | navMgr = this.NavigationManager
90 | httpFac = this.HttpFac
91 | getInitConfig = getInitConfig
92 | }
93 |
94 | let update = Update.update uparms
95 |
96 | Program.mkProgram (fun _ -> Model.initModel, Cmd.ofMsg GetInitConfig) update view
97 | |> Program.withSubscription Subscription.asyncMessages
98 | |> Program.withRouter router
99 | #if DEBUG
100 | |> Program.withHotReload
101 | //|> Program.withConsoleTrace
102 | #endif
103 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/App/ClientHub.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client
2 | open System
3 | open System.Text.Json
4 | open System.Text.Json.Serialization
5 | open Microsoft.AspNetCore.Components
6 | open Microsoft.AspNetCore.Components.WebAssembly.Authentication
7 | open Microsoft.Extensions.Logging
8 | open Microsoft.AspNetCore.SignalR.Client
9 | open Microsoft.Extensions.DependencyInjection
10 | open FSharp.Control
11 | open FsOpenAI.Shared
12 |
13 | module ClientHub =
14 |
15 | let configureSer (o:JsonSerializerOptions)=
16 | JsonFSharpOptions.Default()
17 | .WithAllowNullFields(true)
18 | .WithAllowOverride(true)
19 | .AddToJsonSerializerOptions(o)
20 | o
21 |
22 | let getToken (accessTokenProvider:IAccessTokenProvider) () =
23 | task {
24 | if accessTokenProvider = null then
25 | printfn "access token provider not set"
26 | return null
27 | else
28 | let! token = accessTokenProvider.RequestAccessToken()
29 | match token.TryGetToken() with
30 | | true, token -> printfn $"have access token; expires: {token.Expires}"; return token.Value
31 | | _ -> printfn "don't have access token"; return null
32 | }
33 |
34 | let retryPolicy = [| TimeSpan(0,0,5); TimeSpan(0,0,10); TimeSpan(0,0,30); TimeSpan(0,0,30) |]
35 |
36 | //signalr hub connection that can send/receive messages to/from server
37 | let connection
38 | (tokenProvider:IAccessTokenProvider)
39 | (loggerProvider: ILoggerProvider)
40 | (navMgr:NavigationManager)
41 | =
42 | task {
43 | let hubConnection =
44 | HubConnectionBuilder()
45 | .AddJsonProtocol(fun o -> configureSer o.PayloadSerializerOptions |> ignore)
46 | .WithUrl(
47 | navMgr.ToAbsoluteUri(C.ClientHub.urlPath),
48 | fun w ->
49 | w.AccessTokenProvider <- (getToken tokenProvider)
50 | //w.Transports <- Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets
51 | )
52 | .WithAutomaticReconnect(retryPolicy)
53 | .ConfigureLogging(fun logging ->
54 | logging.AddProvider(loggerProvider) |> ignore
55 | )
56 | .Build()
57 | do! hubConnection.StartAsync()
58 | return hubConnection
59 | }
60 |
61 |
62 | let reconnect (conn:HubConnection) =
63 | task {
64 | try
65 | do! conn.StopAsync()
66 | do! conn.StartAsync()
67 | printfn "hub reconnected"
68 | with ex ->
69 | printfn $"hub reconnect failed {ex.Message}"
70 | } |> ignore
71 |
72 | let rec private retrySend methodName count (conn:HubConnection) (msg:ClientInitiatedMessages) =
73 | if count < 7 then
74 | async {
75 | printfn $"try resend message {count + 1}"
76 | try
77 | if conn.State = HubConnectionState.Connected then
78 | do! conn.SendAsync(methodName,msg) |> Async.AwaitTask
79 | else
80 | do! Async.Sleep 1000
81 | return! retrySend methodName (count+1) conn msg
82 | with ex ->
83 | do! Async.Sleep 1000
84 | return! retrySend methodName (count+1) conn msg
85 | }
86 | else
87 | async {
88 | printfn $"retry limit reached of {count}"
89 | return ()
90 | }
91 |
92 | let private _send invokeMethod clientDispatch (conn:HubConnection) (msg:ClientInitiatedMessages) =
93 | task {
94 | try
95 | if conn.State = HubConnectionState.Connected then
96 | do! conn.SendAsync(invokeMethod,msg)
97 | else
98 | retrySend invokeMethod 0 conn msg |> Async.Start
99 | with ex ->
100 | retrySend invokeMethod 0 conn msg |> Async.Start
101 | clientDispatch (ShowError ex.Message)
102 | }
103 | |> ignore
104 |
105 | let send clientDispatch (conn:Ref) (msg:ClientInitiatedMessages) =
106 | match conn.Value with
107 | | Some conn -> _send C.ClientHub.fromClient clientDispatch conn msg
108 | | None -> printfn "no hub connection"
109 |
110 | let call (conn:Ref) (msg:ClientInitiatedMessages) =
111 | match conn.Value with
112 | | Some conn -> conn.SendAsync(C.ClientHub.fromClient,msg)
113 | | None -> printfn "no hub connection"; Threading.Tasks.Task.CompletedTask
114 |
115 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/App/Subscription.fs:
--------------------------------------------------------------------------------
1 |
2 | namespace FsOpenAI.Client
3 | open System
4 | open Elmish
5 | open System.Threading.Channels
6 | open FSharp.Control
7 |
8 | //pump background-generated messages into the main program message loop via an Elmish subscription
9 | module Subscription =
10 |
11 | let asyncMsgQueue =
12 | let ops = BoundedChannelOptions(1000,
13 | SingleReader = true,
14 | FullMode = BoundedChannelFullMode.DropNewest,
15 | SingleWriter = true)
16 | Channel.CreateBounded(ops)
17 |
18 | let queueReader() =
19 | asyncSeq{
20 | while true do
21 | let! msg = asyncMsgQueue.Reader.ReadAsync().AsTask() |> Async.AwaitTask
22 | yield msg
23 | }
24 |
25 | let asyncMessages (model:Model) : (SubId * Subscribe) list =
26 | let sub dispatch : IDisposable =
27 | queueReader()
28 | |> AsyncSeq.iter(fun msg ->
29 | try dispatch msg with ex -> printfn "%A" ex.Message)
30 | |> Async.Start
31 | {new IDisposable with member _.Dispose() = ()}
32 | [["asyncMessages"],sub]
33 |
34 | let post msg = asyncMsgQueue.Writer.TryWrite(msg) |> ignore
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/FsOpenAI.Client.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | false
5 | false
6 | Debug;Release;UNAUTHENTICATED
7 | AnyCPU
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Model/Auth.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client
2 | open System
3 | open Elmish
4 | open FSharp.Control
5 | open Microsoft.AspNetCore.Components
6 | open Microsoft.AspNetCore.Components.WebAssembly.Authentication
7 | open System.Security.Claims
8 | open FsOpenAI.Shared
9 |
10 | //manage user authentication
11 | module Auth =
12 | let getEmail (u:ClaimsPrincipal) =
13 | u.Claims
14 | |> Seq.tryFind(fun x -> x.Type="preferred_username")
15 | |> Option.map(_.Value)
16 | |> Option.defaultValue u.Identity.Name
17 |
18 | let initiateLogin() =
19 | async{
20 | do! Async.Sleep 1000 //post login after delay so user can see flash message
21 | return LoginLogout
22 | }
23 |
24 | let checkAuth model apply =
25 | match model.appConfig.RequireLogin, model.user with
26 | | true,Unauthenticated -> model, Cmd.batch [Cmd.ofMsg (ShowInfo "Authenticating..."); (Cmd.OfAsync.perform initiateLogin () id) ]
27 | | true,Authenticated u when not u.IsAuthorized -> model, Cmd.ofMsg (ShowInfo "User not authorized")
28 | | _,_ -> apply model
29 |
30 | let checkAuthFlip apply model = checkAuth model apply
31 |
32 | let isAuthorized model =
33 | match model.appConfig.RequireLogin, model.user with
34 | | true,Unauthenticated -> false
35 | | true,Authenticated u when not u.IsAuthorized -> false
36 | | _,_ -> true
37 |
38 | //Ultimately takes the user to the login/logout page of AD
39 | let loginLogout (navMgr:NavigationManager) model =
40 | match model.user with
41 | | Unauthenticated -> navMgr.NavigateToLogin("authentication/login")
42 | | Authenticated _ -> navMgr.NavigateToLogout("authentication/logout")
43 |
44 | ///Post authentication processing
45 | let postAuth model (claimsPrincipal:ClaimsPrincipal option) =
46 | match claimsPrincipal with
47 | | None -> {model with user=Unauthenticated}, Cmd.none
48 | | Some p when not p.Identity.IsAuthenticated -> {model with user=Unauthenticated}, Cmd.none
49 | | Some p ->
50 | //printfn $"Claims: %O{p.Identity.Name}"
51 | //p.Identities |> Seq.iter(fun x -> printfn $"{x.Name}")
52 | //p.Claims |> Seq.iter(fun x -> printfn $"{x.Type}={x.Value}")
53 | let claims =
54 | p.Claims
55 | |> Seq.tryFind(fun x ->x.Type="roles")
56 | |> Option.map(fun x->Text.Json.JsonSerializer.Deserialize(x.Value))
57 | |> Option.defaultValue []
58 | |> set
59 | let roles = model.appConfig.Roles |> set
60 | let userRoles = Set.intersect claims roles
61 | let hasAuth = model.appConfig.Roles.IsEmpty || not userRoles.IsEmpty
62 | let email = getEmail p
63 | let user = {Name=p.Identity.Name; IsAuthorized=hasAuth; Principal=p; Roles=userRoles; Email=email}
64 | let model =
65 | {model with
66 | user = UserState.Authenticated user
67 | busy = hasAuth && Model.isChatPeristenceConfigured model
68 | }
69 | let cmds =
70 | Cmd.batch
71 | [
72 | if not hasAuth then
73 | Cmd.ofMsg (ShowError "User not authorized")
74 | else
75 | Cmd.ofMsg StartInit
76 | Cmd.ofMsg GetUserDetails
77 | ]
78 | model,cmds
79 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Model/Graph.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Graph
2 | open System
3 | open System.Net.Http
4 | open Microsoft.AspNetCore.Components
5 | open Microsoft.AspNetCore.Components.WebAssembly.Authentication
6 |
7 | type GraphAPIAuthorizationMessageHandler(p,n) as this =
8 | inherit AuthorizationMessageHandler(p,n)
9 | do
10 | this.ConfigureHandler(
11 | authorizedUrls = [|"https://graph.microsoft.com"|],
12 | scopes = [| "https://graph.microsoft.com/User.Read"|]
13 | )
14 | |> ignore
15 |
16 | module Api =
17 | open System.Net.Http.Headers
18 | let CLIENT_ID = "GraphAPI"
19 |
20 | let configure (client:HttpClient) =
21 | client.BaseAddress <- new Uri("https://graph.microsoft.com")
22 |
23 | let getDetails (user,httpFac:IHttpClientFactory) =
24 | task {
25 | let client = httpFac.CreateClient(CLIENT_ID)
26 | let! ph = client.GetAsync("v1.0/me/photo/$value")
27 | let! photo =
28 | if ph.IsSuccessStatusCode then
29 | task {
30 | let! bytes = ph.Content.ReadAsByteArrayAsync()
31 | let str = bytes |> Convert.ToBase64String
32 | let str = $"data:image/png;base64,{str}"
33 | return Some str
34 | }
35 | else
36 | task {return None}
37 | return photo
38 | }
39 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Model/TmpState.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client
2 | open FsOpenAI.Shared
3 |
4 | //manage temporary state for chat UI settings
5 | module TmpState =
6 | let isOpenDef defVal key model =
7 | model.settingsOpen
8 | |> Map.tryFind key
9 | |> Option.defaultValue defVal
10 |
11 | let isOpen key model = isOpenDef false key model
12 |
13 | let toggle key model =
14 | {model with
15 | settingsOpen =
16 | model.settingsOpen
17 | |> Map.tryFind key
18 | |> Option.map(fun b -> model.settingsOpen |> Map.add key (not b))
19 | |> Option.defaultWith(fun _ -> model.settingsOpen |> Map.add key true)}
20 |
21 | let setState key value model =
22 | {model with settingsOpen = model.settingsOpen |> Map.add key value}
23 |
24 | let openClose id model =
25 | {model with
26 | settingsOpen =
27 | model.settingsOpen
28 | |> Map.tryFind id
29 | |> Option.map(fun b -> model.settingsOpen |> Map.add id (not b))
30 | |> Option.defaultWith(fun _ -> model.settingsOpen |> Map.add id true)
31 | }
32 |
33 | let updateChatTempState id model fSet defaultValue =
34 | let st =
35 | model.tempChatSettings
36 | |> Map.tryFind id
37 | |> Option.map fSet
38 | |> Option.defaultValue defaultValue
39 | |> fun cs -> Map.add id cs model.tempChatSettings
40 | {model with tempChatSettings=st}
41 |
42 | let toggleChatSettings id model =
43 | updateChatTempState id model
44 | (fun s -> {s with SettingsOpen = not s.SettingsOpen})
45 | {TempChatState.Default with SettingsOpen=true}
46 |
47 | let toggleChatDocs (id,msgId) model =
48 | updateChatTempState id model
49 | (fun s -> {s with DocsOpen = msgId})
50 | {TempChatState.Default with DocsOpen=msgId}
51 |
52 | let toggleDocDetails id model =
53 | updateChatTempState id model
54 | (fun s -> {s with DocDetailsOpen = not s.DocDetailsOpen})
55 | {TempChatState.Default with DocDetailsOpen=true}
56 |
57 | let togglePrompts id model =
58 | updateChatTempState id model
59 | (fun s -> {s with PromptsOpen = not s.PromptsOpen})
60 | {TempChatState.Default with PromptsOpen=true}
61 |
62 | let toggleIndex id model =
63 | updateChatTempState id model
64 | (fun s -> {s with IndexOpen = not s.IndexOpen})
65 | {TempChatState.Default with IndexOpen=true}
66 |
67 | let toggleSysMsg id model =
68 | updateChatTempState id model
69 | (fun s -> {s with SysMsgOpen = not s.SysMsgOpen})
70 | {TempChatState.Default with SysMsgOpen=true}
71 |
72 | let toggleFeedback id model =
73 | updateChatTempState id model
74 | (fun s -> {s with FeedbackOpen = not s.FeedbackOpen})
75 | {TempChatState.Default with FeedbackOpen=true}
76 |
77 | let isDocsOpen model =
78 | let selChat = Model.selectedChat model
79 | let docs =
80 | selChat
81 | |> Option.bind (fun chat ->
82 | model.tempChatSettings
83 | |> Map.tryFind chat.Id
84 | |> Option.bind (_.DocsOpen)
85 | |> Option.bind(fun msgId ->
86 | chat.Messages
87 | |> List.filter(fun m -> m.MsgId=msgId)
88 | |> List.tryHead
89 | |> Option.map(fun m -> match m.Role with Assistant s -> s.DocRefs | _ -> failwith "unexpected"))
90 | )
91 | |> Option.defaultValue []
92 | selChat |> Option.map (fun c -> c.Id),docs
93 |
94 | let chatSettingsOpen id model =
95 | model.tempChatSettings
96 | |> Map.tryFind id
97 | |> Option.map(fun x -> x.SettingsOpen)
98 | |> Option.defaultValue false
99 |
100 | let isDocDetailsOpen id model =
101 | model.tempChatSettings
102 | |> Map.tryFind id
103 | |> Option.map(fun x -> x.DocDetailsOpen)
104 | |> Option.defaultValue false
105 |
106 | let isPromptsOpen id model =
107 | model.tempChatSettings
108 | |> Map.tryFind id
109 | |> Option.map(fun x -> x.PromptsOpen)
110 | |> Option.defaultValue false
111 |
112 | let isIndexOpen id model =
113 | model.tempChatSettings
114 | |> Map.tryFind id
115 | |> Option.map(fun x -> x.IndexOpen)
116 | |> Option.defaultValue false
117 |
118 | let isSysMsgOpen id model =
119 | model.tempChatSettings
120 | |> Map.tryFind id
121 | |> Option.map(fun x -> x.SysMsgOpen)
122 | |> Option.defaultValue false
123 |
124 | let isFeedbackOpen id model =
125 | model.tempChatSettings
126 | |> Map.tryFind id
127 | |> Option.map(fun x -> x.FeedbackOpen)
128 | |> Option.defaultValue false
129 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "https": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "environmentVariables": {
7 | "ASPNETCORE_ENVIRONMENT": "Development"
8 | },
9 | "dotnetRunMessages": true,
10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
11 | "applicationUrl": "https://localhost:52035;http://localhost:52037"
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client
2 | open System
3 | open System.Net.Http
4 | open Microsoft.Extensions.Configuration
5 | open Microsoft.Extensions.DependencyInjection
6 | open Microsoft.AspNetCore.Components.WebAssembly.Hosting
7 | open Blazored.LocalStorage
8 | open Bolero.Remoting.Client
9 | open Radzen
10 |
11 | module Program =
12 | open Microsoft.AspNetCore.Components.Web
13 |
14 | []
15 | let Main args =
16 | let builder = WebAssemblyHostBuilder.CreateDefault(args)
17 | builder.RootComponents.Add("#main")
18 | builder.RootComponents.Add("head::after")
19 | builder.Services.AddBoleroRemoting(builder.HostEnvironment) |> ignore
20 | builder.Services.AddRadzenComponents() |> ignore
21 | builder.Services.AddBlazoredLocalStorage(fun o -> o.JsonSerializerOptions <- ClientHub.configureSer o.JsonSerializerOptions) |> ignore
22 |
23 | //http factory to create clients to call Microsoft graph api
24 | builder.Services.AddScoped() |> ignore
25 | builder.Services.AddHttpClient(
26 | Graph.Api.CLIENT_ID,
27 | Action(Graph.Api.configure)) //need type annotation to bind to the correct overload
28 |
29 | .AddHttpMessageHandler()
30 | |> ignore
31 |
32 | //add authentication that internally uses the msal.js library
33 | builder.Services.AddMsalAuthentication(fun o ->
34 | //read configuration to reference the AD-app
35 | builder.Configuration.Bind("AzureAd", o.ProviderOptions.Authentication)
36 | let defScope = builder.Configuration.["AzureAd:DefaultScope"]
37 | //NOTE: EntraID app registration should have a scope called API.Access in 'expose an api' section
38 | let defScope =
39 | if defScope = null then
40 | $"api://{o.ProviderOptions.Authentication.ClientId}/API.Access"
41 | else
42 | defScope
43 | printfn $"adding msal authentication with default scope {defScope}"
44 |
45 | o.ProviderOptions.DefaultAccessTokenScopes.Add(defScope)
46 | ) |> ignore
47 |
48 | builder.Build().RunAsync() |> ignore
49 | 0
50 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/AssistantMessageView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open Bolero.Html
3 | open FsOpenAI.Client
4 | open FsOpenAI.Shared
5 | open FSharp.Formatting.Markdown
6 |
7 | module AssistantMessage =
8 | open Radzen
9 | open Radzen.Blazor
10 |
11 | let view (msg:InteractionMessage) (chat:Interaction) lastMsg model dispatch =
12 | let docs = match msg.Role with Assistant r -> r.DocRefs | _ -> []
13 | let docsr =
14 | if docs |> List.exists (fun x -> x.SortOrder.IsSome) then
15 | docs |> List.filter (fun x -> x.SortOrder.IsSome)
16 | else
17 | docs
18 | comp {
19 | attr.``class`` $"rz-mt-1 rz-border-radius-3"
20 | comp {
21 | comp {
22 | "Size" => 1
23 | match model.appConfig.AssistantIcon with //assume alt icon is an image (updated to match radzen changes)
24 | | Some path ->
25 | comp {
26 | "Path" => path
27 | "Style" => "width: 2rem; height: 2rem;"
28 | }
29 | | None ->
30 | comp {
31 | "Icon" => C.DFLT_ASST_ICON
32 | "IconColor" => (model.appConfig.AssistantIconColor |> Option.defaultValue C.DFLT_ASST_ICON_COLOR)
33 | }
34 | }
35 | comp {
36 | "Size" => 11
37 | div {
38 | attr.style "white-space: pre-line;"
39 | if Utils.isEmpty msg.Message then
40 | "..."
41 | else
42 | let html = Markdown.ToHtml(Markdown.Parse(msg.Message))
43 | let html = html
44 | .Replace("", "")
45 | .Replace("
", "
")
46 |
47 | Bolero.Html.rawHtml(html)
48 | }
49 | table {
50 | attr.style "width: 100%;"
51 | if not docs.IsEmpty && not chat.IsBuffering then
52 | tr {
53 | attr.``class`` "rz-mt-1"// rz-color-on-primary-darker rz-background-color-primary-darker"
54 | attr.style "background-color: var(--rz-primary-lighter);"
55 | td {
56 | attr.style "width: 1.5rem;"
57 | ecomp (Some chat.Id,docs) dispatch { attr.empty() }
58 | }
59 | td {
60 | comp {
61 | attr.``class`` "rz-p-2"
62 | "Style" => "height: 3.5rem; overflow: auto;"
63 | "Orientation" => Orientation.Horizontal
64 | "Wrap" => FlexWrap.Wrap
65 | for d in docsr do
66 | comp {
67 | attr.title (Utils.shorten 40 d.Title)
68 | attr.``class`` "rz-ml-2 "
69 | "Style" => "max-width: 140px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; color: var(--rz-danger-light);"
70 | "Path" => d.Ref
71 | "Target" => "_blank"
72 | $"{d.Id}: {d.Title}"
73 | }
74 | }
75 | }
76 | }
77 | if lastMsg && not chat.IsBuffering then
78 | match Interactions.CodeEval.Interaction.codeBag chat with
79 | | None -> ()
80 | | Some v ->
81 | tr {
82 | td {
83 | attr.colspan 2
84 | ecomp model dispatch {attr.empty()}
85 | }
86 | }
87 | tr {
88 |
89 | td {
90 | attr.colspan 2
91 | match chat.Feedback with
92 | | Some fb ->
93 | ecomp (fb,chat,model) dispatch { attr.empty() }
94 | | None -> ()
95 | }
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/AsyncExts.fs:
--------------------------------------------------------------------------------
1 | module AsyncExts
2 | open System
3 | open FSharp.Control
4 | open System.Threading
5 | open System.Threading.Channels
6 | open System.Threading.Tasks
7 |
8 | module Async =
9 | let map f a = async.Bind(a, f >> async.Return)
10 |
11 | module AsyncSeq =
12 | let mapAsyncParallelThrottled (parallelism:int) (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> = asyncSeq {
13 | use mb = MailboxProcessor.Start (ignore >> async.Return)
14 | use sm = new SemaphoreSlim(parallelism)
15 | let! err =
16 | s
17 | |> AsyncSeq.iterAsync (fun a -> async {
18 | let! _ = sm.WaitAsync () |> Async.AwaitTask
19 | let! b = Async.StartChild (async {
20 | try return! f a
21 | finally sm.Release () |> ignore })
22 | mb.Post (Some b) })
23 | |> Async.map (fun _ -> mb.Post None)
24 | |> Async.StartChildAsTask
25 | yield!
26 | AsyncSeq.unfoldAsync (fun (t:Task) -> async{
27 | if t.IsFaulted then
28 | return None
29 | else
30 | let! d = mb.Receive()
31 | match d with
32 | | Some c ->
33 | let! d' = c
34 | return Some (d',t)
35 | | None -> return None
36 | })
37 | err
38 | }
39 | (*
40 | //implementation possible within AsyncSeq, with the supporting code available there
41 | let mapAsyncParallelThrottled (parallelism:int) (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> = asyncSeq {
42 | use mb = MailboxProcessor.Start (ignore >> async.Return)
43 | use sm = new SemaphoreSlim(parallelism)
44 | let! err =
45 | s
46 | |> iterAsync (fun a -> async {
47 | do! sm.WaitAsync () |> Async.awaitTaskUnitCancellationAsError
48 | let! b = Async.StartChild (async {
49 | try return! f a
50 | finally sm.Release () |> ignore })
51 | mb.Post (Some b) })
52 | |> Async.map (fun _ -> mb.Post None)
53 | |> Async.StartChildAsTask
54 | yield!
55 | replicateUntilNoneAsync (Task.chooseTask (err |> Task.taskFault) (async.Delay mb.Receive))
56 | |> mapAsync id }
57 | *)
58 |
59 | let private _mapAsyncParallelUnits (units:string) (unitsPerMinute:float) (f:'a -> Async<'b>) (s:AsyncSeq) : AsyncSeq<'b> = asyncSeq {
60 | use mb = MailboxProcessor.Start (ignore >> async.Return)
61 | let resolution = 0.5 //minutes
62 | let mutable markTime = DateTime.Now.Ticks
63 | let mutable unitCount = 0uL
64 |
65 | let incrUnits i = Interlocked.Add(&unitCount,i)
66 |
67 | let reset i t =
68 | Interlocked.Exchange(&unitCount,i) |> ignore
69 | Interlocked.Exchange(&markTime,t) |> ignore
70 |
71 | //some randomness to stagger calls
72 | let rand5Pct() =
73 | let rng = Random()
74 | (rng.Next(0,5) |> float) / 100.0
75 |
76 | let! err =
77 | s
78 | |> AsyncSeq.iterAsync (fun (t,a) -> async {
79 | let unitCountF = incrUnits t |> float
80 | let elapsed = (DateTime.Now - DateTime(markTime)).TotalMinutes
81 | if elapsed > resolution then
82 | //use a sliding rate
83 | let t = DateTime.Now.AddMinutes (-resolution / 2.0)
84 | let i = (uint64 (unitCountF / 2.0))
85 | reset i t.Ticks
86 | let rate =
87 | if elapsed < 0.001 then
88 | let overagePct = rand5Pct()
89 | unitsPerMinute * (1.0 + overagePct) //initially (elapsed ~= 0) assume small random overage so initial calls are staggered
90 | else
91 | unitCountF / elapsed |> min (unitsPerMinute * 2.0) //cap rate to 2x
92 | printfn $"{units}/min: %0.0f{rate} [per sec %0.1f{rate/60.0}]"
93 | let overage = rate - unitsPerMinute
94 | if overage > 0.0 then
95 | //how much of next resolution period we should wait?
96 | //scale based on overage as %age of base rate
97 | let overagePct = overage / unitsPerMinute + (rand5Pct())
98 | let wait = resolution * overagePct * 60000.0 |> int
99 | printfn $"wait sec %0.1f{float wait/1000.0}"
100 | do! Async.Sleep wait
101 | let! b = Async.StartChild (async {
102 | try return! f a
103 | finally () })
104 | mb.Post (Some b) })
105 | |> Async.map (fun _ -> mb.Post None)
106 | |> Async.StartChildAsTask
107 | yield!
108 | AsyncSeq.unfoldAsync (fun (t:Task) -> async{
109 | if t.IsFaulted then
110 | return None
111 | else
112 | let! d = mb.Receive()
113 | match d with
114 | | Some c ->
115 | let! d' = c
116 | return Some (d',t)
117 | | None -> return None
118 | })
119 | err
120 | }
121 |
122 | ///Invoke f in parallel while maintaining the tokens per minute rate.
123 | ///Input is a sequence of (tokens:unint64 *'a) where the tokens is the number of input tokens associated with value 'a.
124 | ///Note: ordering is not maintained
125 | let mapAsyncParallelTokenLimit (tokensPerMinute:float) (f:'a -> Async<'b>) (s:AsyncSeq) : AsyncSeq<'b> =
126 | _mapAsyncParallelUnits "tokens" tokensPerMinute f s
127 |
128 | ///Invoke f in parallel while maintaining opsPerSecond rate.
129 | ///Note: ordering is not maintained
130 | let mapAsyncParallelRateLimit (opsPerSecond:float) (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> =
131 | _mapAsyncParallelUnits "ops" (opsPerSecond * 60.0) f (s |> AsyncSeq.map (fun a -> 1UL,a))
132 |
133 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/AuthenticationViews.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero
4 | open Bolero.Html
5 | open Elmish
6 | open Radzen
7 | open Radzen.Blazor
8 | open FsOpenAI.Client
9 | open Microsoft.AspNetCore.Components
10 | open Microsoft.AspNetCore.Components.WebAssembly.Authentication
11 | open Microsoft.AspNetCore.Components.Web
12 | open FsOpenAI.Shared
13 |
14 | type LoginRedirectView() =
15 | inherit ElmishComponent()
16 |
17 | []
18 | member val NavMgr : NavigationManager = Unchecked.defaultof<_> with get, set
19 |
20 | []
21 | member val ThemeService = Unchecked.defaultof with get, set
22 |
23 | override this.View mdl (dispatch:Message -> unit) =
24 | let action,model = mdl
25 | concat {
26 | comp { text (model.appConfig.AppName |> Option.defaultValue "") }
27 | comp { "Theme" => this.ThemeService.Theme }
28 | comp{attr.empty()}
29 | comp{attr.empty()}
30 | comp {
31 | comp {
32 | comp {
33 | "Action" => action
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/ChatHistoryView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero
4 | open Bolero.Html
5 | open Radzen
6 | open Radzen.Blazor
7 | open FsOpenAI.Client
8 | open Microsoft.AspNetCore.Components
9 | open Microsoft.JSInterop
10 | open FsOpenAI.Shared
11 | open FsOpenAI.Shared.Interactions
12 |
13 | module Notes =
14 | let timeline markerId (chat:Interaction) (marker:Ref) =
15 | let notes = List.rev chat.Notifications
16 | comp {
17 | attr.``class`` "rz-mt-1 rz-p-2"
18 | "LinePosition" => LinePosition.Left
19 | attr.fragment "Items" (
20 | concat {
21 | yield
22 | comp {
23 | "PointSize" => PointSize.Small
24 | attr.fragment "ChildContent" (
25 | comp {
26 | "TextStyle" => TextStyle.Caption
27 | attr.id markerId
28 | "Text" => (notes |> List.tryHead |> Option.defaultValue "...")
29 | marker
30 | }
31 | )
32 | }
33 | if notes.Length > 1 then
34 | yield
35 | comp {
36 | "PointSize" => PointSize.Small
37 | comp {
38 | "Gap" => "0.5rem"
39 | "Orientation" => Orientation.Horizontal
40 | concat {
41 | for t in notes.Tail do
42 | yield
43 | concat{
44 | comp {
45 | "Shade" => Shade.Light
46 | "BadgeStyle" => BadgeStyle.Base
47 | }
48 | comp {
49 | "Style" => "max-width: 10rem; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;"
50 | "TextStyle" => TextStyle.Caption
51 | attr.title t
52 | "Text" => t
53 | }
54 | }
55 |
56 | }
57 | }
58 | }
59 | })
60 | }
61 |
62 | type ChatHistoryView() =
63 | inherit ElmishComponent()
64 |
65 | let marker = Ref()
66 |
67 | member val IsBuffering = false with get, set
68 | member val markerId = "" with get, set
69 |
70 | []
71 | member val JSRuntime = Unchecked.defaultof with get, set
72 |
73 | member this.CopyToClipboard(text:string) =
74 | this.JSRuntime.InvokeVoidAsync ("navigator.clipboard.writeText", text) |> ignore
75 |
76 | member this.ScrollToEnd() =
77 | if Utils.notEmpty this.markerId then
78 | this.JSRuntime.InvokeVoidAsync ("fso_scrollTo", [|this.markerId|]) |> ignore
79 |
80 | override this.OnAfterRenderAsync(a) =
81 | if this.IsBuffering then this.ScrollToEnd()
82 | base.OnAfterRenderAsync(a)
83 |
84 | override this.View model dispatch =
85 | comp {
86 | attr.``class`` "rz-mr-1"
87 | comp {
88 | comp {
89 | comp {
90 | "Sytle" => "height: 1rem;"
91 | //"Size" => 1
92 | match Model.selectedChat model with
93 | | Some chat ->
94 | let m =
95 | {
96 | ChatId = chat.Id
97 | Model = model
98 | Parms = chat.Parameters
99 | QaBag =
100 | Interaction.qaBag chat
101 | |> Option.orElseWith (fun _ ->
102 | if Model.isEnabledAny [M_Index; M_Doc_Index] this.Model then
103 | Some QABag.Default
104 | else
105 | None)
106 | SystemMessage = Interaction.systemMessage chat
107 | }
108 | ecomp m dispatch {attr.empty()}
109 | | None -> ()
110 | }
111 | }
112 | comp {
113 | "Style" => "max-height: calc(100vh - 18rem);overflow-y:auto;overflow-x:hidden;"
114 | comp {
115 | match Model.selectedChat model with
116 | | Some chat ->
117 | let lastMsgId = List.tryLast chat.Messages |> Option.map (fun x -> x.MsgId) |> Option.defaultValue ""
118 | this.markerId <- $"{chat.Id}_marker"
119 | this.IsBuffering <- chat.IsBuffering
120 | for m in chat.Messages do
121 | if m.IsUser then
122 | yield UserMessage.view m chat model dispatch
123 | else
124 | yield AssistantMessage.view m chat (m.MsgId=lastMsgId) model dispatch
125 | if chat.IsBuffering then
126 | yield
127 | Notes.timeline this.markerId chat marker
128 | | None -> ()
129 | }
130 | }
131 | comp {
132 | "Style" => "height: auto; margin-top: 1rem;"
133 | attr.``class`` "rz-mt-1"
134 | ecomp model dispatch {attr.empty()}
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/FeedbackView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Microsoft.AspNetCore.Components.Web
4 | open Bolero
5 | open Bolero.Html
6 | open Radzen
7 | open Radzen.Blazor
8 | open FsOpenAI.Client
9 | open FsOpenAI.Shared
10 |
11 | type FeedbackView() =
12 | inherit ElmishComponent()
13 | let commentRef = Ref()
14 |
15 | override this.View m dispatch =
16 | let fb,chat,model = m
17 | comp {
18 | attr.``class`` "rz-p-1"
19 | "AlignItems" => Radzen.AlignItems.Center
20 | "Orientation" => Radzen.Orientation.Horizontal
21 | let colorUp = if fb.ThumbsUpDn > 0 then "var(--rz-success)" else ""
22 | let colorDn = if fb.ThumbsUpDn < 0 then "var(--rz-danger)" else ""
23 | comp {
24 | "Placeholder" => "Comment (optional)"
25 | "Rows" => 1
26 | "Cols" => 30
27 | "Style" => "resize: none; width: 100%; outline: none;"
28 | "Value" => (fb.Comment |> Option.defaultValue "")
29 | on.blur (fun _ ->
30 | commentRef.Value
31 | |> Option.iter(fun m ->
32 | dispatch(Ia_Feedback_Set (chat.Id, {fb with Comment = Some m.Value}))))
33 | commentRef
34 | }
35 | comp {
36 | "Icon" => "thumb_up"
37 | "ButtonStyle" => ButtonStyle.Base
38 | "Style" => "background: transparent;"
39 | "Size" => ButtonSize.ExtraSmall
40 | "IconColor" => colorUp
41 | attr.callback "Click" (fun (e:MouseEventArgs) ->
42 | dispatch (Ia_Feedback_Set(chat.Id, {fb with ThumbsUpDn = +1 }))
43 | dispatch (Ia_Feedback_Submit(chat.Id)))
44 | }
45 | comp {
46 | "Icon" => "thumb_down"
47 | "IconColor" => colorDn
48 | "Style" => "background: transparent;"
49 | "ButtonStyle" => ButtonStyle.Base
50 | "Size" => ButtonSize.ExtraSmall
51 | attr.callback "Click" (fun (e:MouseEventArgs) ->
52 | dispatch (Ia_Feedback_Set(chat.Id, {fb with ThumbsUpDn = -1}))
53 | dispatch (Ia_Feedback_Submit(chat.Id)))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/FooterView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open FsOpenAI.Client
3 | open FsOpenAI.Shared
4 | open FsOpenAI.Shared.Interactions
5 | open System
6 | open Microsoft.AspNetCore.Components
7 | open Microsoft.AspNetCore.Components.Web
8 | open Bolero
9 | open Bolero.Html
10 | open Radzen
11 | open Radzen.Blazor
12 |
13 |
14 | module Footer =
15 | open Microsoft.JSInterop
16 | let copyToClipboard (jsr:IJSRuntime) (text:string) =
17 | jsr.InvokeVoidAsync ("navigator.clipboard.writeText", text) |> ignore
18 |
19 | let view jsr model dispatch =
20 | comp {
21 | //"Style" => "height:3rem;"
22 | comp {
23 | comp {
24 | "Size" => 1
25 | }
26 | comp {
27 | "Size" => 10
28 | match model.appConfig.Disclaimer with
29 | | Some t ->
30 | comp {
31 | "TextAlign" => TextAlign.Center
32 | "Text" => t
33 | "Style" => "color:var(--rz-text-color-secondary);"
34 | }
35 | | None -> ()
36 | }
37 | comp {
38 | "Size" => 1
39 | comp {
40 | "ButtonStyle" => ButtonStyle.Base
41 | "Size" => ButtonSize.Small
42 | "Variant" => Variant.Outlined
43 | "Style" => "background:transparent;border: 1px solid var(--rz-base-light)"
44 | attr.``class`` "rz-border-radius-10 rz-shadow-10"
45 | attr.title "Copy chat to clipboard"
46 | "Icon" => "content_copy"
47 | attr.callback "Click" (fun (e:MouseEventArgs) ->
48 | Model.selectedChat model
49 | |> Option.iter (fun c ->
50 | let text = Interaction.getText c
51 | copyToClipboard jsr text
52 | dispatch (ShowInfo "Copied")))
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/HeaderView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero
4 | open Bolero.Html
5 | open FsOpenAI.Client
6 | open FsOpenAI.Shared
7 | open Radzen
8 | open Radzen.Blazor
9 | open Microsoft.AspNetCore.Components
10 | open Microsoft.AspNetCore.Components.Web
11 | open Microsoft.JSInterop
12 |
13 | type HeaderView() =
14 | inherit ElmishComponent()
15 | let transparentBg = "background: transparent;"
16 |
17 | [] member val ThemeService:ThemeService = Unchecked.defaultof<_> with get,set
18 |
19 | override this.View model dispatch =
20 | let sidebarExpanded = TmpState.isOpenDef true C.SIDE_BAR_EXPANDED model
21 | comp {
22 | //attr.``class`` "rz-background-color-danger-dark"
23 | comp {
24 | "AlignItems" => AlignItems.Center
25 | comp {
26 | "Size" => 1
27 | comp {
28 | //"Style" => transparentBg
29 | "Icon" => (if sidebarExpanded then "chevron_left" else "chevron_right")
30 | attr.callback "Click" (fun (e:EventArgs) -> dispatch ToggleSideBar)
31 | }
32 | }
33 | comp {
34 | "Size" => 2
35 | if model.busy then
36 | comp {
37 | "ShowValue" => false
38 | "Mode" => ProgressBarMode.Indeterminate
39 | "Size" => ProgressBarCircularSize.Small
40 | "ProgressBarStyle" => ProgressBarStyle.Danger
41 | }
42 | }
43 | comp{
44 | "Size" => 5
45 | comp {
46 | "Style" => "text-align: center; width: 100%; align-self: center; color: var(--rz-primary)"
47 | "Text" => (match model.loadConfig.AppTitle with Some t -> t | _ -> "")
48 | "TextStyle" => TextStyle.H6
49 | }
50 | }
51 | comp{
52 | "Size" => 1
53 | if model.loadConfig.RequireLogin then
54 | let isAuthenticated = match model.user with | Authenticated _ -> true | _ -> false
55 | if not isAuthenticated then
56 | comp {
57 | "Text" => "Login"
58 | attr.callback "Click" (fun (e:MouseEventArgs) -> dispatch LoginLogout)
59 | }
60 | else
61 | comp {
62 | attr.fragment "Template" (
63 | comp {
64 | "Path" => (model.photo |> Option.defaultValue "imgs/person.png")
65 | "Style" => "width: 1.5rem; border-radius: 50%;"
66 | })
67 | comp {
68 | "Text" => match model.user with
69 | | Unauthenticated -> "Login"
70 | | Authenticated user -> $"Log out {user.Name}"
71 | }
72 | }
73 | }
74 | comp {
75 | "Size" => 1
76 | comp {attr.empty()}
77 | }
78 | comp {
79 | "Size" => 2
80 | ecomp model dispatch {attr.empty()}
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/MainLayout.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero.Html
4 | open FsOpenAI.Client
5 | open FsOpenAI.Shared
6 | open FsOpenAI.Shared.Interactions
7 | open Bolero
8 | open Microsoft.AspNetCore.Components.Web
9 | open Microsoft.AspNetCore.Components
10 | open Microsoft.JSInterop
11 | open Radzen
12 | open Radzen.Blazor
13 |
14 | type MainLayout() =
15 | inherit ElmishComponent()
16 |
17 | []
18 | member val JSRuntime = Unchecked.defaultof with get, set
19 |
20 | []
21 | member val ThemeService = Unchecked.defaultof with get, set
22 |
23 | member this.CopyToClipboard(text:string) =
24 | this.JSRuntime.InvokeVoidAsync ("navigator.clipboard.writeText", text) |> ignore
25 |
26 | override this.OnParametersSet() =
27 | if this.ThemeService.Theme = null then
28 | this.ThemeService.SetTheme "Humanistic"
29 |
30 | override this.View model dispatch =
31 |
32 | match model.page with
33 | | Page.Authentication action ->
34 | ecomp (action,model) dispatch {attr.empty()}
35 |
36 | | Page.Home ->
37 | concat {
38 | comp{attr.empty()}
39 | comp { text (model.appConfig.AppName |> Option.defaultValue "") }
40 | comp { "Theme" => this.ThemeService.Theme }
41 | comp{attr.empty()}
42 | comp {
43 | comp { attr.empty() }
44 | ecomp model dispatch {attr.empty()}
45 | ecomp model dispatch {attr.empty()}
46 | comp {
47 | comp {
48 | "Style" => "height: 100%;"
49 | "Orientation" => Orientation.Horizontal
50 | comp {
51 | "Size" => "75%"
52 | ecomp model dispatch {attr.empty()}
53 | }
54 | comp {
55 | "Size" => "25%"
56 | attr.``class`` "rz-p-0 rz-p-lg-12"
57 | "Style" => "overflow:auto;"
58 | ecomp model dispatch {attr.empty()}
59 | }
60 | }
61 | }
62 | Footer.view this.JSRuntime model dispatch
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/MainSettingsView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open Microsoft.AspNetCore.Components
3 | open Microsoft.AspNetCore.Components.Web
4 | open Bolero
5 | open Bolero.Html
6 | open Radzen
7 | open Radzen.Blazor
8 | open FsOpenAI.Client
9 | open FsOpenAI.Shared
10 | open System.Collections.Generic
11 |
12 | type OpenAIKey() =
13 | inherit ElmishComponent()
14 |
15 | override this.View model (dispatch:Message -> unit) =
16 | comp {
17 | comp {
18 | "Orientation" => Orientation.Horizontal
19 | "AlignItems" => AlignItems.Center
20 | comp {"Text" => "OpenAI Key"}
21 | comp {
22 | attr.style "display: flex; flex-grow:1;"
23 | "Placeholder" => "OpenAI Key"
24 | "Value" => (model.serviceParameters |> Option.bind(fun p -> p.OPENAI_KEY) |> Option.defaultValue null)
25 | attr.callback "ValueChanged" (fun e -> dispatch (UpdateOpenKey e))
26 | }
27 | }
28 | }
29 |
30 | type private M_MenuItems =
31 | | M_ClearChats
32 | | M_PurgeLocalData
33 | | M_SetOpenAIKey
34 |
35 | type MainSettingsView() =
36 | inherit ElmishComponent()
37 |
38 | []
39 | member val DialogService = Unchecked.defaultof with get, set
40 |
41 | []
42 | member val ContextMenuService = Unchecked.defaultof with get, set
43 |
44 | override this.View model (dispatch:Message -> unit) =
45 | comp {
46 | "Icon" => "menu"
47 | "Variant" => Variant.Flat
48 | "ButtonStyle" => ButtonStyle.Base
49 | "Style" => "background-color: transparent;"
50 | "Size" => ButtonSize.Small
51 | attr.callback "Click" (fun (e:MouseEventArgs) ->
52 | this.ContextMenuService.Open(
53 | e,
54 | [
55 | ContextMenuItem(Icon="delete_sweep", Text="Clear chats", Value=M_ClearChats, IconColor=Colors.Warning)
56 | ContextMenuItem(Icon="folder_delete", Text="Purge local browser storage", Value=M_PurgeLocalData)
57 | if model.appConfig.EnabledBackends |> List.contains OpenAI then
58 | ContextMenuItem(Icon="key", Text="Set OpenAI Key", Value=M_SetOpenAIKey)
59 | ],
60 | fun (e:MenuItemEventArgs) ->
61 | match e.Value :?> M_MenuItems with
62 | | M_ClearChats -> this.ContextMenuService.Close(); dispatch Ia_ClearChats
63 | | M_PurgeLocalData -> this.ContextMenuService.Close(); dispatch PurgeLocalData
64 | | M_SetOpenAIKey ->
65 | this.ContextMenuService.Close();
66 | let parms = ["Model", model :> obj; "Dispatch", dispatch] |> dict |> Dictionary
67 | this.DialogService.OpenAsync("OpenAI Key", parameters=parms) |> ignore
68 | ))
69 | }
70 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/PromptView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero
4 | open Bolero.Html
5 | open Elmish
6 | open MudBlazor
7 | open FsOpenAI.Client
8 | ()
9 | //type PromptView() =
10 | // inherit ElmishComponent()
11 |
12 | // override this.View model dispatch =
13 | // let chat,message = model
14 | // concat {
15 | // comp {
16 | // //"Class" => "d-flex flex-grow-1"
17 | // "Spacing" => 0
18 | // comp {
19 | // //"Class" => "d-flex flex-grow-1"
20 | // "xs" => 12
21 | // comp> {
22 | // attr.callback "ValueChanged" (fun e -> dispatch (Chat_AddMsg (chat.Id,e)))
23 | // "Variant" => Variant.Outlined
24 | // "Label" => "Prompt"
25 | // "Lines" => 15
26 | // "Placeholder" => "Enter prompt or question"
27 | // "Text" => message.Message
28 | // }
29 | // }
30 | // comp {
31 | // //"Class" => "d-flex flex-grow-0"
32 | // "xs" => 12
33 | // comp {
34 | // "Row" => true
35 | // comp {
36 | // "Icon" => Icons.Material.Filled.Settings
37 | // }
38 | // comp {
39 | // "Icon" => Icons.Material.Filled.DeleteSweep
40 | // "Tooltip" => "Clear data"
41 | // on.click(fun ev -> dispatch Reset )
42 | // }
43 | // comp {attr.empty()}
44 | // comp {
45 | // "Icon" => Icons.Material.Filled.Add
46 | // on.click(fun ev -> dispatch AddDummyContent)
47 | // }
48 | // comp {attr.empty()}
49 | // comp {
50 | // "Icon" => Icons.Material.Filled.Send
51 | // on.click(fun ev -> dispatch (SubmitChat chat.Id) )
52 | // }
53 | // }
54 | // }
55 | // }
56 | // }
57 |
58 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/SearchResultsView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero
4 | open Bolero.Html
5 | open Microsoft.AspNetCore.Components.Web
6 | open Radzen
7 | open Radzen.Blazor
8 | open FsOpenAI.Client
9 | open FsOpenAI.Shared
10 | open System.Linq.Expressions
11 | open Radzen.Blazor.Rendering
12 |
13 | type SVAttr =
14 | static member Property(expr:Expression>) =
15 | "Property" => expr
16 |
17 | type SearchResultsView() =
18 | inherit ElmishComponent()
19 | let popup = Ref()
20 | let button = Ref()
21 |
22 | override this.View model dispatch =
23 | let chatId,docs = model
24 | concat {
25 | comp {
26 | "Style" => "background:transparent;height:2rem;"
27 | "Icon" => "snippet_folder"
28 | "ButtonStyle" => ButtonStyle.Base
29 | attr.title "Preview search results"
30 | attr.callback "Click" (fun (e:MouseEventArgs) -> popup.Value |> Option.iter (fun p -> p.ToggleAsync(button.Value.Value.Element) |> ignore))
31 | button
32 | }
33 | comp {
34 | "Style" => $"display:none;position:absolute;max-height:90vh;max-width:90vw;height:20rem;width:30rem;padding:5px;background:transparent;"
35 | "Lazy" => false
36 | "PreventDefault" => false
37 | popup
38 | comp {
39 | "Style" => "height:100%;width:100%;overflow:none;background-color:var(--rz-panel-background-color);"
40 | "Variant" => Variant.Filled
41 | attr.``class`` "rz-shadow-5 rz-p-2 rz-border-radius-5 rz-border-danger-light"
42 | comp> {
43 | "Style" => "height:100%;width:100%;overflow:none;"
44 | attr.``class`` "rz-mb-2"
45 | "Data" => docs
46 | "Item" => "Document"
47 | "AllowPaging" => true
48 | "AllowVirtualization" => false
49 | "PageSize" => 1
50 | "Density" => Density.Compact
51 | attr.fragmentWith "Template" (fun (o:DocRef) ->
52 | comp {
53 | "Style" => "width:100%; height:100%; overflow:none;"
54 | comp {
55 | "Style" => "height:2rem;"
56 | "Path" => o.Ref
57 | "Target" => "_blank"
58 | $"{o.Id}: {o.Title}"
59 | }
60 | comp {
61 | attr.style "max-height:10rem; overflow:auto; white-space: pre-line;"
62 | div {
63 | attr.style "white-space: pre-line;"
64 | text o.Text
65 | }
66 | }
67 | })
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/SidebarView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open System
3 | open Bolero
4 | open Bolero.Html
5 | open FsOpenAI.Client
6 | open FsOpenAI.Shared
7 | open Radzen
8 | open Radzen.Blazor
9 | open FsOpenAI.Shared.Interactions
10 | open Microsoft.AspNetCore.Components
11 | open Microsoft.AspNetCore.Components.Web
12 |
13 | type SidebarView() =
14 | inherit ElmishComponent()
15 |
16 | override this.View model dispatch =
17 | let selChatId = Model.selectedChat model |> Option.map (fun x -> x.Id) |> Option.defaultValue ""
18 | comp {
19 | "Style" => "background-color: var(--rz-surface);"
20 | "Expanded" => TmpState.isOpenDef true C.SIDE_BAR_EXPANDED model
21 | attr.callback "ExpandedChanged" (fun (b:bool) -> dispatch (SidebarExpanded b))
22 | comp {
23 | comp {
24 | "Style" => "width: 100%;"
25 | attr.``class`` "rz-p-2"
26 | "AlignItems" => AlignItems.Center
27 | "Orientation" => Orientation.Horizontal
28 | comp {
29 | "ButtonStyle" => ButtonStyle.Primary
30 | attr.``class`` "rz-border-radius-10 rz-shadow-10"
31 | attr.title "Add chat"
32 | "Icon" => "add"
33 | attr.callback "Click" (fun (e:MouseEventArgs) -> dispatch (Ia_Add InteractionMode.M_Index))
34 | }
35 | }
36 | comp {
37 | "Gap" => "0"
38 | for x in model.interactions do
39 | comp {
40 | "Gap" => "0"
41 | attr.``class`` "rz-p-2"
42 | "Orientation" => Orientation.Horizontal
43 | "AlignItems" => AlignItems.Center
44 | comp {
45 | "Size" => ButtonSize.Small
46 | "ButtonStyle" => ButtonStyle.Base
47 | if x.Id = selChatId then
48 | "Style" => "outline: 2px solid var(--rz-primary); width: 100%;"
49 | else
50 | "Style" => "width: 100%;"
51 | "ButtonStyle" => ButtonStyle.Base
52 |
53 | attr.callback "Click" (fun (e:MouseEventArgs) -> dispatch (Ia_Selected x.Id))
54 | Interaction.label x
55 | }
56 | comp {
57 | "ButtonStyle" => ButtonStyle.Base
58 | attr.``class`` "rz-ml-2"
59 | "Style" => "background-color: var(--rz-surface);"
60 |
61 | "Size" => ButtonSize.Small
62 | "Icon" => "close"
63 | attr.callback "Click" (fun (e:MouseEventArgs) -> dispatch (Ia_Remove x.Id))
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/Views/UserMessageView.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Client.Views
2 | open Bolero.Html
3 | open FsOpenAI.Client
4 | open FsOpenAI.Shared
5 |
6 | module UserMessage =
7 | open Radzen
8 | open Radzen.Blazor
9 |
10 | let view (m:InteractionMessage) (chat:Interaction) model dispatch =
11 | let bg = "background-color: transparent;"
12 | comp {
13 | "Style" => bg
14 | attr.``class`` "rz-mt-1 rz-mr-2"
15 | comp {
16 | "Size" => 12
17 | attr.style "display: flex; justify-content: flex-end;"
18 | comp {
19 | "Orientation" => Orientation.Horizontal
20 | attr.``class`` $"rz-border-radius-5 rz-p-8; rz-background-color-info-lighter"
21 | div {
22 | attr.style "white-space: pre-line;"
23 | attr.``class`` "rz-p-2"
24 | text m.Message
25 | }
26 | comp {
27 | "Style" => bg
28 | "Responsive" => false
29 | comp {
30 | "Icon" => "refresh"
31 | attr.title "Edit and resubmit this message"
32 | attr.callback "Click" (fun (e:MenuItemEventArgs) -> dispatch (Ia_Restart (chat.Id, m)))
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/app/imgs/Persona.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Client/wwwroot/app/imgs/Persona.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/app/imgs/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Client/wwwroot/app/imgs/favicon.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/app/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Client/wwwroot/app/imgs/logo.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/appsettings.json.template:
--------------------------------------------------------------------------------
1 | {
2 | "Comment" : {
3 | "1":"Copy contents of this file to appsettings.json and place it in the same location as this"
4 | "2":"In the 'AzureAd' section set the appropriate values from the MS EntraID app registration for MSAL authentication.",
5 | "3":"appsettings.json is 'gitignored' as it may contain proprietary information"
6 | },
7 | "AzureAd": {
8 | "Authority": "https://login.microsoftonline.com/",
9 | "ClientId": "",
10 | "ValidateAuthority": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/css/index.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | }
4 |
5 | body {
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
10 | #main {
11 | flex: 1;
12 | }
13 |
14 | #main > .columns {
15 | min-height: 100%;
16 | }
17 |
18 | .sidebar {
19 | width: 250px;
20 | background: whitesmoke;
21 | min-height: 100%;
22 | }
23 |
24 | .Class {
25 | background: #e20074
26 | }
27 |
28 | .magenta-background-2 {
29 | background: #ac0059
30 | }
31 |
32 | .white-background-1 {
33 | background:#faf9f8
34 | }
35 |
36 | #counter {
37 | width: 80px;
38 | }
39 |
40 | #notification-area {
41 | position: fixed;
42 | bottom: 0;
43 | left: 0;
44 | right: 0;
45 | }
46 |
47 | #notification-area > div {
48 | max-width: 600px;
49 | margin-left: auto;
50 | margin-right: auto;
51 | margin-bottom: 5px;
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/css/theme-override.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --rz-primary-test: #a7df58 !important;
3 | }
4 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Client/wwwroot/favicon.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/imgs/person.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Client/wwwroot/imgs/person.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/scripts/utils.js:
--------------------------------------------------------------------------------
1 |
2 | function fso_scrollTo(elementId) {
3 | var element = document.getElementById(elementId);
4 | //elementId.parentNode.scrollTop = target.offsetTop;
5 | //element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
6 | //element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
7 | element.scrollIntoView(true)
8 | }
9 |
10 | function inputValue(elementId) {
11 | var element = document.getElementById(elementId)
12 | return element.value
13 | }
14 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Client/wwwroot/theme-override.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --rz-primary-test: #a7df58 !important;
3 | }
4 |
--------------------------------------------------------------------------------
/src/FsOpenAI.CodeEvaluator/FsOpenAI.CodeEvaluator.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | true
5 | Debug;Release;UNAUTHENTICATED
6 | AnyCPU
7 |
8 |
9 | True
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/AsyncExts.fs:
--------------------------------------------------------------------------------
1 | module AsyncExts
2 | open System
3 | open FSharp.Control
4 | open System.Threading
5 | open System.Threading.Tasks
6 |
7 | module Async =
8 | let map f a = async.Bind(a, f >> async.Return)
9 |
10 | module AsyncSeq =
11 | let mapAsyncParallelThrottled (parallelism:int) (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> = asyncSeq {
12 | use mb = MailboxProcessor.Start (ignore >> async.Return)
13 | use sm = new SemaphoreSlim(parallelism)
14 | let! err =
15 | s
16 | |> AsyncSeq.iterAsync (fun a -> async {
17 | let! _ = sm.WaitAsync () |> Async.AwaitTask
18 | let! b = Async.StartChild (async {
19 | try return! f a
20 | finally sm.Release () |> ignore })
21 | mb.Post (Some b) })
22 | |> Async.map (fun _ -> mb.Post None)
23 | |> Async.StartChildAsTask
24 | yield!
25 | AsyncSeq.unfoldAsync (fun (t:Task) -> async{
26 | if t.IsFaulted then
27 | return None
28 | else
29 | let! d = mb.Receive()
30 | match d with
31 | | Some c ->
32 | let! d' = c
33 | return Some (d',t)
34 | | None -> return None
35 | })
36 | err
37 | }
38 | (*
39 | //implementation possible within AsyncSeq, with the supporting code available there
40 | let mapAsyncParallelThrottled (parallelism:int) (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> = asyncSeq {
41 | use mb = MailboxProcessor.Start (ignore >> async.Return)
42 | use sm = new SemaphoreSlim(parallelism)
43 | let! err =
44 | s
45 | |> iterAsync (fun a -> async {
46 | do! sm.WaitAsync () |> Async.awaitTaskUnitCancellationAsError
47 | let! b = Async.StartChild (async {
48 | try return! f a
49 | finally sm.Release () |> ignore })
50 | mb.Post (Some b) })
51 | |> Async.map (fun _ -> mb.Post None)
52 | |> Async.StartChildAsTask
53 | yield!
54 | replicateUntilNoneAsync (Task.chooseTask (err |> Task.taskFault) (async.Delay mb.Receive))
55 | |> mapAsync id }
56 | *)
57 |
58 | let private _mapAsyncParallelUnits (units:string) (unitsPerMinute:float) (f:'a -> Async<'b>) (s:AsyncSeq) : AsyncSeq<'b> = asyncSeq {
59 | use mb = MailboxProcessor.Start (ignore >> async.Return)
60 | let resolution = 0.5 //minutes
61 | let mutable markTime = DateTime.Now.Ticks
62 | let mutable unitCount = 0uL
63 |
64 | let incrUnits i = Interlocked.Add(&unitCount,i)
65 |
66 | let reset i t =
67 | Interlocked.Exchange(&unitCount,i) |> ignore
68 | Interlocked.Exchange(&markTime,t) |> ignore
69 |
70 | //some randomness to stagger calls
71 | let rand5Pct() =
72 | let rng = Random()
73 | (rng.Next(0,5) |> float) / 100.0
74 |
75 | let! err =
76 | s
77 | |> AsyncSeq.iterAsync (fun (t,a) -> async {
78 | let unitCountF = incrUnits t |> float
79 | let elapsed = (DateTime.Now - DateTime(markTime)).TotalMinutes
80 | if elapsed > resolution then
81 | //use a sliding rate
82 | let t = DateTime.Now.AddMinutes (-resolution / 2.0)
83 | let i = (uint64 (unitCountF / 2.0))
84 | reset i t.Ticks
85 | let rate =
86 | if elapsed < 0.001 then
87 | let overagePct = rand5Pct()
88 | unitsPerMinute * (1.0 + overagePct) //initially (elapsed ~= 0) assume small random overage so initial calls are staggered
89 | else
90 | unitCountF / elapsed |> min (unitsPerMinute * 2.0) //cap rate to 2x
91 | printfn $"{units}/min: %0.0f{rate} [per sec %0.1f{rate/60.0}]"
92 | let overage = rate - unitsPerMinute
93 | if overage > 0.0 then
94 | //how much of next resolution period we should wait?
95 | //scale based on overage as %age of base rate
96 | let overagePct = overage / unitsPerMinute + (rand5Pct())
97 | let wait = resolution * overagePct * 60000.0 |> int
98 | printfn $"wait sec %0.1f{float wait/1000.0}"
99 | do! Async.Sleep wait
100 | let! b = Async.StartChild (async {
101 | try return! f a
102 | finally () })
103 | mb.Post (Some b) })
104 | |> Async.map (fun _ -> mb.Post None)
105 | |> Async.StartChildAsTask
106 | yield!
107 | AsyncSeq.unfoldAsync (fun (t:Task) -> async{
108 | if t.IsFaulted then
109 | return None
110 | else
111 | let! d = mb.Receive()
112 | match d with
113 | | Some c ->
114 | let! d' = c
115 | return Some (d',t)
116 | | None -> return None
117 | })
118 | err
119 | }
120 |
121 | ///Invoke f in parallel while maintaining the tokens per minute rate.
122 | ///Input is a sequence of (tokens:unint64 *'a) where the tokens is the number of input tokens associated with value 'a.
123 | ///Note: ordering is not maintained
124 | let mapAsyncParallelTokenLimit (tokensPerMinute:float) (f:'a -> Async<'b>) (s:AsyncSeq) : AsyncSeq<'b> =
125 | _mapAsyncParallelUnits "tokens" tokensPerMinute f s
126 |
127 | ///Invoke f in parallel while maintaining opsPerSecond rate.
128 | ///Note: ordering is not maintained
129 | let mapAsyncParallelRateLimit (opsPerSecond:float) (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> =
130 | _mapAsyncParallelUnits "ops" (opsPerSecond * 60.0) f (s |> AsyncSeq.map (fun a -> 1UL,a))
131 |
132 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Connection.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.GenAI
2 |
3 | type CosmosDbConnection = {
4 | ConnectionString : string
5 | DatabaseName : string
6 | ContainerName : string
7 | }
8 |
9 | module Connection =
10 | open FSharp.CosmosDb
11 | open FSharp.Control
12 |
13 | let tryCreate<'containerType>(cstr,database,container) =
14 | try
15 | let db =
16 | Cosmos.fromConnectionString cstr
17 | |> Cosmos.database database
18 | do
19 | db
20 | |> Cosmos.createDatabaseIfNotExists
21 | |> Cosmos.execAsync
22 | |> AsyncSeq.iter (printfn "%A")
23 | |> Async.RunSynchronously
24 |
25 | do
26 | db
27 | |> Cosmos.container container
28 | |> Cosmos.createContainerIfNotExists<'containerType>
29 | |> Cosmos.execAsync
30 | |> AsyncSeq.iter (printfn "%A")
31 | |> Async.RunSynchronously
32 | Some { ConnectionString = cstr; DatabaseName = database; ContainerName = container }
33 | with ex ->
34 | Env.logException (ex,"Monitoring.installTable: ")
35 | None
36 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/FsOpenAI.GenAI.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | true
5 | Debug;Release;UNAUTHENTICATED
6 | AnyCPU
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/ChatUtils.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.GenAI.ChatUtils
2 | open FsOpenAI.Shared
3 | open Microsoft.SemanticKernel.ChatCompletion
4 |
5 | []
6 | module ChatUtils =
7 |
8 | let temperature = function
9 | | Factual -> 0.f
10 | | Exploratory -> 0.2f
11 | | Creative -> 0.7f
12 |
13 | let serializeChat (ch:Interaction) : ChatLog =
14 | {
15 | SystemMessge = ch.SystemMessage
16 | Messages =
17 | ch.Messages
18 | |> Seq.filter (fun m -> not(Utils.isEmpty m.Message))
19 | |> Seq.map(fun m -> {ChatLogMsg.Role = (match m.Role with User -> "User" | _ -> "Assistant"); ChatLogMsg.Content = m.Message})
20 | |> Seq.toList
21 | Temperature = ch.Parameters.Mode |> temperature |> float
22 | MaxTokens = ch.Parameters.MaxTokens
23 | }
24 |
25 | let toChatHistory (ch:Interaction) =
26 | let h = ChatHistory()
27 | if ch.Parameters.ModelType <> MT_Logic && Utils.notEmpty ch.SystemMessage then //o1 does not support system messages
28 | h.AddSystemMessage(ch.SystemMessage)
29 | for m in ch.Messages do
30 | let role = if m.IsUser then AuthorRole.User else AuthorRole.Assistant
31 | h.AddMessage(role,m.Message)
32 | h
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/Endpoints.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.GenAI.Endpoints
2 | open System
3 | open Microsoft.SemanticKernel
4 | open FsOpenAI.Shared
5 | open Microsoft.SemanticKernel.Connectors.OpenAI
6 | open Microsoft.SemanticKernel.ChatCompletion
7 | open Microsoft.SemanticKernel.Embeddings
8 |
9 | []
10 | module Endpoints =
11 | let rng = Random()
12 | let randSelect (ls:_ list) = ls.[rng.Next(ls.Length)]
13 |
14 | let getAzureEndpoint (endpoints:AzureOpenAIEndpoints list) =
15 | if List.isEmpty endpoints then raise (ConfigurationError "No Azure OpenAI endpoints configured")
16 | let endpt = randSelect endpoints
17 | let rg = endpt.RESOURCE_GROUP
18 | let url = $"https://{rg}.openai.azure.com"
19 | rg,url,endpt.API_KEY
20 |
21 | let raiseNoOpenAIKey() = raise (ConfigurationError "No OpenAI key configured. Please set the key in application settings")
22 |
23 | let getOpenAIEndpoint parms =
24 | match parms.OPENAI_KEY with
25 | | Some key when Utils.notEmpty key -> "https://api.openai.com/v1/chat/completions",key
26 | | _ -> raiseNoOpenAIKey()
27 |
28 | let serviceEndpoint (parms:ServiceSettings) (backend:Backend) (model:string) =
29 | match backend with
30 | | AzureOpenAI ->
31 | let rg,url,key = getAzureEndpoint parms.AZURE_OPENAI_ENDPOINTS
32 | let url = $"https://{rg}.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-07-01-preview";
33 | url,key
34 | | OpenAI -> getOpenAIEndpoint parms
35 |
36 | let private getClientFor (parms:ServiceSettings) backend model : IChatCompletionService*string =
37 | match backend with
38 | | AzureOpenAI ->
39 | let rg,url,key = getAzureEndpoint parms.AZURE_OPENAI_ENDPOINTS
40 | let clr = Connectors.AzureOpenAI.AzureOpenAIChatCompletionService(model,url,key)
41 | clr,rg
42 | | OpenAI ->
43 | let key = match parms.OPENAI_KEY with Some key when Utils.notEmpty key -> key | _ -> raiseNoOpenAIKey()
44 | OpenAIChatCompletionService(model,key),"OpenAI"
45 |
46 | let private getEmbeddingsClientFor (parms:ServiceSettings) backend model : ITextEmbeddingGenerationService*string =
47 | match backend with
48 | | AzureOpenAI ->
49 | let rg,url,key = getAzureEndpoint parms.AZURE_OPENAI_ENDPOINTS
50 | let clr = Connectors.AzureOpenAI.AzureOpenAITextEmbeddingGenerationService(model,url,key)
51 | clr,rg
52 | | OpenAI ->
53 | let key = match parms.OPENAI_KEY with Some key when Utils.notEmpty key -> key | _ -> raiseNoOpenAIKey()
54 | OpenAITextEmbeddingGenerationService(model,key),"OpenAI"
55 |
56 | let getClient (parms:ServiceSettings) (ch:Interaction) model = getClientFor parms ch.Parameters.Backend model
57 |
58 | let getEmbeddingsClient (parms:ServiceSettings) (ch:Interaction) model = getEmbeddingsClientFor parms ch.Parameters.Backend model
59 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/Models.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.GenAI.Models
2 | open FsOpenAI.Shared
3 |
4 | []
5 | module Models =
6 |
7 | let pick modelRefs =
8 | modelRefs
9 | |> List.tryHead
10 | |> Option.defaultWith(fun () -> raise (ConfigurationError "No model configured" ))
11 |
12 | let internal chatModels (invCtx:InvocationContext) backend =
13 | let modelsConfig = invCtx.ModelsConfig
14 | let modelRefs =
15 | modelsConfig.ChatModels
16 | |> List.tryFind (fun m -> m.Backend = backend)
17 | |> Option.map(fun x -> [x])
18 | |> Option.defaultValue []
19 | if modelRefs.IsEmpty then raise (ConfigurationError $"No chat model(s) configured for backend '{backend}'")
20 | modelRefs
21 |
22 | let private logicModels (invCtx:InvocationContext) backend =
23 | let modelsConfig = invCtx.ModelsConfig
24 | let modelRefs =
25 | modelsConfig.LogicModels
26 | |> List.tryFind (fun m -> m.Backend = backend)
27 | |> Option.map(fun x -> [x])
28 | |> Option.defaultValue (chatModels invCtx backend)
29 | if modelRefs.IsEmpty then raise (ConfigurationError $"No logic or chat model(s) configured for backend '{backend}'")
30 | modelRefs
31 |
32 | let getModels (ch:InteractionParameters) invCtx backend =
33 | match ch.ModelType with
34 | | MT_Chat -> chatModels invCtx backend
35 | | MT_Logic -> logicModels invCtx backend
36 |
37 | let lowcostModels (invCtx:InvocationContext) backend =
38 | let modelsConfig = invCtx.ModelsConfig
39 | let modelRefs =
40 | modelsConfig.ChatModels
41 | |> List.tryFind (fun m -> m.Backend = backend)
42 | |> Option.map(fun x -> [x])
43 | |> Option.defaultValue []
44 | if modelRefs.IsEmpty then raise (ConfigurationError $"No lowcost chat model(s) configured for backend '{backend}'") //primarily used for ancilary tasks
45 | modelRefs
46 |
47 | let visionModel (backend:Backend) (modelConfig:ModelsConfig) =
48 | let filter (m:ModelRef) = if m.Backend = backend then Some m else None
49 | (modelConfig.ChatModels |> List.choose filter)
50 | |> List.filter (fun x->x.Model.Contains("-4o"))
51 | |> List.tryHead
52 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/SKernel.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.GenAI.SKernel
2 | open Microsoft.SemanticKernel
3 | open Microsoft.Extensions.Logging
4 | open Microsoft.SemanticKernel.Connectors.OpenAI
5 | open Microsoft.Extensions.DependencyInjection
6 | open FSharp.Control
7 | open FsOpenAI.Shared
8 | open FsOpenAI.GenAI.Endpoints
9 | open FsOpenAI.GenAI.ChatUtils
10 |
11 | []
12 | module SKernel =
13 | let logger =
14 | {new ILogger with
15 | member this.BeginScope(state) = raise (System.NotImplementedException())
16 | member this.IsEnabled(logLevel) = true
17 | member this.Log(logLevel, eventId, state, ``exception``, formatter) =
18 | let msg = formatter.Invoke(state,``exception``)
19 | printfn "Kernel: %s" msg
20 | }
21 |
22 | let loggerFactory =
23 | {new ILoggerFactory with
24 | member this.AddProvider(provider) = ()
25 | member this.CreateLogger(categoryName) = logger
26 | member this.Dispose() = ()
27 | }
28 |
29 | let promptSettings (parms:ServiceSettings) (ch:Interaction) =
30 | match ch.Parameters.ModelType with
31 | | MT_Chat ->
32 | new OpenAIPromptExecutionSettings(
33 | MaxTokens = ch.Parameters.MaxTokens,
34 | Temperature = (ChatUtils.temperature ch.Parameters.Mode |> float),
35 | TopP = 1)
36 | | MT_Logic ->
37 | new OpenAIPromptExecutionSettings(MaxTokens = ch.Parameters.MaxTokens, Temperature=1.0)
38 |
39 | let baseKernel (parms:ServiceSettings) (modelRefs:ModelRef list) (ch:Interaction) =
40 | let chatModel = modelRefs.Head.Model
41 | let builder = Kernel.CreateBuilder()
42 | builder.Services.AddLogging(fun c -> c.AddConsole().SetMinimumLevel(LogLevel.Information) |>ignore) |> ignore
43 | match ch.Parameters.Backend with
44 | | AzureOpenAI ->
45 | let rg,uri,key = Endpoints.getAzureEndpoint parms.AZURE_OPENAI_ENDPOINTS
46 | builder.AddAzureOpenAIChatCompletion(deploymentName = chatModel,endpoint = uri, apiKey = key)
47 | | OpenAI ->
48 | let key = match parms.OPENAI_KEY with Some k -> k | None -> Endpoints.raiseNoOpenAIKey()
49 | builder.AddOpenAIChatCompletion(chatModel,key)
50 |
51 | let kernelArgsFrom parms ch (args:(string*string) seq) =
52 | let sttngs = promptSettings parms ch
53 | let kargs = KernelArguments(sttngs)
54 | for (k,v) in args do
55 | kargs.Add(k,v)
56 | kargs
57 |
58 | let kernelArgsDefault (args:(string*string) seq) =
59 | let sttngs = new OpenAIPromptExecutionSettings(MaxTokens = 150, Temperature = 0, TopP = 1)
60 | let kargs = KernelArguments(sttngs)
61 | for (k,v) in args do
62 | kargs.Add(k,v)
63 | kargs
64 |
65 | let kernelArgs (args:(string*string) seq) (overrides:OpenAIPromptExecutionSettings->unit) =
66 | let args = kernelArgsDefault args
67 | args.ExecutionSettings
68 | |> Seq.iter(fun kv ->
69 | let sttngs = (kv.Value :?> OpenAIPromptExecutionSettings)
70 | overrides sttngs)
71 | args
72 |
73 | let renderPrompt (prompt:string) (args:KernelArguments) =
74 | task {
75 | let k = Kernel.CreateBuilder().Build()
76 | let fac = KernelPromptTemplateFactory()
77 | let cfg = PromptTemplateConfig(template = prompt)
78 | let pt = fac.Create(cfg)
79 | let! rslt = pt.RenderAsync(k,args) |> Async.AwaitTask
80 | return rslt
81 | }
82 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/SemanticVectorSearch.fs:
--------------------------------------------------------------------------------
1 | module SemanticVectorSearch
2 | open System
3 | open Microsoft.SemanticKernel.Memory
4 | open System.Runtime.CompilerServices
5 | open Azure.AI.OpenAI
6 | open Azure.Search.Documents.Models
7 | open Azure.Search.Documents
8 | open FSharp.Control
9 | open Microsoft.SemanticKernel.Embeddings
10 |
11 | type SearchMode = Semantic | Hybrid | Plain
12 |
13 | type CognitiveSearch
14 | (
15 | mode,
16 | srchClient:SearchClient,
17 | embeddingClient:ITextEmbeddingGenerationService,
18 | vectorFields,
19 | contentField,
20 | sourceRefField,
21 | descriptionField) =
22 |
23 | let idField = "id"
24 |
25 | let toMetadata (d:SearchDocument) =
26 | let id = d.[idField] :?> string
27 | let content = d.[contentField] :?> string
28 | let source = d.[sourceRefField] :?> string
29 | let desc = d.[descriptionField] :?> string
30 | MemoryRecordMetadata(
31 | isReference = true,
32 | id = id,
33 | text = content,
34 | description = desc,
35 | externalSourceName = source,
36 | additionalMetadata = srchClient.IndexName)
37 |
38 | let ( ?> ) (a:Nullable) def = if a.HasValue then a.Value else def
39 |
40 | let toMemoryResult(r:SearchResult) =
41 | let vectorField = Seq.head vectorFields
42 | let embRaw : obj = if r.Document.ContainsKey vectorField then r.Document.[vectorField] else null
43 | let emb : Nullable> =
44 | if embRaw = null then Nullable()
45 | else
46 | let emb : float32[] = embRaw :?> obj[] |> Array.map (fun x -> x :?> float |> float32)
47 | emb |> ReadOnlyMemory |> Nullable
48 | MemoryQueryResult(toMetadata(r.Document),r.Score ?> 1.0 ,emb)
49 |
50 | interface ISemanticTextMemory with
51 | member this.GetAsync(collection, key, withEmbedding, kernel, cancellationToken) = raise (System.NotImplementedException())
52 | member this.GetCollectionsAsync(kernel, cancellationToken) = raise (System.NotImplementedException())
53 | member this.RemoveAsync(collection, key, kernel, cancellationToken) = raise (System.NotImplementedException())
54 | member this.SaveInformationAsync(collection, text, id, description, additionalMetadata, kernel, cancellationToken) = raise (System.NotImplementedException())
55 | member this.SaveReferenceAsync(collection, text, externalId, externalSourceName, description, additionalMetadata, kernel, cancellationToken) = raise (System.NotImplementedException())
56 | member this.SearchAsync(collection, query, limit, minRelevanceScore, withEmbeddings, kernel, [] cancellationToken) =
57 | asyncSeq {
58 | let so = SearchOptions(Size=limit)
59 | [idField; contentField; sourceRefField; descriptionField] |> Seq.iter so.Select.Add
60 | match mode with
61 | | Semantic | Hybrid ->
62 | let! resp = embeddingClient.GenerateEmbeddingsAsync(ResizeArray[query]) |> Async.AwaitTask
63 | let eVec = resp.[0]
64 | let vec = VectorizedQuery(KNearestNeighborsCount = limit,vector = eVec)
65 | vectorFields |> Seq.iter vec.Fields.Add
66 | //so.Filter <- query
67 | so.VectorSearch <- new VectorSearchOptions()
68 | so.VectorSearch.Queries.Add(vec)
69 | let qSearch = if mode = SearchMode.Hybrid then query else null
70 | let! srchRslt = srchClient.SearchAsync(qSearch,so) |> Async.AwaitTask
71 | let rs = srchRslt.Value.GetResultsAsync() |> AsyncSeq.ofAsyncEnum |> AsyncSeq.map toMemoryResult
72 | yield! rs
73 | | Plain ->
74 | vectorFields |> Seq.iter so.Select.Add
75 | let! srchRslt = srchClient.SearchAsync(query,so) |> Async.AwaitTask
76 | let rs = srchRslt.Value.GetResultsAsync() |> AsyncSeq.ofAsyncEnum |> AsyncSeq.map toMemoryResult
77 | yield! rs
78 | }
79 | |> AsyncSeq.toAsyncEnum
80 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/TemplateParser.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.GenAI.TemplateParser
2 | open System
3 |
4 | ///parse semantic kernel template to extract variables and function blocks
5 |
6 | type Block = VarBlock of string | FuncBlock of string*string option
7 |
8 | []
9 | module internal StateMachine =
10 | let MAX_LITERAL = 3000
11 | let eof = Seq.toArray ""
12 | let inline error x xs = failwithf "%s got %s" x (String(xs |> Seq.truncate 100 |> Seq.toArray))
13 |
14 | let c2s cs = cs |> List.rev |> Seq.toArray |> String
15 |
16 | let toVar = c2s >> VarBlock
17 | let toFunc1 cs = FuncBlock (c2s cs,None)
18 | let toFunc2 cs vs = FuncBlock(c2s cs, Some (c2s vs))
19 |
20 | let rec start (acc:Block list) = function
21 | | [] -> acc
22 | | '{'::rest -> brace1 acc rest
23 | | _::rest -> start acc rest
24 | and brace1 acc = function
25 | | [] -> error "expected {" eof
26 | | '{'::rest -> brace2 acc rest
27 | | x::rest -> error "expected {" rest
28 | and brace2 acc = function
29 | | [] -> error "expecting $ after {{" eof
30 | | '$'::rest -> beginVar [] acc rest
31 | | c::rest when Char.IsWhiteSpace c -> brace2 acc rest
32 | | c::rest when c <> '}' && c <> '{' -> beginFunc [] acc (c::rest)
33 | | xs -> error "Expected '$'" xs
34 | and beginVar vacc acc = function
35 | | [] -> error "expecting }" eof
36 | | '}'::rest -> braceEnd1 ((toVar vacc)::acc) rest
37 | | c::rest when (Char.IsWhiteSpace c) -> braceEnd1 ((toVar vacc)::acc) rest
38 | | x::rest -> beginVar (x::vacc) acc rest
39 | and braceEnds acc = function
40 | | [] -> error "expecting }}" eof
41 | | c::rest when Char.IsWhiteSpace c -> braceEnds acc rest
42 | | c::rest when c = '}' -> braceEnd1 acc rest
43 | | c::rest -> error "expected }}" rest
44 | and braceEnd1 acc = function
45 | | [] -> error "expecting }" eof
46 | | '}'::rest -> start acc rest
47 | | ' '::rest -> braceEnd1 acc rest //can ignore whitespace
48 | | xs -> error "expecting }}" xs
49 | and beginFunc facc acc = function
50 | | [] -> error "expecting function name" eof
51 | | c::rest when Char.IsWhiteSpace c -> beginParm [] facc acc rest
52 | | c::rest when c = '}' -> braceEnd1 ((toFunc1 facc)::acc) rest
53 | | c::rest -> beginFunc (c::facc) acc rest
54 | and beginParm pacc facc acc = function
55 | | [] -> error "expecting function call parameter" eof
56 | | c::rest when Char.IsWhiteSpace c -> beginParm pacc facc acc rest
57 | | c::rest when c = '$' -> beginParmVar (c::pacc) facc acc rest
58 | | c::rest when c = '"' -> beginParmLit [] facc acc rest
59 | | c::rest -> beginParmVar (c::pacc) facc acc rest
60 | and beginParmVar pacc facc acc = function
61 | | [] -> error "expecting parameter name after $" eof
62 | | c::rest when Char.IsWhiteSpace c -> braceEnds ((toFunc2 facc pacc)::acc) rest
63 | | c::rest when c = '}' -> braceEnd1 ((toFunc2 facc pacc)::acc) rest
64 | | c::rest -> beginParmVar (c::pacc) facc acc rest
65 | and beginParmLit pacc facc acc = function
66 | | [] -> error """expecting " """ eof
67 | | c::rest when (List.length pacc > MAX_LITERAL) -> error "max literal size exceeded" rest
68 | | c::rest when c = '"' -> braceEnds ((toFunc2 facc pacc)::acc) rest
69 | | c::rest -> beginParmLit (c::pacc) facc acc rest
70 |
71 |
72 | let extractVars templateStr =
73 | start [] (templateStr |> Seq.toList)
74 | |> List.distinct
75 |
76 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/Tokens.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.GenAI.Tokens
2 | open System
3 | open Microsoft.DeepDev
4 | open FsOpenAI.Shared
5 | open FsOpenAI.GenAI.Models
6 |
7 | []
8 | module Tokens =
9 | let tokenSize (s:string) =
10 | let tokenizer = TokenizerBuilder.CreateByModelNameAsync("gpt-4").GetAwaiter().GetResult();
11 | let tokens = tokenizer.Encode(s, new System.Collections.Generic.HashSet());
12 | float tokens.Count
13 |
14 | let msgRole (m:InteractionMessage) = if m.IsUser then "User" else "Assistant"
15 |
16 | let tokenEstimateMessages (msgs:InteractionMessage seq) =
17 | let xs =
18 | seq {
19 | for m in msgs do
20 | yield $"[{msgRole m}]"
21 | yield m.Message
22 | }
23 | String.Join("\n",xs)
24 | |> tokenSize
25 |
26 | let tokenEstimate ch =
27 | let xs =
28 | seq {
29 | yield "[System]"
30 | yield ch.SystemMessage
31 | for m in ch.Messages do
32 | yield $"[{msgRole m}]"
33 | yield m.Message
34 | }
35 | String.Join("\n",xs)
36 | |> tokenSize
37 |
38 | let tokenBudget modelsConfig ch =
39 | Models.chatModels modelsConfig ch.Parameters.Backend
40 | |> List.map (_.TokenLimit) |> List.max |> float
41 |
42 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Gen/WebCompletion.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.GenAI
2 | open System
3 | open System.Net.Http
4 | open FSharp.Control
5 | open FsOpenAI.Shared
6 | open FsOpenAI.Shared.Interactions
7 | open Microsoft.SemanticKernel
8 | open FsOpenAI.GenAI.SKernel
9 |
10 | module bingApi =
11 | let private searchQuery (s:string) =
12 | $"https://api.bing.microsoft.com/v7.0/search?q={Uri.EscapeDataString(s)}&safeSearch=Strict&responseFilter=webPages&count=20"
13 |
14 | type WebPage =
15 | {
16 | name : string
17 | url : string
18 | snippet : string
19 | }
20 |
21 | type WebPages = {
22 | value : WebPage list
23 | }
24 |
25 | type Resp =
26 | {
27 | webPages : WebPages option
28 | }
29 |
30 | type HttpC(key:string) as this =
31 | inherit HttpClient()
32 | do
33 | this.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key",key)
34 |
35 | let search key s =
36 | async{
37 | use c = new HttpC(key)
38 | let uri = searchQuery s
39 | let! resp = c.GetStringAsync(uri) |> Async.AwaitTask
40 | try
41 | let wresp = Text.Json.JsonSerializer.Deserialize(resp)
42 | return wresp
43 | with ex ->
44 | return {webPages=None}
45 | }
46 |
47 | module WebCompletion =
48 |
49 | let searchIndicated (answer:string) = (answer.Contains("bing.search", StringComparison.OrdinalIgnoreCase))
50 |
51 | let askModel parms invCtx ch dispatch =
52 | async {
53 | dispatch (Srv_Ia_Notification (ch.Id,"Querying model first"))
54 | let question = Interaction.lastNonEmptyUserMessageText ch
55 | let args = SKernel.kernelArgsDefault ["input",question]
56 | let! prompt = SKernel.renderPrompt Prompts.WebSearch.answerQuestionOrDoSearch args |> Async.AwaitTask
57 | let ch = Interaction.setUserMessage prompt ch
58 | let! rslt = Completions.completeChat parms invCtx ch dispatch None None
59 | return (rslt.Content,question)
60 | }
61 |
62 | let processWebChat (parms:ServiceSettings) (invCtx:InvocationContext) (ch:Interaction) dispatch =
63 | async {
64 | try
65 | if parms.BING_ENDPOINT.IsNone then failwith "Bing endpoint not set"
66 | let bingKey = parms.BING_ENDPOINT.Value.API_KEY
67 | let! answer,question = askModel parms invCtx ch dispatch
68 | if searchIndicated answer then
69 | let webQ =
70 | try
71 | let ans = (answer.Replace("\"\"","\""))
72 | let vs = TemplateParser.extractVars ans
73 | vs
74 | |> List.tryPick (function TemplateParser.FuncBlock ("bing.search",Some v) -> Some v | _ -> None)
75 | |> Option.defaultValue question
76 | with ex ->
77 | dispatch (Srv_Ia_Notification (ch.Id,"Model did not respond with a usable search query for Bing. Using user question instead for search"))
78 | question
79 |
80 | dispatch (Srv_Ia_Notification (ch.Id,"Model instructed to invoke websearch search. Searching..."))
81 |
82 | let! webRslt = bingApi.search bingKey webQ
83 |
84 | let information =
85 | match webRslt.webPages with
86 | | Some v when v.value.IsEmpty |> not ->
87 | dispatch (Srv_Ia_Notification (ch.Id,"Got web search results. Querying model again with web data included"))
88 | let docs = v.value |> List.mapi(fun i w -> {Text=w.snippet; Embedding=[||]; Ref=w.url; Title=w.name; Id=string i; Relevance=0.0; SortOrder=None;})
89 | dispatch (Srv_Ia_SetDocs(ch.Id,docs))
90 | String.Join('\n', v.value |> List.map(fun w -> w.snippet))
91 | | _ ->
92 | dispatch (Srv_Ia_Notification (ch.Id,"Web search did not yield results. Continuing without web results"))
93 | ""
94 | let args = SKernel.kernelArgsFrom parms ch ["input",question; "externalInformation",information]
95 | let! prompt = SKernel.renderPrompt Prompts.WebSearch.answerQuestion args |> Async.AwaitTask
96 | let ch = Interaction.setUserMessage prompt ch
97 | do! Completions.checkStreamCompleteChat parms invCtx ch dispatch None true
98 | else
99 | dispatch (Srv_Ia_Notification(ch.Id,"Model was able to answer query by itself"))
100 | dispatch (Srv_Ia_Delta(ch.Id,answer))
101 | dispatch(Srv_Ia_Done(ch.Id,None))
102 |
103 | with ex ->
104 | GenUtils.handleChatException dispatch ch.Id "WebCompletion.processWebChat" ex
105 | }
106 |
--------------------------------------------------------------------------------
/src/FsOpenAI.GenAI/Monitoring.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.GenAI
2 | open System
3 | open FSharp.Control
4 | open System.Threading.Channels
5 | open FsOpenAI.Shared
6 | open FSharp.CosmosDb
7 |
8 | type ChatLogMsg = {
9 | Role : string
10 | Content : string
11 | }
12 |
13 | type ChatLog = {
14 | SystemMessge: string
15 | Messages : ChatLogMsg list
16 | Temperature : float
17 | MaxTokens : int
18 | }
19 |
20 | type PromptLog =
21 | | Embedding of string
22 | | Chat of ChatLog
23 |
24 | type MFeedback = {
25 | ThumbsUpDn : int
26 | Comment : string option
27 | }
28 |
29 |
30 | type FeedbackEntry = {
31 | LogId : string
32 | UserId : string
33 | Feedback : MFeedback
34 | }
35 |
36 | type MIndexRef = {
37 | Backend: string
38 | Name: string
39 | }
40 |
41 | type DiagEntry = {
42 | []
43 | id : string
44 | AppId : string
45 | []
46 | UserId : string
47 | Prompt : PromptLog
48 | Feedback : MFeedback option
49 | Question : string
50 | Response : string
51 | InputTokens : int
52 | OutputTokens : int
53 | Error : string
54 | Backend : string
55 | Resource : string
56 | Model : string
57 | IndexRefs : MIndexRef list
58 | Timestamp : DateTime
59 | }
60 |
61 | type LogEntry = Diag of DiagEntry | Feedback of FeedbackEntry
62 |
63 | []
64 | module Monitoring =
65 | let BUFFER_SIZE = 1000
66 | let BUFFER_WAIT = 10000
67 |
68 | let mutable private _cnctnInfo = lazy None
69 |
70 | let init (ccstr,database,container) =
71 | match Connection.tryCreate(ccstr,database,container) with
72 | | Some x -> _cnctnInfo <- lazy(Some x)
73 | | None -> ()
74 |
75 | let getConnectionFromConfig() =
76 | try
77 | Env.appConfig.Value
78 | |> Option.bind(fun x -> Env.logInfo $"{x.DatabaseName},{x.DiagTableName}"; x.DiagTableName |> Option.map(fun t -> x.DatabaseName,t))
79 | |> Option.bind(fun (database,container) ->
80 | Settings.getSettings().Value.LOG_CONN_STR
81 | |> Option.map(fun cstr -> Env.logInfo $"{Utils.shorten 30 cstr}";cstr,database,container))
82 | with ex ->
83 | Env.logException (ex,"Monitoring.getConnectionFromConfig")
84 | None
85 |
86 | let ensureConnection() =
87 | match _cnctnInfo.Value with
88 | | Some _ -> ()
89 | | None ->
90 | match getConnectionFromConfig() with
91 | | Some (cstr,db,cntnr) -> init(cstr,db,cntnr)
92 | | None -> ()
93 |
94 | let private writeDiagAsync (diagEntries:DiagEntry[]) =
95 | async {
96 | match _cnctnInfo.Value with
97 | | Some c ->
98 | try
99 | do!
100 | Cosmos.fromConnectionString c.ConnectionString
101 | |> Cosmos.database c.DatabaseName
102 | |> Cosmos.container c.ContainerName
103 | |> Cosmos.upsertMany (Array.toList diagEntries)
104 | |> Cosmos.execAsync
105 | |> AsyncSeq.iter (fun _ -> ())
106 | with ex ->
107 | Env.logException (ex,"writeLog")
108 | | None -> ()
109 | }
110 |
111 | let private updateDiagEntry (fb:MFeedback) (de:DiagEntry) =
112 | {de with Feedback = Some fb}
113 |
114 | let private updateWithFeedbackAsync (fbEntries:FeedbackEntry[]) =
115 | async {
116 | match _cnctnInfo.Value with
117 | | Some c ->
118 | try
119 | let db =
120 | Cosmos.fromConnectionString c.ConnectionString
121 | |> Cosmos.database c.DatabaseName
122 | |> Cosmos.container c.ContainerName
123 |
124 | do!
125 | fbEntries
126 | |> AsyncSeq.ofSeq
127 | |> AsyncSeq.iterAsync(fun fb ->
128 | db
129 | |> Cosmos.update fb.LogId fb.UserId (updateDiagEntry fb.Feedback)
130 | |> Cosmos.execAsync
131 | |> AsyncSeq.iter (fun _ -> ())
132 | )
133 | with ex ->
134 | Env.logException (ex,"writeLog")
135 | | None -> ()
136 | }
137 |
138 | let private channel = Channel.CreateBounded(BoundedChannelOptions(BUFFER_SIZE,FullMode = BoundedChannelFullMode.DropOldest))
139 |
140 | //background loop to read channel and write diagnostics entry to backend
141 | let private consumerLoop =
142 | asyncSeq {
143 | while true do
144 | let! data = channel.Reader.ReadAsync().AsTask() |> Async.AwaitTask
145 | yield data
146 | }
147 | |> AsyncSeq.bufferByCountAndTime 10 BUFFER_WAIT
148 | |> AsyncSeq.iterAsync (fun entries -> async {
149 | let diagEntries = entries |> Array.choose (function Diag de -> Some de | _ -> None)
150 | let feedbackEntries = entries |> Array.choose (function Feedback fb -> Some fb | _ -> None)
151 | do! writeDiagAsync diagEntries
152 | do! updateWithFeedbackAsync feedbackEntries
153 | })
154 | |> Async.Start
155 |
156 | let write logEntry = channel.Writer.WriteAsync logEntry |> ignore
157 |
158 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "dotnet-ef": {
6 | "version": "8.0.1",
7 | "commands": [
8 | "dotnet-ef"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/BackgroundTasks.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Server
2 | open System
3 | open System.Threading
4 | open System.IO
5 | open Microsoft.Extensions.Hosting
6 | open Microsoft.Extensions.Logging
7 | open FsOpenAI.Shared
8 | open FsOpenAI.GenAI
9 |
10 | //Start background activities at startup
11 | type BackgroundTasks(logger:ILogger) =
12 | inherit BackgroundService()
13 | let mutable cts : CancellationTokenSource = Unchecked.defaultof<_>
14 |
15 | let dispose() =
16 | if cts <> Unchecked.defaultof<_> then
17 | cts.Dispose()
18 | cts <- Unchecked.defaultof<_>
19 |
20 | let scan() =
21 | logger.LogInformation("starting scan")
22 | try
23 | Directory.GetFiles(Path.GetTempPath(), $"*.{C.UPLOAD_EXT}")
24 | |> Seq.append(Directory.GetFiles(Path.GetTempPath(),"*.fsx"))
25 | |> Seq.iter(fun fn ->
26 | try
27 | let fi = FileInfo(fn)
28 | if fi.LastWriteTime < DateTime.Now.Add(C.UPLOAD_FILE_STALENESS) then
29 | File.Delete(fn)
30 | logger.LogInformation($"removed stale file {fn}")
31 | with ex ->
32 | logger.LogError(ex,$"delete stale file: {fn}")
33 | )
34 | with ex ->
35 | logger.LogError(ex,"scan")
36 |
37 | ///remove any dangling uploaded or code eval files in the temp folder
38 | let startScanning() =
39 | cts <- new CancellationTokenSource()
40 | let scanner =
41 | async{
42 | while not cts.IsCancellationRequested do
43 | do! Async.Sleep C.SCAN_PERIOD
44 | scan()
45 | }
46 | Async.Start(scanner,cts.Token)
47 |
48 | let connectServices() =
49 | async {
50 | try Monitoring.ensureConnection() with ex ->
51 | Env.logException (ex,"monitor")
52 | try Sessions.ensureConnection() with ex ->
53 | Env.logException (ex,"connection")
54 | }
55 | |> Async.Start
56 |
57 | override this.Dispose(): unit = dispose()
58 |
59 | override this.ExecuteAsync(stoppingToken: System.Threading.CancellationToken): System.Threading.Tasks.Task =
60 | Tasks.Task.CompletedTask
61 |
62 | override this.ExecuteTask: System.Threading.Tasks.Task =
63 | Tasks.Task.CompletedTask
64 |
65 | override this.StartAsync(cancellationToken: System.Threading.CancellationToken): System.Threading.Tasks.Task =
66 | startScanning()
67 | connectServices()
68 | Tasks.Task.CompletedTask
69 |
70 | override this.StopAsync(cancellationToken: System.Threading.CancellationToken): System.Threading.Tasks.Task =
71 | dispose()
72 | Tasks.Task.CompletedTask
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/FsOpenAI.Server.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | false
6 | Debug;Release;UNAUTHENTICATED
7 | AnyCPU
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/Index.fs:
--------------------------------------------------------------------------------
1 | module FsOpenAI.Server.Index
2 | open System
3 | open System.Text
4 | open Bolero
5 | open Bolero.Html
6 | open Bolero.Server.Html
7 | open FsOpenAI
8 | open FsOpenAI.Shared
9 |
10 | let page = doctypeHtml {
11 | let uiVersion = typeof.Assembly.GetName().Version.ToString();
12 |
13 | let appConfig = FsOpenAI.GenAI.Env.appConfig.Value
14 | let tabTitle = appConfig |> Option.bind _.AppName |> Option.defaultValue ""
15 | let appTitle =
16 | appConfig
17 | |> Option.map _.AppBarType
18 | |> Option.bind (function
19 | | Some (AppB_Base t) -> Some t
20 | | Some (AppB_Alt t) -> Some t
21 | | None -> None)
22 | let requireLogin = appConfig |> Option.map _.RequireLogin |> Option.defaultValue false
23 | let cfg = {AppTitle=appTitle; RequireLogin=requireLogin}
24 | let cfgStr = Json.JsonSerializer.Serialize(cfg, Utils.serOptions()) |> Encoding.UTF8.GetBytes |> Convert.ToBase64String
25 |
26 | head {
27 | meta { attr.charset "UTF-8" }
28 | meta { attr.name "viewport"; attr.content "width=device-width, initial-scale=1.0" }
29 | title { tabTitle }
30 | ``base`` { attr.href "/" }
31 | link {attr.rel "short icon"; attr.``type`` "image/png"; attr.href "app/imgs/favicon.png"}
32 | link { attr.rel "stylesheet"; attr.href "css/index.css" }
33 | //utils
34 | script {attr.src $"scripts/utils.js?v={uiVersion}"}
35 | //authentication
36 | script {attr.src "_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js" }
37 | link { attr.rel "stylesheet"; attr.href "css/theme-override.css?v=2" }
38 | }
39 | body {
40 | input {attr.id C.LOAD_CONFIG_ID; attr.``type`` "hidden"; attr.value cfgStr; }
41 | div {
42 | attr.id "main"
43 | comp
44 | }
45 | boleroScript
46 | }
47 |
48 | //radzen - note this needs to be after the body otherwise javascript does not find DOM elements
49 | //update version to force reload over older scripts that may be cached
50 | script {attr.src $"_content/Radzen.Blazor/Radzen.Blazor.js?v={uiVersion}"} //version change forces js refresh on client
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "https": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "environmentVariables": {
7 | "ASPNETCORE_ENVIRONMENT": "Development"
8 | },
9 | "dotnetRunMessages": true,
10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
11 | "applicationUrl": "https://localhost:52034;http://localhost:52033"
12 | },
13 | "server": {
14 | "commandName": "Project",
15 | "launchBrowser": true,
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | },
19 | "dotnetRunMessages": true,
20 | "applicationUrl": "https://localhost:52034;http://localhost:52033"
21 | },
22 | "serverDebug": {
23 | "commandName": "Project",
24 | "launchBrowser": true,
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | },
28 | "dotnetRunMessages": true,
29 | "applicationUrl": "https://localhost:52034?server=true;http://localhost:52033"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/Samples.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Server.Templates
2 | open System.IO
3 | open FSharp.Control
4 | open FsOpenAI.Shared
5 | open FsOpenAI.GenAI
6 | open System.Text.Json
7 | open System.Text.Json.Serialization
8 |
9 | module Samples =
10 |
11 | let loadSamples() =
12 | task {
13 | let path = Path.Combine(Env.wwwRootPath(),C.TEMPLATES_ROOT.Value)
14 | let dirs = Directory.GetDirectories(path)
15 | let samples =
16 | dirs
17 | |> Seq.map (fun d ->
18 | let name = Path.GetFileName d
19 | let str = File.ReadAllText (Path.Combine(d,C.SAMPLES_JSON))
20 | let vs : SamplePrompt list = JsonSerializer.Deserialize(str,Utils.serOptions())
21 | name,vs)
22 | return samples
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/Startup.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Server
2 | open Microsoft.AspNetCore.Builder
3 | open Microsoft.Extensions.DependencyInjection
4 | open Microsoft.Extensions.Hosting
5 | open Microsoft.Extensions.Configuration
6 | open Microsoft.Identity.Web
7 | open Bolero.Remoting.Server
8 | open Bolero.Server
9 | open Bolero.Templating.Server
10 | open Microsoft.Extensions.Logging
11 | open Blazored.LocalStorage
12 | open FsOpenAI.GenAI
13 | open Radzen
14 |
15 | module Startup =
16 | let inline (!>) (x:^a) : ^b = ((^a or ^b) : (static member op_Implicit : ^a -> ^b) x)
17 |
18 | // This method gets called by the runtime. Use this method to add services to the container.
19 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
20 | let configureServices (builder:WebApplicationBuilder) =
21 | let services = builder.Services
22 |
23 | services
24 | .AddMicrosoftIdentityWebApiAuthentication(builder.Configuration, "AzureAd")
25 | // .EnableTokenAcquisitionToCallDownstreamApi()
26 | // .AddInMemoryTokenCaches()
27 | |> ignore
28 |
29 | services.AddMvc() |> ignore
30 | services.AddServerSideBlazor() |> ignore
31 | services.AddRadzenComponents() |> ignore
32 | services.AddBlazoredLocalStorage() |> ignore
33 | services.AddControllersWithViews() |> ignore
34 | services.AddRazorPages() |> ignore
35 | services.AddMsalAuthentication(fun o -> ()) |> ignore
36 | services.AddHostedService() |> ignore
37 |
38 | services
39 | .AddSignalR(fun o -> o.MaximumReceiveMessageSize <- 1_000_000; )
40 | .AddJsonProtocol(fun o ->FsOpenAI.Client.ClientHub.configureSer o.PayloadSerializerOptions |> ignore) |> ignore
41 |
42 | services
43 | .AddAuthorization()
44 | .AddLogging()
45 | .AddBoleroHost(prerendered=false) //**** NOTE: MSAL authenication works on client side only so set prerendered=false
46 | //.AddBoleroHost() //**** for debugging only
47 | #if DEBUG
48 | .AddHotReload(templateDir = __SOURCE_DIRECTORY__ + "/../FsOpenAI.Client")
49 | #endif
50 | |> ignore
51 |
52 | let configureApp (app:WebApplication) =
53 | let env = app.Environment
54 |
55 | //configuration
56 | let config = app.Services.GetRequiredService()
57 | let logger = app.Services.GetRequiredService>()
58 | Env.init(config,logger,env.WebRootPath)
59 |
60 | app
61 | .UseWebSockets() |> ignore
62 |
63 | if env.IsDevelopment() then
64 | app.UseWebAssemblyDebugging()
65 | else
66 | app.UseHsts() |> ignore
67 |
68 | app
69 | .UseHttpsRedirection()
70 | .UseMiddleware()
71 | .UseAuthentication()
72 | .UseStaticFiles()
73 | .UseRouting()
74 | .UseAuthorization()
75 | .UseBlazorFrameworkFiles()
76 | .UseEndpoints(fun endpoints ->
77 | #if DEBUG
78 | endpoints.UseHotReload()
79 | #endif
80 | endpoints.MapBlazorHub() |> ignore
81 | endpoints.MapBoleroRemoting() |> ignore
82 | endpoints.MapHub(FsOpenAI.Shared.C.ClientHub.urlPath) |> ignore
83 | endpoints.MapFallbackToBolero(Index.page) |> ignore)
84 | |> ignore
85 |
86 | module Program =
87 |
88 | []
89 | let main args =
90 | let builder = WebApplication.CreateBuilder(args)
91 | Startup.configureServices builder
92 | let app = builder.Build()
93 | Startup.configureApp app
94 | app.Run()
95 | 0
96 |
97 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/Templates.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Server.Templates
2 | open System.IO
3 | open FSharp.Control
4 | open FsOpenAI.Shared
5 | open FsOpenAI.GenAI
6 | open Microsoft.SemanticKernel
7 |
8 | module Templates =
9 | let private (@@) a b = Path.Combine(a,b)
10 |
11 | let toTemplates p (plugin:KernelPlugin) =
12 | plugin
13 | |> Seq.map (fun fn ->
14 | let fnq = p @@ fn.Name @@ "question.txt"
15 | let question = if File.Exists fnq then File.ReadAllText fnq |> Some else None
16 | {
17 | Name = fn.Name
18 | Description = fn.Description
19 | Template = File.ReadAllText(p @@ fn.Name @@ "skprompt.txt")
20 | Question = question
21 | }
22 | )
23 | |> Seq.toList
24 |
25 | let loadSkill p skillName =
26 | let k = Kernel.CreateBuilder().Build()
27 | let p = Path.Combine(p,skillName)
28 | let functions = k.ImportPluginFromPromptDirectory(p)
29 | toTemplates p functions
30 |
31 | let loadTemplates() =
32 | task {
33 | let path = Path.Combine(Env.wwwRootPath(),C.TEMPLATES_ROOT.Value)
34 | let dirs = Directory.GetDirectories(path)
35 | let templates =
36 | dirs
37 | |> Seq.filter (fun p ->
38 | let dirs = Directory.GetDirectories p |> Seq.map Path.GetFileName |> Set.ofSeq
39 | dirs.Contains(C.TMPLTS_DOCQUERY) && dirs.Contains(C.TMPLTS_EXTRACTION)
40 | )
41 | |> Seq.map (fun d ->
42 | let name = Path.GetFileName d
43 | let dqTemplates = loadSkill d C.TMPLTS_DOCQUERY
44 | let extTemplates = loadSkill d C.TMPLTS_EXTRACTION
45 | {
46 | Label = name
47 | Templates = [DocQuery, dqTemplates; Extraction, extTemplates] |> Map.ofList
48 | }
49 | )
50 | |> Seq.toList
51 | return templates
52 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/appsettings.json.template:
--------------------------------------------------------------------------------
1 | {
2 | "Comment" : {
3 | "1":"Copy contents of this file to appsettings.json and place it in the same location as this"
4 | "2":"In the 'AzureAd' section set the appropriate values from the MS EntraID app registration for MSAL authentication.",
5 | "3":"appsettings.json is 'gitignored' as it may contain proprietary information"
6 | },
7 | "Parms": {
8 | "Settings": ".fsopenai/ServiceSettings.json"
9 | },
10 | "AzureAd": {
11 | "Instance": "https://login.microsoftonline.com/",
12 | "Domain": "",
13 | "TenantId": "",
14 | "ClientId": "",
15 | "Audience": "api://..."
16 | },
17 | "Logging": {
18 | "LogLevel": {
19 | "Microsoft.AspNetCore.Hosting": "Warning"
20 | }
21 | },
22 | "DebugLogging": {
23 | "LogLevel": {
24 | "Default": "Debug",
25 | "System": "Information",
26 | "Microsoft": "Information",
27 | "Microsoft.AspNetCore.SignalR": "Debug",
28 | "Microsoft.AspNetCore.Http.Connections": "Debug"
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/AppConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "EnabledBackends": [
3 | {
4 | "Case": "OpenAI"
5 | }
6 | ],
7 | "EnabledChatModes": [
8 | [
9 | {
10 | "Case": "M_Index"
11 | },
12 | "You are a helpful AI assistant"
13 | ],
14 | [
15 | {
16 | "Case": "M_Doc_Index"
17 | },
18 | "You are a helpful AI assistant"
19 | ],
20 | [
21 | {
22 | "Case": "M_Doc"
23 | },
24 | "You are a helpful AI assistant"
25 | ],
26 | [
27 | {
28 | "Case": "M_Plain"
29 | },
30 | "You are a helpful AI assistant"
31 | ]
32 | ],
33 | "DefaultMaxDocs": 10,
34 | "Roles": [],
35 | "AppBarType": {
36 | "Case": "AppB_Base",
37 | "Fields": [
38 | "FsOpenAI Chat"
39 | ]
40 | },
41 | "RequireLogin": false,
42 | "AssistantIcon": null,
43 | "AssistantIconColor": null,
44 | "LogoUrl": "https:/github.com/fwaris/FsOpenAI",
45 | "AppName": "FsOpenAI Chat",
46 | "AppId": "default",
47 | "PersonaText": "FsOpenAI Chat",
48 | "PersonaSubText": "Loading ...",
49 | "Disclaimer": null,
50 | "MetaIndex": "fsopenai-meta",
51 | "IndexGroups": [
52 | "default"
53 | ],
54 | "ModelsConfig": {
55 | "EmbeddingsModels": [
56 | {
57 | "Backend": {
58 | "Case": "AzureOpenAI"
59 | },
60 | "Model": "text-embedding-ada-002",
61 | "TokenLimit": 8192
62 | },
63 | {
64 | "Backend": {
65 | "Case": "OpenAI"
66 | },
67 | "Model": "text-embedding-ada-002",
68 | "TokenLimit": 8192
69 | }
70 | ],
71 | "ChatModels": [
72 | {
73 | "Backend": {
74 | "Case": "AzureOpenAI"
75 | },
76 | "Model": "gpt-4o",
77 | "TokenLimit": 30000
78 | },
79 | {
80 | "Backend": {
81 | "Case": "OpenAI"
82 | },
83 | "Model": "gpt-4o",
84 | "TokenLimit": 127000
85 | }
86 | ],
87 | "LogicModels": [
88 | {
89 | "Backend": {
90 | "Case": "AzureOpenAI"
91 | },
92 | "Model": "o3-mini",
93 | "TokenLimit": 127000
94 | },
95 | {
96 | "Backend": {
97 | "Case": "OpenAI"
98 | },
99 | "Model": "o3-mini",
100 | "TokenLimit": 127000
101 | }
102 | ],
103 | "LowCostModels": [
104 | {
105 | "Backend": {
106 | "Case": "AzureOpenAI"
107 | },
108 | "Model": "gpt-4o-mini",
109 | "TokenLimit": 8000
110 | },
111 | {
112 | "Backend": {
113 | "Case": "OpenAI"
114 | },
115 | "Model": "gpt-4o-mini",
116 | "TokenLimit": 8000
117 | }
118 | ]
119 | },
120 | "DatabaseName": "fsopenai",
121 | "DiagTableName": "log1",
122 | "SessionTableName": "sessions"
123 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/DocQuerySkill/Contextual/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": 1,
3 | "description": "Combine document and search results when answering question",
4 | "execution_settings": {
5 | "default": {
6 | "max_tokens": 2000,
7 | "temperature": 0.1,
8 | "top_p": 1.0,
9 | "presence_penalty": 0.0,
10 | "frequency_penalty": 0.0
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/DocQuerySkill/Contextual/question.txt:
--------------------------------------------------------------------------------
1 | Analyze the DOCUMENT with respect to the Accounting Policies in SEARCH RESULTS and provide a side-by-side comparison of how the content in DOCUMENT affects the existing content.
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/DocQuerySkill/Contextual/skprompt.txt:
--------------------------------------------------------------------------------
1 | DOCUMENT: '''
2 | {{$document}}
3 | '''
4 |
5 | SEARCH RESULTS: '''
6 | {{$searchResults}}
7 | '''
8 |
9 | Analyze the DOCUMENT in relation to SEARCH RESULTS for ANSWERING QUESTIONS.
10 | BE CONTEXTUAL.
11 | If you don't know, ask.
12 | If you are not sure, ask.
13 | Based on calculates from TODAY
14 | TODAY is {{$date}}
15 |
16 | QUESTION: '''
17 | {{$question}}
18 | '''
19 |
20 | ANSWER:
21 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/DocQuerySkill/ContextualBrief/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": 1,
3 | "description": "Combine document and search results when answering question. Produce brief output",
4 | "execution_settings": {
5 | "default": {
6 | "max_tokens": 2000,
7 | "temperature": 0.1,
8 | "top_p": 1.0,
9 | "presence_penalty": 0.0,
10 | "frequency_penalty": 0.0
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/DocQuerySkill/ContextualBrief/question.txt:
--------------------------------------------------------------------------------
1 | Analyze the DOCUMENT with respect to the Accounting Policies in SEARCH RESULTS and provide a side-by-side comparison of how the content in DOCUMENT affects the existing content.
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/DocQuerySkill/ContextualBrief/skprompt.txt:
--------------------------------------------------------------------------------
1 | DOCUMENT: '''
2 | {{$document}}
3 | '''
4 |
5 | SEARCH RESULTS: '''
6 | {{$searchResults}}
7 | '''
8 |
9 | Analyze the DOCUMENT in relation to SEARCH RESULTS for ANSWERING QUESTIONS.
10 | BE BRIEF AND TO THE POINT, BUT WHEN SUPPLYING OPINION, IF YOU SEE THE NEED, YOU CAN BE LONGER.
11 | BE CONTEXTUAL.
12 | If you don't know, ask.
13 | If you are not sure, ask.
14 | Based on calculates from TODAY
15 | TODAY is {{$date}}
16 |
17 | QUESTION: '''
18 | {{$question}}
19 | '''
20 |
21 | ANSWER:
22 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/ExtractionSkill/Concepts/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": 1,
3 | "description": "Ask AI to extract concepts and key words from document for the purpose of search",
4 | "execution_settings": {
5 | "default": {
6 | "max_tokens": 2000,
7 | "temperature": 0.1,
8 | "top_p": 1.0,
9 | "presence_penalty": 0.0,
10 | "frequency_penalty": 0.0
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/ExtractionSkill/Concepts/skprompt.txt:
--------------------------------------------------------------------------------
1 | DOCUMENT: '''
2 | {{$document}}
3 | '''
4 |
5 | Analyze the DOCUMENT and extract terms and concepts which can be used to perform lookup. Be sure to include any Finance terms and standards in the query.
6 |
7 | DONT GENEARTE SQL. JUST LIST THE TERMS AS COMMA-SEPARATED VALUES
8 |
9 | QUERY:
10 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Server/wwwroot/app/Templates/default/Samples.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "SampleChatType": {
4 | "Case": "SM_IndexQnA",
5 | "Fields": [
6 | "ml-docs"
7 | ]
8 | },
9 | "SampleSysMsg": "You are a helpful AI assistant",
10 | "SampleQuestion": "How to prevent overfitting?",
11 | "SampleMode": {
12 | "Case": "Factual"
13 | },
14 | "MaxDocs": 5
15 | },
16 | {
17 | "SampleChatType": {
18 | "Case": "SM_CodeEval"
19 | },
20 | "SampleSysMsg": "ignored",
21 | "SampleQuestion": "Write recursive function for the Fibonacci series and return the value of the 15th element",
22 | "SampleMode": {
23 | "Case": "Factual"
24 | },
25 | "MaxDocs": 5
26 | },
27 | {
28 | "SampleChatType": {
29 | "Case": "SM_CodeEval"
30 | },
31 | "SampleSysMsg": "ignored",
32 | "SampleQuestion": "Write a function to download a page from the internet and download \u0027https://www.google.com\u0027 display results",
33 | "SampleMode": {
34 | "Case": "Factual"
35 | },
36 | "MaxDocs": 5
37 | },
38 | {
39 | "SampleChatType": {
40 | "Case": "SM_Plain",
41 | "Fields": [
42 | false
43 | ]
44 | },
45 | "SampleSysMsg": "You are a helpful AI assistant",
46 | "SampleQuestion": "What are the major languages and dialects spoken in France? Only list the language names",
47 | "SampleMode": {
48 | "Case": "Factual"
49 | },
50 | "MaxDocs": 5
51 | }
52 | ]
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/AppConfig.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Shared
2 |
3 | type Backend = OpenAI | AzureOpenAI
4 |
5 | type ModelRef =
6 | {
7 | Backend : Backend
8 | Model : string
9 | TokenLimit : int
10 | }
11 | with
12 | static member Default =
13 | {
14 | Backend = OpenAI
15 | Model = "gpt-4o"
16 | TokenLimit = 2000
17 | }
18 |
19 | type ModelsConfig =
20 | {
21 | ///List of models that can be used to generated embeddings
22 | EmbeddingsModels : ModelRef list
23 |
24 | ///List of models that may be used when input is longer than the context length of short models
25 | ChatModels : ModelRef list
26 |
27 | ///List of models that may be used for complex logic processing
28 | LogicModels : ModelRef list
29 |
30 | ///List of models that may be used for ancillary tasks (e.g. summarization to reduce token count)
31 | LowCostModels : ModelRef list
32 | }
33 | with
34 | static member Default =
35 | {
36 | EmbeddingsModels = []
37 | ChatModels = [ModelRef.Default]
38 | LogicModels = []
39 | LowCostModels = []
40 | }
41 |
42 | type InvocationContext =
43 | {
44 | ModelsConfig : ModelsConfig
45 | AppId : string option
46 | User : string option
47 | }
48 | with
49 | static member Default =
50 | {
51 | ModelsConfig = ModelsConfig.Default
52 | AppId = None
53 | User = None
54 | }
55 |
56 | type InteractionMode =
57 | | M_Plain
58 | | M_Index
59 | | M_Doc
60 | | M_Doc_Index
61 | | M_CodeEval
62 |
63 | type AppBarType =
64 | | AppB_Base of string
65 | | AppB_Alt of string
66 |
67 | type AppConfig =
68 | {
69 | ///Backends that are enabled for this app. First backend in the list is the default
70 | EnabledBackends : Backend list
71 |
72 | ///Set the modes of chat that can be created under this configuration
73 | EnabledChatModes : (InteractionMode*string) list
74 |
75 | ///Default number of docs for new chats
76 | DefaultMaxDocs : int
77 |
78 | ///List of app roles. If the user's identity provider provides any of the roles, the authenticated user
79 | ///is authorized. If the list is empty then any authenticted user is authorized to use this app
80 | Roles : string list
81 |
82 | ///AppBar style
83 | AppBarType : AppBarType option
84 |
85 | ///If true, only authenticated and authorized users will be allowed to
86 | ///invoke models
87 | RequireLogin : bool
88 |
89 | AssistantIcon : string option
90 | AssistantIconColor : string option
91 |
92 | ///Url to go to when main logo is clicked
93 | LogoUrl : string option
94 |
95 | ///Name of the application that will show on the browser tab
96 | AppName: string option
97 |
98 | ///Application identifier that will be logged with each call (along if user name, if authenticated).
99 | ///This string is logged as 'User' property of the API call
100 | AppId : string option
101 |
102 | PersonaText : string option
103 | PersonaSubText : string option
104 | Disclaimer : string option
105 |
106 | MetaIndex : string option
107 |
108 | ///This application can see indexes that are associated with the given groups.
109 | ///The index-to-group association is contained in the 'meta' index named in C.META_INDEX constant
110 | IndexGroups : string list
111 |
112 | ModelsConfig : ModelsConfig
113 |
114 | DatabaseName : string
115 |
116 | DiagTableName : string option
117 |
118 | SessionTableName : string option
119 |
120 | }
121 | with
122 | static member Default =
123 | {
124 | EnabledBackends = [OpenAI]
125 | EnabledChatModes = []//M_Plain,"You are a helpful AI assistant"]
126 | DefaultMaxDocs = 10
127 | Roles = []
128 | RequireLogin = false
129 | AppBarType = None
130 | AssistantIcon = None
131 | AssistantIconColor = None
132 | Disclaimer = None
133 | LogoUrl = Some "https://github.com/fwaris/FsOpenAI"
134 | MetaIndex = None
135 | IndexGroups = []
136 | ModelsConfig = ModelsConfig.Default
137 | AppId = None
138 | AppName = None
139 | PersonaText = None
140 | PersonaSubText = None
141 | DatabaseName = C.DFLT_COSMOSDB_NAME
142 | DiagTableName = None
143 | SessionTableName = None
144 | }
145 |
146 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/Constants.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Shared
2 | open Utils
3 |
4 | module C =
5 | let MAX_INTERACTIONS = 15
6 | let MAX_DOCLISTS_PER_CHAT = 3 //number of document lists that a chat can maintain (others are dropped as new doc lists are added to chat)
7 | let MAX_COMMENT_LENGTH = 10000;
8 |
9 | let CHAT_RESPONSE_TIMEOUT = 1000 * 30 //15 seconds
10 | let TIMEOUT_MSG = "timeout while waiting for service to respond"
11 |
12 | let LS_OPENAI_KEY = "LS_OPENAI_KEY"
13 | let MAIN_SETTINGS = "MAIN_SETTINGS"
14 | let ADD_CHAT_MENU = "ADD_CHAT_MENU"
15 | let CHATS = "CHATS"
16 |
17 | let DARK_THEME = "DARK_THEME"
18 | let SIDE_BAR_EXPANDED = "SIDE_BAR_EXPANDED"
19 |
20 | let MAX_VIDEO_FRAMES = 20
21 | let MAX_UPLOAD_FILE_SIZE = 1024L * 1024L * 15L
22 | let UPLOAD_EXT = ".fsopenai"
23 | let UPLOAD_FILE_STALENESS = System.TimeSpan.FromMinutes(-10.0)
24 | let SCAN_PERIOD = System.TimeSpan.FromMinutes(1.0)
25 |
26 | let defaultSystemMessage = "You are a helpful AI Assistant"
27 |
28 | let TEMPLATES_ROOT = lazy("app" @@ "Templates")
29 | let TMPLTS_EXTRACTION = "ExtractionSkill"
30 | let TMPLTS_DOCQUERY = "DocQuerySkill"
31 |
32 | let SAMPLES_JSON = "Samples.json"
33 | let APP_CONFIG_PATH = lazy("app" @@ "AppConfig.json")
34 |
35 | let DEFAULT_META_INDEX = "fsopenai-meta" //meta index name. allowed: lower case letters; digits; and dashes
36 |
37 | let VALIDATE_TOKEN_EXPIRY = "Parms:ValidateTokenExpiry" // validate token expiry on each client message
38 |
39 | let SETTINGS_FILE = "Parms:Settings" //1st: look for a reference to settings json file in appSettings
40 | let SETTINGS_FILE_ENV = "FSOPENAI_SETTINGS_FILE" //2nd: look for the whole settings file in this environment variable (base64 encoded json string)
41 | let FSOPENAI_AZURE_KEYVAULT = "FSOPENAI_AZURE_KEYVAULT" //3nd: look for a key vault name in this environment variable,
42 | let FSOPENAI_AZURE_KEYVAULT_KEY = "FSOPENAI_AZURE_KEYVAULT_KEY" // and key vault key name in this environment variable
43 | // Note: key vault key value is a base64 encoded json string
44 | let DFLT_COSMOSDB_NAME = "fsopenai"
45 | let DFLT_MONITOR_TABLE_NAME = "tmgenai-log1" //Note: follow Azure Table naming rules (no underscores or dashes)
46 | let UNAUTHENTICATED = "Unauthenticated"
47 | let DFLT_APP_ID = "default"
48 |
49 | let DFLT_ASST_ICON = "robot_2"
50 | let DFLT_ASST_ICON_COLOR = "var(--rz-primary)"
51 | let LOAD_CONFIG_ID = "fsopenai_h1"
52 |
53 | module ClientHub =
54 | let fromServer = "FromServer"
55 | let fromClient = "FromClient"
56 | let uploadStream = "UploadStream"
57 | let urlPath = "/fsopenaihub"
58 |
59 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/FsOpenAI.Shared.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | true
5 | Debug;Release;UNAUTHENTICATED
6 | AnyCPU
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/Interactions.CodeEval.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Shared.Interactions.CodeEval
2 | open System
3 | open FsOpenAI.Shared
4 |
5 | module Interaction =
6 | let codeBag (ch:Interaction) = ch.Types |> List.tryPick (function CodeEval c -> Some c | _ -> None)
7 |
8 | let setCode c ch =
9 | match codeBag ch with
10 | | Some _ -> {ch with Types = ch.Types |> List.map (function CodeEval bag -> CodeEval {bag with Code = c} | x -> x)}
11 | | None -> {ch with Types = (CodeEval {CodeEvalBag.Default with Code=c})::ch.Types}
12 |
13 | let setPlan p ch =
14 | match codeBag ch with
15 | | Some _ -> {ch with Types = ch.Types |> List.map (function CodeEval bag -> CodeEval {bag with Plan = p} | x -> x)}
16 | | None -> {ch with Types = (CodeEval {CodeEvalBag.Default with Plan=p})::ch.Types}
17 |
18 | let setEvalParms p ch =
19 | match codeBag ch with
20 | | Some _ -> {ch with Types = ch.Types |> List.map (function CodeEval bag -> CodeEval {bag with CodeEvalParms = p} | x -> x)}
21 | | None -> {ch with Types = (CodeEval {CodeEvalBag.Default with CodeEvalParms=p})::ch.Types}
22 |
23 |
24 | module Interactions =
25 | open FsOpenAI.Shared.Interactions.Core.Interactions
26 |
27 | let setCode id c cs = updateWith (Interaction.setCode c) id cs
28 | let setPlan id p cs = updateWith (Interaction.setPlan p) id cs
29 | let setEvalParms id p cs = updateWith (Interaction.setEvalParms p) id cs
30 |
31 | module CodeEvalPrompts =
32 |
33 | ///GPT models are good at F# code generation but stumble on F# string interpolation syntax.
34 | ///These instructions provide guidance on string interpolation as well general code generation
35 | let sampleCodeGenPrompt = """
36 | # String Formatting:
37 | - Use F# string interpolation in generated code, instead of the sprintf function.
38 | - Only format number as percentages if number is a percentage.
39 | - Remember in F# interpolated strings are prefixed with $.
40 | - Do not use $ in the string interpolation.
41 | - Remember to escape %% in the string interpolation for percentage formatting with %%%%.
42 |
43 | # Code generation:
44 | - The main function return signature should be of type string
45 | - It is the answer to the user query
46 | - In addition to user QUERY follow any PLAN and/or F# Type descriptions, if provided, to generate the code.
47 | - Only generate new code. Do not output any existing code
48 | - Do not create F# modules - use existing types and functions.
49 | - Always generate functions with curried arguments.
50 | - To create a set consider the 'set' operator instead of Set.ofXXX functions.
51 | - Ensure that code formatting (whitespace, etc.) is correct according to F# conventions.
52 | - Declare all constants at the top before the main function and then reference them in the code later
53 | - Do not use F# reserved keywords as variable names (e.g. 'end', 'as', etc.).
54 | - Put type annotations where needed so types are clear in the generated code.
55 | - Prefer using |> when working with lists (e.g. invoices |> List.map ...) so the types are better inferred.
56 | - When creating lists always use put the '[' on a new line, properly indented.
57 | - ALWAYS TYPE ANNOTATE THE LAMBDA FUNCTION PARAMETERS.
58 | - BE SURE TO ACTUALLY INVOKE THE GENERATED FUNCTION WITH THE APPROPRIATE ARGUMENTS TO RETURN THE FINAL RESULT.
59 | - ALWAYS PRINT THE FINAL RESULT TO THE CONSOLE
60 | """
61 |
62 | let regenPromptTemplate = """
63 | While compiling the given [F# CODE TO FIX] a compilation ERROR was encountered. Regenerate the code after fixing the error.
64 | Only fix the error. Do not change the code structure. The line number of the error may not be accurate. Refer to
65 | F# Types to fix code namely to properly type annotate the lambda function parameters.
66 |
67 | ERROR```
68 | {{{{$errorMessage}}}}
69 | ```
70 |
71 | [F# CODE TO FIX]```
72 | {{{{$code}}}}
73 | ```
74 |
75 | # Other Instructions
76 | FOLLOW F# WHITESPACE RULES
77 | ENSURE THAT THE F# CODE RETURNS A VALUE OF TYPE string. Fix the code if necessary.
78 |
79 | ```fsharp
80 |
81 | """
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/Interactions.Core.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Shared.Interactions.Core
2 | open FsOpenAI.Shared
3 |
4 | module Interactions =
5 | let private update f id (c:Interaction) = if c.Id = id then f c else c
6 | let updateWith f id cs = cs |> List.map(update f id)
7 | let replace id cs c = cs |> List.map(fun x -> if x.Id = id then c else x)
8 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/Settings.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Shared
2 | open System
3 | open System.Security.Claims
4 |
5 | type AzureOpenAIEndpoints =
6 | {
7 | API_KEY : string
8 | RESOURCE_GROUP : string
9 | }
10 |
11 | type ApiEndpoint =
12 | {
13 | API_KEY : string
14 | ENDPOINT : string
15 | }
16 |
17 | type ModelDeployments =
18 | {
19 | CHAT : string list
20 | COMPLETION : string list
21 | EMBEDDING : string list
22 | }
23 |
24 | type ServiceSettings =
25 | {
26 | AZURE_SEARCH_ENDPOINTS: ApiEndpoint list
27 | AZURE_OPENAI_ENDPOINTS: AzureOpenAIEndpoints list
28 | EMBEDDING_ENDPOINTS : AzureOpenAIEndpoints list
29 | BING_ENDPOINT : ApiEndpoint option
30 | //GOOGLE_KEY : string option
31 | OPENAI_KEY : string option
32 | LOG_CONN_STR : string option
33 | }
34 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Shared/Utils.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Shared
2 | open System
3 | open System.IO
4 | open System.Text.Json
5 | open System.Text.Json.Serialization
6 | open System.Security.Cryptography
7 |
8 | module Utils =
9 |
10 | let homePath = lazy(
11 | match Environment.OSVersion.Platform with
12 | | PlatformID.Unix
13 | | PlatformID.MacOSX -> Environment.GetEnvironmentVariable("HOME")
14 | | _ -> Environment.GetEnvironmentVariable("USERPROFILE"))
15 |
16 | let newId() =
17 | Guid.NewGuid().ToByteArray()
18 | |> Convert.ToBase64String
19 | |> Seq.takeWhile (fun c -> c <> '=')
20 | |> Seq.map (function '/' -> 'a' | c -> c)
21 | |> Seq.toArray
22 | |> String
23 |
24 | let notEmpty (s:string) = String.IsNullOrWhiteSpace s |> not
25 | let isEmpty (s:string) = String.IsNullOrWhiteSpace s
26 | let contains (s:string) (ptrn:string) = s.Contains(ptrn,StringComparison.CurrentCultureIgnoreCase)
27 |
28 | let isOpen key map = map |> Map.tryFind key |> Option.defaultValue false
29 |
30 | exception NoOpenAIKey of string
31 |
32 | let serOptions() =
33 | let o = JsonSerializerOptions(JsonSerializerDefaults.General)
34 | o.WriteIndented <- true
35 | o.ReadCommentHandling <- JsonCommentHandling.Skip
36 | JsonFSharpOptions.Default()
37 | //.WithUnionEncoding(JsonUnionEncoding.NewtonsoftLike) //consider for future as provides better roundtrip support
38 | .AddToJsonSerializerOptions(o)
39 | o
40 |
41 | let shorten len (s:string) = if s.Length > len then s.Substring(0,len) + "..." else s
42 |
43 | let (@@) a b = System.IO.Path.Combine(a,b)
44 |
45 | let idx (ptrn:string,start:int) (s:string) =
46 | let i = s.IndexOf(ptrn,start)
47 | if i < 0 then None else Some(i)
48 |
49 | let wrapBlock (s:string) = $"{s}
"
50 |
51 | let blockQuotes (msg:string) =
52 | let i1 = idx ("```",0) msg
53 | let i2 = i1 |> Option.bind (fun i -> idx ("\n",i) msg)
54 | let i3 = i2 |> Option.bind (fun i -> idx ("```",i) msg)
55 | match i1,i2,i3 with
56 | | None,_,_ -> msg
57 | | Some i1, None, None -> msg.Substring(0,i1) + (wrapBlock (msg.Substring(i1+3)))
58 | | Some i1, Some i2, None -> msg.Substring(0,i1) + wrapBlock (msg.Substring(i2))
59 | | Some i1, Some i2, Some i3 -> msg.Substring(0,i1) + wrapBlock(msg.Substring(i2+1,i3)) + msg.Substring(i3+3)
60 | | _ -> msg
61 |
62 |
63 | let genKey () =
64 | let key = Aes.Create()
65 | key.GenerateKey()
66 | key.GenerateIV()
67 | key.Key,key.IV
68 |
69 | let encrFile (key,iv) (path:string)(outpath) =
70 | use enc = Aes.Create()
71 | enc.Mode <- CipherMode.CBC
72 | enc.Key <- key
73 | enc.IV <- iv
74 | use inStream = new FileStream(path, FileMode.Open)
75 | use outStream = new FileStream(outpath, FileMode.Create)
76 | use encStream = new CryptoStream(outStream, enc.CreateEncryptor(), CryptoStreamMode.Write)
77 | inStream.CopyTo(encStream)
78 |
79 | let decrFile (key,iv) (path:string) (outpath:string) =
80 | use enc = Aes.Create()
81 | enc.Mode <- CipherMode.CBC
82 | enc.Key <- key
83 | enc.IV <- iv
84 | use inStream = new FileStream(path, FileMode.Open)
85 | use decrStream = new CryptoStream(inStream, enc.CreateDecryptor(), CryptoStreamMode.Read)
86 | use outStream = new FileStream(outpath, FileMode.Create)
87 | decrStream.CopyTo(outStream)
88 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/FsOpenAI.Tasks.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | true
6 | Debug;Release;UNAUTHENTICATED
7 | AnyCPU
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/Library.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Tasks
2 |
3 | module Say =
4 | let hello name =
5 | printfn "Hello %s" name
6 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/client/app/imgs/Persona.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Tasks/deployments/default/client/app/imgs/Persona.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/client/app/imgs/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Tasks/deployments/default/client/app/imgs/favicon.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/client/app/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwaris/FsOpenAI/fdf360acb35352387827ae9a755e79f09de254f0/src/FsOpenAI.Tasks/deployments/default/client/app/imgs/logo.png
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/client/appsettings.json.template:
--------------------------------------------------------------------------------
1 | {
2 | "Comment" : {
3 | "1":"Copy contents of this file to appsettings.json and place it in the same location as this",
4 | "2":"In the 'AzureAd' section set the appropriate values from the MS EntraID app registration for MSAL authentication.",
5 | "3":"appsettings.json is 'gitignored' as it may contain proprietary information"
6 | },
7 | "AzureAd": {
8 | "Authority": "https://login.microsoftonline.com/",
9 | "ClientId": "",
10 | "ValidateAuthority": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/client/theme-override.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --rz-primary-test: #a7df58 !important;
3 | }
4 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/indexed_create.fsx:
--------------------------------------------------------------------------------
1 | #load "../../scripts/ScriptEnv.fsx"
2 | open System.IO
3 | open FsOpenAI.Shared
4 | open FsOpenAI.GenAI
5 | open FSharp.Control
6 | open ScriptEnv
7 |
8 | //'install' settings so secrets are available to script code
9 | let baseSettingsFile = @".fsopenai/poc/ServiceSettings.json"
10 | ScriptEnv.installSettings (Utils.homePath.Value @@ baseSettingsFile)
11 |
12 | //Notes:
13 | // - settings should contain an endpoint for Azure AI Search and OpenAI API key
14 | // - Azure offers a free tier for AI Search that can be used for this purpose
15 |
16 |
17 | let createCitations folder =
18 | //create citations.csv index file so hyperlinks to pdf pages can work - this is a requirement for the app
19 | let pdfs = Directory.GetFiles(folder, "*.pdf")
20 | let citations = pdfs |> Seq.map (fun f -> $"file:///{f},{Path.GetFileName(f)}") |> Seq.toList
21 | do
22 | ["Link,Document"]
23 | @ citations
24 | |> fun lines -> File.WriteAllLines (Path.Combine(folder, "citations.csv"),lines)
25 |
26 | //shred and index the pdfs
27 | let embModel = "text-embedding-ada-002"
28 | let clientFac() = ScriptEnv.openAiEmbeddingClient embModel
29 |
30 | let indexFolder indexName path =
31 | createCitations path
32 | let indexDef = ScriptEnv.Indexes.indexDefinition indexName
33 | ScriptEnv.Indexes.shredPdfsAsync path
34 | |> ScriptEnv.Indexes.getEmbeddingsAsync clientFac 7.0
35 | |> AsyncSeq.map ScriptEnv.Indexes.toSearchDoc
36 | |> ScriptEnv.Indexes.loadIndexAsync true indexDef
37 |
38 | //ai papers
39 | let docFolder = @"E:\s\genai\papers" //folder with pdfs to index
40 |
41 | //ML books
42 | let b1 = @"E:\s\genai\bishop"
43 | let b2 = @"E:\s\genai\mml"
44 |
45 |
46 | (*
47 | indexFolder "genai-papers" docFolder |> Async.RunSynchronously
48 | indexFolder "pattern-recognition" b1 |> Async.Start
49 | indexFolder "ml-math" b2 |> Async.Start
50 | *)
51 |
52 | //ScriptEnv.Indexes.shredPdfsAsync b1 |> AsyncSeq.take 10 |> AsyncSeq.toBlockingSeq |> Seq.iter (fun x -> printfn "%A" x)
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/server/appsettings.json.template:
--------------------------------------------------------------------------------
1 | {
2 | "Comment" : {
3 | "1":"Copy contents of this file to appsettings.json and place it in the same location as this"
4 | "2":"In the 'AzureAd' section set the appropriate values from the MS EntraID app registration for MSAL authentication.",
5 | "3":"appsettings.json is 'gitignored' as it may contain proprietary information"
6 | },
7 | "Parms": {
8 | "Settings": ".fsopenai/ServiceSettings.json"
9 | },
10 | "AzureAd": {
11 | "Instance": "https://login.microsoftonline.com/",
12 | "Domain": "",
13 | "TenantId": "",
14 | "ClientId": "",
15 | "Audience": "api://..."
16 | },
17 | "Logging": {
18 | "LogLevel": {
19 | "Microsoft.AspNetCore.Hosting": "Warning"
20 | }
21 | },
22 | "DebugLogging": {
23 | "LogLevel": {
24 | "Default": "Debug",
25 | "System": "Information",
26 | "Microsoft": "Information",
27 | "Microsoft.AspNetCore.SignalR": "Debug",
28 | "Microsoft.AspNetCore.Http.Connections": "Debug"
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/DocQuerySkill/Contextual/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": 1,
3 | "description": "Combine document and search results when answering question",
4 | "execution_settings": {
5 | "default": {
6 | "max_tokens": 2000,
7 | "temperature": 0.1,
8 | "top_p": 1.0,
9 | "presence_penalty": 0.0,
10 | "frequency_penalty": 0.0
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/DocQuerySkill/Contextual/question.txt:
--------------------------------------------------------------------------------
1 | Analyze the DOCUMENT with respect to the Accounting Policies in SEARCH RESULTS and provide a side-by-side comparison of how the content in DOCUMENT affects the existing content.
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/DocQuerySkill/Contextual/skprompt.txt:
--------------------------------------------------------------------------------
1 | DOCUMENT: '''
2 | {{$document}}
3 | '''
4 |
5 | SEARCH RESULTS: '''
6 | {{$searchResults}}
7 | '''
8 |
9 | Analyze the DOCUMENT in relation to SEARCH RESULTS for ANSWERING QUESTIONS.
10 | BE CONTEXTUAL.
11 | If you don't know, ask.
12 | If you are not sure, ask.
13 | Based on calculates from TODAY
14 | TODAY is {{$date}}
15 |
16 | QUESTION: '''
17 | {{$question}}
18 | '''
19 |
20 | ANSWER:
21 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/DocQuerySkill/ContextualBrief/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": 1,
3 | "description": "Combine document and search results when answering question. Produce brief output",
4 | "execution_settings": {
5 | "default": {
6 | "max_tokens": 2000,
7 | "temperature": 0.1,
8 | "top_p": 1.0,
9 | "presence_penalty": 0.0,
10 | "frequency_penalty": 0.0
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/DocQuerySkill/ContextualBrief/question.txt:
--------------------------------------------------------------------------------
1 | Analyze the DOCUMENT with respect to the Accounting Policies in SEARCH RESULTS and provide a side-by-side comparison of how the content in DOCUMENT affects the existing content.
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/DocQuerySkill/ContextualBrief/skprompt.txt:
--------------------------------------------------------------------------------
1 | DOCUMENT: '''
2 | {{$document}}
3 | '''
4 |
5 | SEARCH RESULTS: '''
6 | {{$searchResults}}
7 | '''
8 |
9 | Analyze the DOCUMENT in relation to SEARCH RESULTS for ANSWERING QUESTIONS.
10 | BE BRIEF AND TO THE POINT, BUT WHEN SUPPLYING OPINION, IF YOU SEE THE NEED, YOU CAN BE LONGER.
11 | BE CONTEXTUAL.
12 | If you don't know, ask.
13 | If you are not sure, ask.
14 | Based on calculates from TODAY
15 | TODAY is {{$date}}
16 |
17 | QUESTION: '''
18 | {{$question}}
19 | '''
20 |
21 | ANSWER:
22 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/ExtractionSkill/Concepts/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": 1,
3 | "description": "Ask AI to extract concepts and key words from document for the purpose of search",
4 | "execution_settings": {
5 | "default": {
6 | "max_tokens": 2000,
7 | "temperature": 0.1,
8 | "top_p": 1.0,
9 | "presence_penalty": 0.0,
10 | "frequency_penalty": 0.0
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/ExtractionSkill/Concepts/skprompt.txt:
--------------------------------------------------------------------------------
1 | DOCUMENT: '''
2 | {{$document}}
3 | '''
4 |
5 | Analyze the DOCUMENT and extract terms and concepts which can be used to perform lookup. Be sure to include any Finance terms and standards in the query.
6 |
7 | DONT GENEARTE SQL. JUST LIST THE TERMS AS COMMA-SEPARATED VALUES
8 |
9 | QUERY:
10 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/deployments/default/templates/default/Samples.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "SampleChatType": {
4 | "Case": "SM_IndexQnA",
5 | "Fields": [
6 | "ml-docs"
7 | ]
8 | },
9 | "SampleSysMsg": "You are a helpful AI assistant",
10 | "SampleQuestion": "How to prevent overfitting?",
11 | "SampleMode": {
12 | "Case": "Factual"
13 | },
14 | "MaxDocs": 5
15 | },
16 | {
17 | "SampleChatType": {
18 | "Case": "SM_CodeEval"
19 | },
20 | "SampleSysMsg": "ignored",
21 | "SampleQuestion": "Write recursive function for the Fibonacci series and return the value of the 15th element",
22 | "SampleMode": {
23 | "Case": "Factual"
24 | },
25 | "MaxDocs": 5
26 | },
27 | {
28 | "SampleChatType": {
29 | "Case": "SM_CodeEval"
30 | },
31 | "SampleSysMsg": "ignored",
32 | "SampleQuestion": "Write a function to download a page from the internet and download \u0027https://www.google.com\u0027 display results",
33 | "SampleMode": {
34 | "Case": "Factual"
35 | },
36 | "MaxDocs": 5
37 | },
38 | {
39 | "SampleChatType": {
40 | "Case": "SM_Plain",
41 | "Fields": [
42 | false
43 | ]
44 | },
45 | "SampleSysMsg": "You are a helpful AI assistant",
46 | "SampleQuestion": "What are the major languages and dialects spoken in France? Only list the language names",
47 | "SampleMode": {
48 | "Case": "Factual"
49 | },
50 | "MaxDocs": 5
51 | }
52 | ]
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/CopyAppSettings.fsx:
--------------------------------------------------------------------------------
1 | //find all appsettings.json files in a folder structure and copy them
2 | //to a new folder structure that mimics the original folder structure
3 | //(but only has appsettings.json files)
4 | //Because appsettings.json files are not committed to repo, this script
5 | //is useful for extracting these files and later merging them back
6 | //in into a new local instance of the repo
7 |
8 | open System
9 | open System.IO
10 |
11 | let inputFolder = @"C:\venv\source\reposTemp\TM_FsOpenAI"
12 | let outputFolder = @"C:\venv\source\reposTemp\TM_FsOpenAI_AppSettings"
13 |
14 | let isAppSettings (path:string) = Path.GetFileName(path).Equals("appsettings.json", StringComparison.OrdinalIgnoreCase)
15 | let excludeDirs = set ["bin";"obj";".git";".vs";".vscode";".git"]
16 |
17 | let makeLowerCase (path:string) =
18 | let dir = Path.GetDirectoryName(path)
19 | let file = Path.GetFileName(path).ToLower()
20 | Path.Combine(dir, file)
21 |
22 | let getAppSettings (root:string) =
23 | let rec loop acc (path:string) =
24 | let files = Directory.GetFiles(path, "*.json")
25 | let files =
26 | files
27 | |> Seq.filter (fun f ->
28 | f.ToLower().Split([|'/';'\\'|])
29 | |> Array.exists (fun f -> excludeDirs.Contains(f)) |> not)
30 | let appSettings = files |> Seq.filter isAppSettings |> Seq.toList
31 | let appSettings = appSettings |> List.map makeLowerCase
32 | let acc = acc @ appSettings
33 | let dirs = Directory.GetDirectories(path)
34 | (acc,dirs) ||> Array.fold loop
35 | loop [] root
36 |
37 | if Directory.Exists(outputFolder) then
38 | Directory.Delete(outputFolder, true)
39 |
40 | for apps in (getAppSettings inputFolder) do
41 | let relativePath = Path.GetRelativePath(inputFolder, apps)
42 | let outputPath = Path.Combine(outputFolder, relativePath)
43 | Directory.CreateDirectory(Path.GetDirectoryName(outputPath)) |> ignore
44 | File.Copy(apps, outputPath, true)
45 | printfn "Copied %s to %s" apps outputPath
46 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/CosmoDB.fsx:
--------------------------------------------------------------------------------
1 | #load "ScriptEnv.fsx"
2 | open System
3 | open FSharp.CosmosDb
4 | open FSharp.Control
5 | //example on how to pull log data from Azure CosmosDB
6 |
7 | type SREf = {[] id:string; [] UserId:string; Timestamp:DateTime}
8 |
9 | let baseSettingsFile = @"%USERPROFILE%/.fsopenai/poc/ServiceSettings.json"
10 | ScriptEnv.installSettings baseSettingsFile
11 | let cstr = ScriptEnv.settings.Value.LOG_CONN_STR.Value
12 |
13 | let db() =
14 | Cosmos.fromConnectionString cstr
15 | |> Cosmos.database FsOpenAI.Shared.C.DFLT_COSMOSDB_NAME
16 | |> Cosmos.container FsOpenAI.Shared.C.DFLT_MONITOR_TABLE_NAME
17 |
18 | let findDrops() =
19 | db()
20 | |> Cosmos.query("select c.id, c.UserId, c.Timestamp from c where c.AppId = ''")
21 | |> Cosmos.execAsync
22 | |> AsyncSeq.toBlockingSeq
23 | |> Seq.toList
24 |
25 | let drop (drops:SREf list) =
26 | drops
27 | |> AsyncSeq.ofSeq
28 | |> AsyncSeq.iterAsync (fun c ->
29 | db()
30 | |> Cosmos.deleteItem c.id c.UserId
31 | |> Cosmos.execAsync
32 | |> AsyncSeq.iterAsync Async.Ignore)
33 | |> Async.Start
34 |
35 | let toDrop = findDrops()
36 |
37 | drop toDrop
38 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/CreateSettings.fsx:
--------------------------------------------------------------------------------
1 | #load "ScriptEnv.fsx"
2 | open FsOpenAI.Shared
3 | open System
4 | open System.IO
5 | open System.Text.Json
6 |
7 | //shows how to create a settings json in a type-safe way
8 |
9 | let serOpts = Utils.serOptions()
10 | let settings =
11 | {
12 | LOG_CONN_STR = None // Some "cosmosdb connection string"
13 | AZURE_OPENAI_ENDPOINTS = [{API_KEY ="api key"; RESOURCE_GROUP="rg"}]
14 | AZURE_SEARCH_ENDPOINTS = []
15 | EMBEDDING_ENDPOINTS = []
16 | BING_ENDPOINT = None // Some {API_KEY = "bing key"; ENDPOINT="https://bing.com"}
17 | OPENAI_KEY = None
18 | // GOOGLE_KEY = None
19 | }
20 | let str = JsonSerializer.Serialize(settings,serOpts)
21 | printfn $"{str}"
22 |
23 | let str2 = File.ReadAllText (Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\.fsopenai/fsopenai1server/ServiceSettings.json"))
24 | let str3 = str2 |> Text.Encoding.UTF8.GetBytes |> Convert.ToBase64String
25 | printfn $"{str3}"
26 | let settings' = JsonSerializer.Deserialize(str2, serOpts)
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/ListOpenAIModels.fsx:
--------------------------------------------------------------------------------
1 | #r "nuget: System.Text.Json"
2 | open System
3 | open System.Text.Json
4 | open System.IO
5 | open System.Net.Http
6 |
7 | //List all models available - for the configured openai key
8 |
9 | let toDate (d:int64) = DateTimeOffset.FromUnixTimeSeconds(d).Date
10 |
11 | let jsonDoc =
12 | use wc = new HttpClient()
13 | let key = System.Environment.GetEnvironmentVariable("OPENAI_API_KEY")
14 | wc.DefaultRequestHeaders.Authorization <- System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",key)
15 | wc.GetStringAsync("https://api.openai.com/v1/models") |> Async.AwaitTask |> Async.RunSynchronously
16 |
17 | let doc = System.Text.Json.JsonDocument.Parse(jsonDoc);;
18 | ;;
19 | doc.RootElement.EnumerateObject()
20 | |> Seq.collect(fun d -> if d.Name = "data" then d.Value.EnumerateArray() |> Seq.cast else Seq.empty)
21 | |> Seq.map(fun n -> n.GetProperty("id").GetString(), n.GetProperty("created").GetInt64() |> toDate)
22 | //|> Seq.filter (fun (m,d) -> m.StartsWith("gpt"))
23 | |> Seq.sortByDescending snd
24 | |> Seq.iter (fun (m,d) -> printfn "%s" $"{m}: {d.ToShortDateString()}")
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/LoadIndex.fsx:
--------------------------------------------------------------------------------
1 | #load "ScriptEnv.fsx"
2 | open FSharp.Control
3 | (*
4 | Example script that loads a collection of PDF documents in a local folder
5 | to an Azure vector search index
6 | *)
7 |
8 | ScriptEnv.installSettings "%USERPROFILE%/.fsopenai/ServiceSettings.json"
9 | let embModel = "text-embedding-ada-002"
10 | let clientFac() = ScriptEnv.openAiEmbeddingClient embModel
11 | let shredded = ScriptEnv.Indexes.shredHtmlAsync @"C:\s\glean\plans"
12 | let embedded = ScriptEnv.Indexes.getEmbeddingsAsync clientFac 7.0 shredded
13 | let searchDocs = embedded |> AsyncSeq.map ScriptEnv.Indexes.toSearchDoc
14 | let indexDef = ScriptEnv.Indexes.indexDefinition "plans"
15 | ScriptEnv.Indexes.loadIndexAsync true indexDef searchDocs |> Async.Start
16 |
17 |
18 | //let docs = Env.Index.shredHtmlAsync @"C:\s\glean\plans" |> AsyncSeq.toBlockingSeq |> Seq.toList
19 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/Sandbox.fsx:
--------------------------------------------------------------------------------
1 | //need .net sdk 9 installed
2 | #load "ScriptEnv.fsx"
3 | open System
4 | open System.IO
5 | open System.Text.Json
6 | open System.Text.Json.Serialization
7 | open FsOpenAI
8 | open FsOpenAI.GenAI
9 |
10 | let fn = @"C:\s\gc\Untitled-1.json" |> File.ReadAllText
11 | let serOpts = Sessions.sessionOptions.Value
12 |
13 | let fno = JsonSerializer.Deserialize(fn, serOpts)
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/SerializeSettings.fsx:
--------------------------------------------------------------------------------
1 | #load "ScriptEnv.fsx"
2 | open System
3 | open System.Text
4 | open System.IO
5 | open FsOpenAI.Shared
6 | open Utils
7 |
8 | let path = homePath.Value @@ ".fsopenai/ServiceSettings.json"
9 |
10 | let base64 = path |> File.ReadAllText |> Encoding.UTF8.GetBytes |> Convert.ToBase64String
11 | printfn "%s" base64
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/TemplateParsingSandbox.fsx:
--------------------------------------------------------------------------------
1 | open System
2 |
3 | let testSuite =
4 | [
5 | """{{ bing.search "SEC regulations and guidelines applicable to special factors" }}"""
6 | "The weather today is {{weather.getForecast}}"
7 | "The weather today in {{$city}} is {{weather.getForecast $city}}"
8 | """The weather today in Schio is {{weather.getForecast "Schio"}}"""
9 | "{{ bing.search \"\"SEC regulations and guidelines applicable to special factors\"\" }}."
10 | ]
11 |
12 | module TemplateParser =
13 | type Block = VarBlock of string | FuncBlock of string*string option
14 |
15 | []
16 | module internal StateMachine =
17 | let MAX_LITERAL = 1000
18 | let eof = Seq.toArray ""
19 | let inline error x xs = failwithf "%s got %s" x (String(xs |> Seq.truncate 100 |> Seq.toArray))
20 |
21 | let c2s cs = cs |> List.rev |> Seq.toArray |> String
22 |
23 | let toVar = c2s >> VarBlock
24 | let toFunc1 cs = FuncBlock (c2s cs,None)
25 | let toFunc2 cs vs = FuncBlock(c2s cs, Some (c2s vs))
26 |
27 | let rec start (acc:Block list) = function
28 | | [] -> acc
29 | | '{'::rest -> brace1 acc rest
30 | | _::rest -> start acc rest
31 | and brace1 acc = function
32 | | [] -> error "expected {" eof
33 | | '{'::rest -> brace2 acc rest
34 | | x::rest -> error "expected {" rest
35 | and brace2 acc = function
36 | | [] -> error "expecting $ after {{" eof
37 | | '$'::rest -> beginVar [] acc rest
38 | | c::rest when Char.IsWhiteSpace c -> brace2 acc rest
39 | | c::rest when c <> '}' && c <> '{' -> beginFunc [] acc (c::rest)
40 | | xs -> error "Expected '$'" xs
41 | and beginVar vacc acc = function
42 | | [] -> error "expecting }" eof
43 | | '}'::rest -> braceEnd1 ((toVar vacc)::acc) rest
44 | | c::rest when (Char.IsWhiteSpace c) -> braceEnd1 ((toVar vacc)::acc) rest
45 | | x::rest -> beginVar (x::vacc) acc rest
46 | and braceEnds acc = function
47 | | [] -> error "expecting }}" eof
48 | | c::rest when Char.IsWhiteSpace c -> braceEnds acc rest
49 | | c::rest when c = '}' -> braceEnd1 acc rest
50 | | c::rest -> error "expected }}" rest
51 | and braceEnd1 acc = function
52 | | [] -> error "expecting }" eof
53 | | '}'::rest -> start acc rest
54 | | ' '::rest -> braceEnd1 acc rest //can ignore whitespace
55 | | xs -> error "expecting }}" xs
56 | and beginFunc facc acc = function
57 | | [] -> error "expecting function name" eof
58 | | c::rest when Char.IsWhiteSpace c -> beginParm [] facc acc rest
59 | | c::rest when c = '}' -> braceEnd1 ((toFunc1 facc)::acc) rest
60 | | c::rest -> beginFunc (c::facc) acc rest
61 | and beginParm pacc facc acc = function
62 | | [] -> error "expecting function call parameter" eof
63 | | c::rest when Char.IsWhiteSpace c -> beginParm pacc facc acc rest
64 | | c::rest when c = '$' -> beginParmVar (c::pacc) facc acc rest
65 | | c::rest when c = '"' -> beginParmLit [] facc acc rest
66 | | c::rest -> beginParmVar (c::pacc) facc acc rest
67 | and beginParmVar pacc facc acc = function
68 | | [] -> error "expecting parameter name after $" eof
69 | | c::rest when Char.IsWhiteSpace c -> braceEnds ((toFunc2 facc pacc)::acc) rest
70 | | c::rest when c = '}' -> braceEnd1 ((toFunc2 facc pacc)::acc) rest
71 | | c::rest -> beginParmVar (c::pacc) facc acc rest
72 | and beginParmLit pacc facc acc = function
73 | | [] -> error """expecting " """ eof
74 | | c::rest when (List.length pacc > MAX_LITERAL) -> error "max literal size exceeded" rest
75 | | c::rest when c = '"' -> braceEnds ((toFunc2 facc pacc)::acc) rest
76 | | c::rest -> beginParmLit (c::pacc) facc acc rest
77 |
78 |
79 | let extractVars templateStr =
80 | start [] (templateStr |> Seq.toList)
81 | |> List.distinct
82 |
83 |
84 | let vss = testSuite |> List.map(fun x->x.Replace("\"\"","\"")) |> List.map TemplateParser.extractVars
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/TestOpenAIClient.fsx:
--------------------------------------------------------------------------------
1 | #r "nuget: Azure.AI.OpenAI, *-*"
2 | #r "nuget: FSharp.Control.AsyncSeq"
3 |
4 | open System
5 | open FSharp.Control
6 | open OpenAI
7 |
8 | //Shows how to get streaming content for chat completions
9 |
10 | let client = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY"))
11 | let chatc = client.GetChatClient("gpt-4o-mini")
12 |
13 | let m1 = Chat.ChatMessage.CreateUserMessage("What is the meaning of life?")
14 |
15 | let resp = chatc.CompleteChatStreamingAsync(m1)
16 | let gs =
17 | let mutable ls = []
18 | resp
19 | |> AsyncSeq.ofAsyncEnum
20 | |> AsyncSeq.collect(fun cs -> AsyncSeq.ofSeq cs.ContentUpdate)
21 | |> AsyncSeq.map(fun x -> x.Text)
22 | |> AsyncSeq.iter(fun x-> printfn "%A" x; ls<-x::ls)
23 | |> Async.RunSynchronously
24 | String.concat "" (List.rev ls)
25 | printf "%s" gs
--------------------------------------------------------------------------------
/src/FsOpenAI.Tasks/scripts/packages.fsx:
--------------------------------------------------------------------------------
1 | #r "nuget: Microsoft.Extensions.Http"
2 | #r "nuget: Microsoft.Extensions.Hosting"
3 | #r "nuget: Microsoft.AspNetCore.Components.WebAssembly, 8.0.10"
4 | #r "nuget: Microsoft.Extensions.DependencyInjection"
5 | #r "nuget: Microsoft.DeepDev.TokenizerLib"
6 | #r "nuget: Azure.Search.Documents"
7 | #r "nuget: Microsoft.SemanticKernel"
8 | #r "nuget: FSharp.SystemTextJson"
9 | #r "nuget: FSharp.Data"
10 | #r "nuget: FSharp.Collections.ParallelSeq"
11 | #r "nuget: FSharp.Control.AsyncSeq"
12 | #r "nuget: FsPickler.Json"
13 | #r "nuget: Docnet.Core"
14 | #r "nuget: PdfPig, *-*"
15 | #r "nuget: DocumentFormat.OpenXml"
16 | #r "nuget: OpenCvSharp4.Windows"
17 | #r "nuget: OpenCvSharp4.Extensions"
18 | #r "nuget: Azure.Identity"
19 | #r "nuget: azure.security.keyvault.secrets"
20 | #r "nuget: FSharp.CosmosDb"
21 | #r "nuget: ExcelDataReader"
22 | #r "nuget: ExcelDataReader.DataSet"
23 | #r "nuget: FSharp.Compiler.Service"
24 | #r "nuget: System.Text.Encoding.CodePages"
25 | #r "nuget: FSharp.Data.Html.Core"
26 |
27 | //transient packages upgraded to remove security warnings
28 | #r "nuget: System.Text.RegularExpressions"
29 | #r "nuget: NewtonSoft.json"
30 | #r "nuget: System.Net.Http"
31 | #r "nuget: System.Private.Uri"
32 |
33 | #I "../../FsOpenAI.Shared"
34 | #load "Utils.fs"
35 | #load "Constants.fs"
36 | #load "AppConfig.fs"
37 | #load "Settings.fs"
38 | #load "Types.fs"
39 | #load "Interactions.Core.fs"
40 | #load "Interactions.fs"
41 |
42 | #I "../../FsOpenAI.Vision"
43 | #load "Image.fs"
44 | #load "Video.fs"
45 | #load "VisionApi.fs"
46 |
47 | #I "../../FsOpenAI.GenAI"
48 | #load "AsyncExts.fs"
49 | #load "Env.fs"
50 | #load "Connection.fs"
51 | #load "Sessions.fs"
52 | #load "Monitoring.fs"
53 | #load "Gen/SemanticVectorSearch.fs"
54 | #load "Gen/StreamParser.fs"
55 | #load "Gen/TemplateParser.fs"
56 | #load "Gen/Models.fs"
57 | #load "Gen/Tokens.fs"
58 | #load "Gen/ChatUtils.fs"
59 | #load "Gen/Endpoints.fs"
60 | #load "Gen/SKernel.fs"
61 | #load "Gen/GenUtils.fs"
62 | #load "Gen/Prompts.fs"
63 | #load "Gen/Indexes.fs"
64 | #load "Gen/Completions.fs"
65 | #load "Gen/WebCompletion.fs"
66 | #load "Gen/IndexQnA.fs"
67 | #load "Gen/DocQnA.fs"
68 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Vision/FsOpenAI.Vision.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | true
6 | Debug;Release;UNAUTHENTICATED
7 | AnyCPU
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Vision/Image.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Vision
2 | open System
3 | open System.IO
4 |
5 | module ImageUtils =
6 |
7 | let imagePath docPath pageNum imageNum = docPath + $".{pageNum}_{imageNum}.jpeg"
8 | let imageTextPath imagePath = imagePath + ".txt"
9 |
10 | []
11 | module Extraction =
12 | open System.Drawing
13 | open System.Drawing.Imaging
14 | open UglyToad.PdfPig
15 | open DocumentFormat.OpenXml.Packaging
16 | open DocumentFormat.OpenXml.Presentation
17 |
18 | let exportImagesToDiskWord (filePath:string) =
19 | try
20 | use d = WordprocessingDocument.Open(filePath,false)
21 | d.MainDocumentPart.ImageParts
22 | |> Seq.iteri(fun i img ->
23 | let imgPath = ImageUtils.imagePath filePath 0 i
24 | printfn $"Image {i} : {imgPath}"
25 | use bmp = Bitmap.FromStream(img.GetStream())
26 | bmp.Save(imgPath,ImageFormat.Jpeg))
27 | with ex ->
28 | printfn $"Error in extractImagesWord {filePath} : {ex.Message}"
29 |
30 | let exportImagesToDiskPpt (filePath:string) =
31 | try
32 | use d = PresentationDocument.Open(filePath,false)
33 | let images =
34 | d.PresentationPart.SlideParts
35 | |> Seq.collect(fun slidePart ->
36 | slidePart.Slide.Descendants()
37 | |> Seq.choose(fun p ->
38 | try
39 | let part = slidePart.GetPartById(p.BlipFill.Blip.Embed) :?> ImagePart
40 | Some part
41 | with ex ->
42 | printfn $"error extracting as image - part {p.BlipFill.Blip.Embed.InnerText}"
43 | None))
44 | |> Seq.toList
45 | images
46 | |> List.iteri (fun i img ->
47 | let imgPath = ImageUtils.imagePath filePath 0 i
48 | printfn $"Image {i} : {imgPath}"
49 | use bmp = Bitmap.FromStream(img.GetStream())
50 | bmp.Save(imgPath,ImageFormat.Jpeg))
51 | with ex ->
52 | printfn $"Error in extractImagesWord {filePath} : {ex.Message}"
53 |
54 |
55 | ///Extracts any jpeg images in pdf and saves to disk.
56 | ///Image file paths are _{page#}_{image#}.jpeg
57 | let exportImagesToDisk (path:string) =
58 | let pdf = PdfDocument.Open(path);
59 |
60 | let encoders = ImageCodecInfo.GetImageDecoders();
61 | let jpegEncoder = encoders |> Seq.find (fun enc -> enc.FormatID = ImageFormat.Jpeg.Guid)
62 |
63 | for page in pdf.GetPages() do
64 | let images = page.GetImages() |> Seq.toArray
65 | let mutable j = 0
66 | for i in images do
67 | let mutable pngBytes: byte[] = Unchecked.defaultof<_>
68 | let d = i.TryGetPng(&pngBytes)
69 | use stream = new MemoryStream(if pngBytes = null then i.RawBytes.ToArray() else pngBytes)
70 | use image = Image.FromStream(stream, false, false);
71 | let fn = ImageUtils.imagePath path page.Number j
72 | image.Save(fn, jpegEncoder, null);
73 | j <- j + 1
74 |
75 | Console.WriteLine($"Extracted images from {path}")
76 |
77 |
78 | ///Utility function to list jpeg images contained in a pdf file. Can be used to check if file has any embedded images.
79 | ///Returns: page#, image#, PdfDictionary (which can be used with exportImage function to save image to disk)
80 | let listImages (path:string) =
81 | seq {
82 | use pdf = PdfDocument.Open(path);
83 | for page in pdf.GetPages() do
84 | let images = page.GetImages() |> Seq.indexed
85 | for (i,img) in images do
86 | yield page.Number, i, img
87 |
88 | }
89 | |> Seq.toList
90 |
91 | []
92 | module Conversion =
93 | open System.Drawing
94 | open System.Drawing.Imaging
95 |
96 | let addBytes (bitmap:Bitmap) (bytes:byte[]) =
97 | let rect = new Rectangle(0,0,bitmap.Width,bitmap.Height)
98 | let bmpData = bitmap.LockBits(rect,ImageLockMode.ReadWrite,bitmap.PixelFormat)
99 | let ptr = bmpData.Scan0
100 | let bytesCount = bytes.Length
101 | System.Runtime.InteropServices.Marshal.Copy(bytes,0,ptr,bytesCount)
102 | bitmap.UnlockBits(bmpData)
103 |
104 | let jpegEncoder (mimeType:string) =
105 | ImageCodecInfo.GetImageEncoders()
106 | |> Array.tryFind (fun codec -> codec.MimeType = mimeType)
107 |
108 | let saveBmp (bmp:Bitmap) (outPath:string) =
109 | let qualityEncoder = Encoder.Quality;
110 | use qualityParameter = new EncoderParameter(qualityEncoder, 90);
111 | use encoderParms = new EncoderParameters(1)
112 | encoderParms.Param.[0] <- qualityParameter
113 | let codec = jpegEncoder "image/jpeg" |> Option.defaultWith (fun _ -> failwith "jpeg codec not found")
114 | bmp.Save(outPath,codec,encoderParms)
115 |
116 | ///Export entire pages as jpeg images to disk
117 | ///Image file paths are _{page#}_0.jpeg
118 | let exportImagesToDisk (backgroundRGB:(byte*byte*byte) option) (path:string) =
119 | use inst = Docnet.Core.DocLib.Instance
120 | use reader = inst.GetDocReader(path,Docnet.Core.Models.PageDimensions(1.0))
121 | [0 .. reader.GetPageCount()-1]
122 | |> List.iter (fun i ->
123 | use page = reader.GetPageReader(i)
124 | let imgBytes =
125 | match backgroundRGB with
126 | | Some (red,green,blue) -> page.GetImage(new Docnet.Core.Converters.NaiveTransparencyRemover(red,blue,green))
127 | | None -> page.GetImage()
128 | let w,h = page.GetPageWidth(),page.GetPageHeight()
129 | use bmp = new Bitmap(w,h,PixelFormat.Format32bppArgb)
130 | addBytes bmp imgBytes
131 | let outPath = ImageUtils.imagePath path i 0
132 | saveBmp bmp outPath)
133 |
134 | ///Export entire pages as jpeg images to disk
135 | ///Image file paths are _{page#}_0.jpeg
136 | let exportImagesToDiskScaled (backgroundRGB:(byte*byte*byte) option) (scale:float) (path:string) =
137 | use inst = Docnet.Core.DocLib.Instance
138 | use reader = inst.GetDocReader(path,Docnet.Core.Models.PageDimensions(scale))
139 | [0 .. reader.GetPageCount()-1]
140 | |> List.iter (fun i ->
141 | use page = reader.GetPageReader(i)
142 | let imgBytes =
143 | match backgroundRGB with
144 | | Some (red,green,blue) -> page.GetImage(new Docnet.Core.Converters.NaiveTransparencyRemover(red,blue,green))
145 | | None -> page.GetImage()
146 | let w,h = page.GetPageWidth(),page.GetPageHeight()
147 | use bmp = new Bitmap(w,h,PixelFormat.Format32bppArgb)
148 | addBytes bmp imgBytes
149 | let outPath = ImageUtils.imagePath path i 0
150 | saveBmp bmp outPath)
151 |
--------------------------------------------------------------------------------
/src/FsOpenAI.Vision/Video.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Vision
2 | (* commented out to reduce application package size - to deploy to free azure tier
3 | open OpenCvSharp
4 | open System.Drawing
5 | open System.IO
6 | open FSharp.Control
7 |
8 | module Video =
9 | //returns the number of frames, fps, width, height and format of the video
10 | let getInfo f =
11 | use clipIn = new VideoCapture(f:string)
12 | let fc = clipIn.FrameCount
13 | clipIn.FrameCount,clipIn.Fps,clipIn.FrameWidth,clipIn.FrameHeight,string clipIn.Format
14 |
15 | let readFrame (clipIn:VideoCapture) n =
16 | async {
17 | let _ = clipIn.PosFrames <- n
18 | use mat = new Mat()
19 | let resp =
20 | if clipIn.Read(mat) then
21 | let ptr = mat.CvPtr
22 | use bmp = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(mat)
23 | use ms = new MemoryStream()
24 | bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png)
25 | ms.Position <- 0L
26 | Some(ms.ToArray())
27 | else
28 | None
29 | mat.Release()
30 | return resp
31 | }
32 |
33 | let getFrames file maxFrames =
34 | asyncSeq {
35 | use clipIn = new VideoCapture(file:string)
36 | let frames =
37 | if clipIn.FrameCount <= maxFrames then
38 | [0..clipIn.FrameCount-1]
39 | else
40 | let skip = clipIn.FrameCount / maxFrames
41 | [
42 | yield 0 // keep first
43 | for i in 1..maxFrames-1 do // evenly spaced frames
44 | yield i*skip
45 | yield clipIn.FrameCount-1 // keep last
46 | ]
47 | |> set // remove duplicates
48 | |> Seq.toList
49 | |> List.sort
50 | for n in frames do
51 | let! frame = readFrame clipIn n
52 | yield frame
53 | clipIn.Release()
54 | }
55 | *)
--------------------------------------------------------------------------------
/src/FsOpenAI.Vision/VisionApi.fs:
--------------------------------------------------------------------------------
1 | namespace FsOpenAI.Vision
2 | open System
3 | open System.Net.Http
4 | open System.Text
5 | open System.Text.Json
6 | open System.Text.Json.Serialization
7 |
8 | type ImageUrl(url:string) =
9 | member val url : string = url with get, set
10 |
11 | []
12 | [)>]
13 | [)>]
14 | type Content(t:string) =
15 | member val ``type`` : string = t with get, set
16 |
17 | and TextContent(text:string) =
18 | inherit Content("text")
19 | member val text : string = text with get, set
20 |
21 | and ImageContent(data:string) =
22 | inherit Content("image_url")
23 | member val image_url = ImageUrl(data) with get, set
24 |
25 | type Message (role:string, cs:Content list) =
26 |
27 | member val role : string = role with get, set
28 | member val content : Content list = cs with get, set
29 |
30 | type Payload(msgs:Message list) =
31 | member val messages = msgs
32 | member val temperature = 0.7 with get, set
33 | member val top_p = 0.95 with get, set
34 | member val max_tokens = 800 with get,set
35 | member val stream = false with get, set
36 | member val model:string = null with get, set
37 |
38 | type RMsg =
39 | {
40 | role : string
41 | content : string
42 | }
43 |
44 | type RChoice = {
45 | message : RMsg
46 | }
47 |
48 | type RUsage =
49 | {
50 | completion_tokens : int
51 | prompt_tokens : int
52 | total_tokens : int
53 | }
54 |
55 | type Response =
56 | {
57 | choices : RChoice list
58 | usage : RUsage
59 | }
60 |
61 | module VisionApi =
62 | open System.Net.Http.Headers
63 |
64 | let serOptions() =
65 | let o = JsonSerializerOptions(JsonSerializerDefaults.General)
66 | o.WriteIndented <- true
67 | JsonFSharpOptions.Default()
68 | .WithAllowNullFields(true)
69 | .WithAllowOverride(true)
70 | .AddToJsonSerializerOptions(o)
71 | o
72 | let processVision (ep:Uri) (key:string) (user:string) (payload:Payload) =
73 | task {
74 | //let url = $"https://{ep.RESOURCE_GROUP}.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-07-01-preview";
75 | use client = new HttpClient()
76 | client.DefaultRequestHeaders.Add("api-key",key)
77 | client.DefaultRequestHeaders.Add("fsopenai-user",user)
78 | client.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", key)
79 | let productValue = new ProductInfoHeaderValue("fsopenai", "1.0")
80 | client.DefaultRequestHeaders.UserAgent.Add(productValue)
81 | let data = JsonSerializer.Serialize(payload,serOptions())
82 | use content = new StringContent(data, Encoding.UTF8, "application/json")
83 | let! resp = client.PostAsync(ep,content)
84 | if resp.IsSuccessStatusCode then
85 | let! respData = resp.Content.ReadAsStringAsync()
86 | let resp = JsonSerializer.Deserialize(respData,serOptions())
87 | return Some resp
88 | else
89 | printfn $"Error: {resp.StatusCode}, {resp.ReasonPhrase}"
90 | return None
91 | }
92 |
93 |
--------------------------------------------------------------------------------