Skip to content

Commit

Permalink
public init
Browse files Browse the repository at this point in the history
  • Loading branch information
nilsherzig committed Mar 29, 2024
0 parents commit 2817271
Show file tree
Hide file tree
Showing 52 changed files with 7,466 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
31 changes: 31 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
backend/tmp
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM node:20-alpine3.19 AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production

FROM node:20-alpine3.19
WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
ENV NODE_ENV=production
CMD [ "node", "build" ]
5 changes: 5 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:20-alpine3.19
WORKDIR /app
EXPOSE 5173

CMD ["sh", "-c", "npm install && npm run dev -- --host"]
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
PHONY: build-dev
build-dev:
docker-compose -f ./docker-compose.dev.yaml build

PHONY: dev
dev: build-dev
docker-compose -f ./docker-compose.dev.yaml up

PHONY: run
run: build-dev
docker-compose -f ./docker-compose.dev.yaml up -d

PHONY: stop
stop:
docker-compose -f ./docker-compose.dev.yaml down

PHONY: update
update:
git pull

PHONY: upgrade
upgrade: stop update build-dev run
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# LLM search engine

## Status

This is a proof of concept, the code is horrible. I didn't intend to make this public yet, but I wanted to share it with a few people.
Please open issues and PRs if you have any suggestions.

## How it works

1. The user query is sent to server
2. The server starts an agent chain
3. The agent (in our case `starling-lm` running on Ollama) will process the query and select one of its tools
- Websearch (using SearXNG) will scrape the top `n` results from the web (the agent will choose the search query). Remove all unnecessary information (like html tags) and store the result into a vector db
- SearchVectorDB will search the vector db for the most similar results to the query
4. Step 3 will happen multiple times, with the agent choosing different tools and combining the results
5. The final result is sent back to the user and displayed in the frontend

While this is happening, the user can see the progress of the agents steps in the frontend.

## Running / Development

### Requirements

- A running [Ollama](https://ollama.com/) server somewhere in your network
- a model named `search:latest`
- (I recommend `hermes-2-pro-mistral` or `starling-lm-7b-beta`)
- `all-minilm` for embeddings
- Docker Compose

Included in the compose file are
- search backend (based on the Go Langchain library)
- search frontend (svelte & tailwind)
- Chroma DB (for storing search results in vector space)
- SearXNG (meta search engine used by the agent chain)
- Redis (for caching search results)

```
git clone https://github.com/nilsherzig/LocalSearchUI.git
# make sure to check the env vars inside the compose file
# build the containers and start the services
make dev
```

## Roadmap

### backend

- Automatic LLM download to Ollama if not present
- Forced backlinks in final answer
- I have no idea how to do this, besides asking the LLM haha
- Force the search LLM to respond in JSON with links?
- Another round of formatting LLM after this to include these links into the final text?
- Persistent user accounts
- add your own files
- select splitter values based on complexity of the question
- broad vs narrow

### interface

- CLI version
- "tree view" for actions
- different LLM backends (like OpenAI or Anthropic)
- settings table for different models
- Ollama model select dropdown
- would require a lot of other values to be exposed to the UI
- render latex
46 changes: 46 additions & 0 deletions backend/.air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
main_only = false
time = false

[misc]
clean_on_exit = false

[screen]
clear_on_rebuild = false
keep_scroll = true
33 changes: 33 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Stage 1: Build
FROM golang:1.21.7 AS builder

# Set the Current Working Directory inside the container
WORKDIR /app

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download

# Copy the source from the current directory to the Working Directory inside the container
COPY . .

# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .

# Stage 2: Run
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/main .

# Expose port 8080 to the outside world
EXPOSE 8080

# Command to run the executable
CMD ["./main"]
7 changes: 7 additions & 0 deletions backend/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM golang:alpine3.19

WORKDIR /app
RUN go install github.com/cosmtrek/air@latest
EXPOSE 8080

CMD ["air"]
118 changes: 118 additions & 0 deletions backend/agentChain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"context"
"fmt"
"log"
"log/slog"
"time"

"github.com/nilsherzig/localLLMSearch/llm_tools"
"github.com/nilsherzig/localLLMSearch/utils"
"github.com/tmc/langchaingo/agents"
"github.com/tmc/langchaingo/chains"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/tools"
)

func startAgentChain(ctx context.Context, outputChan chan<- utils.HttpJsonStreamElement, userQuery utils.ClientQuery) error {
defer func() {
// send a close message to the client when the function ends
// the client can use this to close the connection gracefully
// outputChan <- utils.HttpJsonStreamElement{
// Close: true,
// }

// this defer acts as a try catch block around the current function
// this prevents the whole server from crashing when an error occurs

// TODO: figure out how to stop the "sending on closed channel" error
// when the client disconnects
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
}
}()

// used to set the vector db namespace
startTime := time.Now()
session := utils.GetSessionString()
slog.Info("Starting agent chain", "session", session, "userQuery", userQuery, "startTime", startTime)

// llm, err := utils.NewGPT35()
// llm, err := utils.NewGPT4()
llm, err := utils.NewOllama()
// llm, err :=
if err != nil {
log.Printf("Error creating new LLM: %v", err)
return err
}

agentTools := []tools.Tool{
tools.Calculator{},
llm_tools.WebSearch{
CallbacksHandler: utils.CustomHandler{
OutputChan: outputChan,
},
SessionString: session,
},
llm_tools.SearchVectorDB{
CallbacksHandler: utils.CustomHandler{
OutputChan: outputChan,
},
SessionString: session,
},
// llm_tools.Feedback{
// CallbacksHandler: utils.CustomHandler{},
// Query: userQuery,
// Llm: llm,
// },
}

executor, err := agents.Initialize(
llm,
agentTools,
agents.ZeroShotReactDescription,
agents.WithParserErrorHandler(agents.NewParserErrorHandler(func(s string) string {
outputChan <- utils.HttpJsonStreamElement{
Message: fmt.Sprintf("Parsing Error. %s", s),
Stream: false,
}
return utils.ParsingErrorPrompt()
})),

agents.WithMaxIterations(userQuery.MaxIterations),
agents.WithCallbacksHandler(utils.CustomHandler{
OutputChan: outputChan,
}),
)

if err != nil {
return err
}

temp := 0.0

finalAnswer, err := chains.Run(ctx, executor, userQuery.Prompt, chains.WithTemperature(temp))
if err != nil {
return err
}

newAns, err := llm.Call(ctx, utils.FormatTextAsMArkdownPrompt(finalAnswer),
llms.WithTemperature(temp),
llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
outputChan <- utils.HttpJsonStreamElement{
StepType: utils.StepHandleFinalAnswer,
Message: string(chunk),
Stream: true,
}
return nil
}),
)
if err != nil {
return err
}
_ = newAns
slog.Info("finished chain", "session", session, "duration", time.Since(startTime), "finalAnswer", newAns)

return nil
}
Loading

0 comments on commit 2817271

Please sign in to comment.