Routing refactored
This commit is contained in:
parent
e044f9c639
commit
9f7ddca90a
114
Dockerfile
114
Dockerfile
@ -154,15 +154,6 @@ RUN apt-get update \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
||||
|
||||
# The client frontend is built using React Expo to allow
|
||||
# easy creation of an Android app as well as web app
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
||||
|
||||
# Install Intel graphics runtimes
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common \
|
||||
@ -215,8 +206,8 @@ RUN pip install intel-extension-for-pytorch==2.6.10+xpu oneccl_bind_pt==2.6.0+xp
|
||||
|
||||
# From https://huggingface.co/docs/bitsandbytes/main/en/installation?backend=Intel+CPU+%2B+GPU#multi-backend
|
||||
RUN pip install "transformers>=4.45.1"
|
||||
RUN pip install 'https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.44.1.dev0-py3-none-manylinux_2_24_x86_64.whl'
|
||||
|
||||
#RUN pip install 'https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.44.1.dev0-py3-none-manylinux_2_24_x86_64.whl'
|
||||
RUN pip install 'https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.45.3.dev272-py3-none-manylinux_2_24_x86_64.whl'
|
||||
# Install ollama python module
|
||||
RUN pip install ollama langchain-ollama
|
||||
|
||||
@ -247,16 +238,19 @@ RUN pip3 install 'bigdl-core-xe-all>=2.6.0b'
|
||||
# NOTE: IPEX includes the oneAPI components... not sure if they still need to be installed separately with a oneAPI env
|
||||
RUN pip install einops diffusers # Required for IPEX optimize(), which is required to convert from Params4bit
|
||||
|
||||
# Needed by src/utils/chroma.py
|
||||
# Needed by src/utils/rag.py
|
||||
RUN pip install watchdog
|
||||
|
||||
# Install packages needed for stock.py
|
||||
RUN pip install yfinance pyzt geopy PyHyphen nltk
|
||||
# Install packages needed for utils/tools/*
|
||||
RUN pip install yfinance pyzt geopy
|
||||
|
||||
# Install packages needed for vector operations
|
||||
RUN pip install umap-learn
|
||||
|
||||
FROM llm-base AS backstory
|
||||
|
||||
COPY /src/requirements.txt /opt/backstory/src/requirements.txt
|
||||
RUN pip install -r /opt/backstory/src/requirements.txt
|
||||
#COPY /src/requirements.txt /opt/backstory/src/requirements.txt
|
||||
#RUN pip install -r /opt/backstory/src/requirements.txt
|
||||
RUN pip install 'markitdown[all]' pydantic
|
||||
|
||||
# Prometheus
|
||||
@ -283,14 +277,6 @@ RUN { \
|
||||
echo ' exec /bin/bash'; \
|
||||
echo ' fi' ; \
|
||||
echo 'else'; \
|
||||
echo ' if [[ "${PRODUCTION}" -eq 0 ]]; then'; \
|
||||
echo ' while true; do'; \
|
||||
echo ' cd /opt/backstory/frontend'; \
|
||||
echo ' echo "Launching Backstory React Frontend..."'; \
|
||||
echo ' npm start "${@}" || echo "Backstory frontend died. Restarting in 3 seconds."'; \
|
||||
echo ' sleep 3'; \
|
||||
echo ' done &' ; \
|
||||
echo ' fi' ; \
|
||||
echo ' if [[ ! -e src/cert.pem ]]; then' ; \
|
||||
echo ' echo "Generating self-signed certificate for HTTPS"'; \
|
||||
echo ' openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout src/key.pem -out src/cert.pem -subj "/C=US/ST=OR/L=Portland/O=Development/CN=localhost"'; \
|
||||
@ -320,11 +306,6 @@ ENV SYCL_CACHE_PERSISTENT=1
|
||||
ENV PATH=/opt/backstory:$PATH
|
||||
|
||||
COPY /src/ /opt/backstory/src/
|
||||
COPY /frontend/ /opt/backstory/frontend/
|
||||
|
||||
WORKDIR /opt/backstory/frontend
|
||||
RUN npm install --force
|
||||
WORKDIR /opt/backstory
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
|
||||
@ -454,6 +435,14 @@ ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
|
||||
FROM llm-base AS jupyter
|
||||
|
||||
# npm and Node.JS are required for jupyterlab
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
||||
|
||||
SHELL [ "/opt/backstory/shell" ]
|
||||
|
||||
# BEGIN setup Jupyter
|
||||
@ -463,9 +452,8 @@ RUN pip install \
|
||||
&& jupyter lab build --dev-build=False --minimize=False
|
||||
# END setup Jupyter
|
||||
|
||||
COPY /src/requirements.txt /opt/backstory/src/requirements.txt
|
||||
|
||||
RUN pip install -r /opt/backstory/src/requirements.txt
|
||||
#COPY /src/requirements.txt /opt/backstory/src/requirements.txt
|
||||
#RUN pip install -r /opt/backstory/src/requirements.txt
|
||||
|
||||
RUN pip install timm xformers
|
||||
|
||||
@ -572,3 +560,65 @@ RUN { \
|
||||
&& chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
|
||||
FROM ubuntu:oracular AS frontend
|
||||
|
||||
# The client frontend is built using React Expo to allow
|
||||
# easy creation of an Android app as well as web app
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
nodejs \
|
||||
npm \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
RUN { \
|
||||
echo '#!/bin/bash'; \
|
||||
echo 'echo "Container: frontend"'; \
|
||||
echo 'set -e'; \
|
||||
echo ''; \
|
||||
echo 'if [[ "${1}" == "/bin/bash" ]] || [[ "${1}" =~ ^(/opt/backstory/)?shell$ ]]; then'; \
|
||||
echo ' echo "Dropping to shell"'; \
|
||||
echo ' shift' ; \
|
||||
echo ' echo "Running: ${@}"' ; \
|
||||
echo ' if [[ "${1}" != "" ]]; then' ; \
|
||||
echo ' exec ${@}'; \
|
||||
echo ' else' ; \
|
||||
echo ' exec /bin/bash'; \
|
||||
echo ' fi' ; \
|
||||
echo 'fi' ; \
|
||||
echo 'cd /opt/backstory/frontend'; \
|
||||
echo 'if [[ "${1}" == "install" ]] || [[ ! -d node_modules ]]; then'; \
|
||||
echo ' echo "Installing node modules"'; \
|
||||
echo ' if [[ -d node_modules ]]; then'; \
|
||||
echo ' echo "Deleting current node_modules"'; \
|
||||
echo ' rm -rf node_modules'; \
|
||||
echo ' fi'; \
|
||||
echo ' npm install --force'; \
|
||||
echo 'fi'; \
|
||||
echo 'if [[ "${1}" == "build" ]]; then'; \
|
||||
echo ' echo "Building production static build"'; \
|
||||
echo ' ./build.sh'; \
|
||||
echo 'fi'; \
|
||||
echo 'while true; do'; \
|
||||
echo ' echo "Launching Backstory React Frontend..."'; \
|
||||
echo ' npm start "${@}" || echo "Backstory frontend died. Restarting in 3 seconds."'; \
|
||||
echo ' sleep 3'; \
|
||||
echo 'done' ; \
|
||||
} > /entrypoint.sh \
|
||||
&& chmod +x /entrypoint.sh
|
||||
|
||||
WORKDIR /opt/backstory/frontend
|
||||
|
||||
RUN { \
|
||||
echo '#!/bin/bash' ; \
|
||||
echo 'if [[ "${1}" != "" ]]; then bash -c "${@}"; else bash; fi' ; \
|
||||
} > /opt/backstory/shell ; \
|
||||
chmod +x /opt/backstory/shell
|
||||
|
||||
COPY /frontend/ /opt/backstory/frontend/
|
||||
ENV PATH=/opt/backstory:$PATH
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
|
@ -91,7 +91,9 @@ This project provides the following containers:
|
||||
|
||||
| Container | Purpose |
|
||||
|:----------|:---------------------------------------------------------------|
|
||||
| backstory | Base container with GPU packages installed and configured. Main server entry point. Also used for frontend development. |
|
||||
| backstory | Base container with GPU packages installed and configured. Main server entry point. Exposes an HTTPS entrypoint for use by frontend development |
|
||||
| backstory-prod | Base container with GPU packages installed and configured. Main server entry point. Exposes an HTTP entrypoint for exposing via nginx or other reverse proxy server. Serves static files generated by frontend. |
|
||||
| frontend | Frontend development and building static file for backstory-prod. |
|
||||
| jupyter | backstory + Jupyter notebook for running Jupyter sessions |
|
||||
| miniircd | Tiny deployment of an IRC server for testing IRC agents |
|
||||
| ollama | Installation of Intel's pre-built Ollama.cpp |
|
||||
|
@ -6,7 +6,7 @@ services:
|
||||
target: backstory
|
||||
container_name: backstory
|
||||
image: backstory
|
||||
restart: "no"
|
||||
restart: "always"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@ -19,8 +19,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- 8912:8911 # Flask React server
|
||||
- 3000:3000 # REACT expo while developing frontend
|
||||
- 8912:8911 # FastAPI React server
|
||||
volumes:
|
||||
- ./cache:/root/.cache # Persist all models and GPU kernel cache
|
||||
- ./sessions:/opt/backstory/sessions:rw # Persist sessions
|
||||
@ -28,7 +27,6 @@ services:
|
||||
- ./dev-keys:/opt/backstory/keys:ro # Developer keys
|
||||
- ./docs:/opt/backstory/docs:rw # Live mount of RAG content
|
||||
- ./src:/opt/backstory/src:rw # Live mount server src
|
||||
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src
|
||||
cap_add: # used for running ze-monitor within container
|
||||
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
|
||||
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
|
||||
@ -54,18 +52,33 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- 8911:8911 # Flask React server
|
||||
- 8911:8911 # FastAPI React server
|
||||
volumes:
|
||||
- ./cache:/root/.cache # Persist all models and GPU kernel cache
|
||||
- ./chromadb-prod:/opt/backstory/chromadb:rw # Persist ChromaDB
|
||||
- ./sessions-prod:/opt/backstory/sessions:rw # Persist sessions
|
||||
- ./docs-prod:/opt/backstory/docs:rw # Live mount of RAG content
|
||||
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src
|
||||
- ./frontend/deployed:/opt/backstory/deployed:ro # Live mount built frontend
|
||||
cap_add: # used for running ze-monitor within container
|
||||
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
|
||||
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
|
||||
- CAP_SYS_PTRACE # PTRACE_MODE_READ_REALCREDS ptrace access mode check
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: frontend
|
||||
container_name: frontend
|
||||
image: frontend
|
||||
restart: "always"
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- 3000:3000 # REACT expo while developing frontend
|
||||
volumes:
|
||||
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src
|
||||
|
||||
ollama:
|
||||
build:
|
||||
context: .
|
||||
@ -116,19 +129,6 @@ services:
|
||||
volumes:
|
||||
- ./jupyter:/opt/jupyter:rw
|
||||
- ./cache:/root/.cache
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: "0" # No memory limit (Docker treats 0 as unlimited)
|
||||
reservations:
|
||||
memory: "0" # No reserved memory (optional)
|
||||
ulimits:
|
||||
memlock: -1 # Prevents memory from being locked
|
||||
#oom_kill_disable: true # Prevents OOM killer from killing the container
|
||||
cap_add: # used for running ze-monitor within container
|
||||
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
|
||||
- CAP_PERFMON # Access to perf_events (vs. overloaded CAP_SYS_ADMIN)
|
||||
- CAP_SYS_PTRACE # PTRACE_MODE_READ_REALCREDS ptrace access mode check
|
||||
|
||||
miniircd:
|
||||
build:
|
||||
@ -140,8 +140,6 @@ services:
|
||||
restart: "no"
|
||||
env_file:
|
||||
- .env
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
ports:
|
||||
- 6667:6667 # IRC
|
||||
networks:
|
||||
@ -153,10 +151,6 @@ services:
|
||||
image: prom/prometheus
|
||||
container_name: prometheus
|
||||
restart: "always"
|
||||
# env_file:
|
||||
# - .env
|
||||
# devices:
|
||||
# - /dev/dri:/dev/dri
|
||||
ports:
|
||||
- 9090:9090 # Prometheus
|
||||
networks:
|
||||
|
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@ -32,6 +32,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-spinners": "^0.15.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@ -18629,6 +18630,50 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
|
||||
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
|
||||
"dependencies": {
|
||||
"react-router": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||
@ -19781,6 +19826,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
@ -27,6 +27,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-spinners": "^0.15.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
|
@ -19,10 +19,16 @@
|
||||
|
||||
## Some questions I've been asked
|
||||
|
||||
Q. <ChatQuery prompt="Why aren't you providing this as a Platform As a Service (PaaS) application?" tunables={{ "enable_tools": false }} />
|
||||
Q. <ChatQuery query={
|
||||
prompt: "Why aren't you providing this as a Platform As a Service (PaaS) application?",
|
||||
tunables: { "enable_tools": false }
|
||||
} />
|
||||
|
||||
A. I could; but I don't want to store your data. I also don't want to have to be on the hook for support of this service. I like it, it's fun, but it's not what I want as my day-gig, you know? If it was, I wouldn't be looking for a job...
|
||||
|
||||
Q. <ChatQuery prompt="Why can't I just ask Backstory these questions?" tunables={{ "enable_tools": false }} />
|
||||
Q. <ChatQuery query={
|
||||
prompt: "Why can't I just ask Backstory these questions?",
|
||||
tunables: { "enable_tools": false }
|
||||
} />
|
||||
|
||||
A. Try it. See what you find out :)
|
||||
|
@ -1,402 +1,40 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Card from '@mui/material/Card';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Box from '@mui/material/Box';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
import React, { useRef, useCallback } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
|
||||
import { SessionWrapper } from "./SessionWrapper";
|
||||
import { Main } from "./Main";
|
||||
import { Snack, SeverityType } from './Snack';
|
||||
import { ConversationHandle } from './Conversation';
|
||||
import { QueryOptions } from './ChatQuery';
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { BackstoryPage, BackstoryTabProps } from './BackstoryTab';
|
||||
|
||||
import { connectionBase } from './Global';
|
||||
export function PathRouter({ setSnack }: { setSnack: any }) {
|
||||
const location = useLocation();
|
||||
const segments = location.pathname.split("/").filter(Boolean);
|
||||
const sessionId = segments[segments.length - 1];
|
||||
|
||||
import { HomePage } from './HomePage';
|
||||
import { ResumeBuilderPage } from './ResumeBuilderPage';
|
||||
import { VectorVisualizerPage } from './VectorVisualizer';
|
||||
import { AboutPage } from './AboutPage';
|
||||
import { ControlsPage } from './ControlsPage';
|
||||
|
||||
|
||||
import './App.css';
|
||||
import './Conversation.css';
|
||||
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
|
||||
|
||||
const isValidUUIDv4 = (str: string): boolean => {
|
||||
const pattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89ab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/i;
|
||||
return pattern.test(str);
|
||||
return (
|
||||
<SessionWrapper setSnack={setSnack}>
|
||||
<Main setSnack={setSnack} sessionId={sessionId} />
|
||||
</SessionWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [isMenuClosing, setIsMenuClosing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const isDesktop = useMediaQuery('(min-width:650px)');
|
||||
const prevIsDesktopRef = useRef<boolean>(isDesktop);
|
||||
const chatRef = useRef<ConversationHandle>(null);
|
||||
const snackRef = useRef<any>(null);
|
||||
const [subRoute, setSubRoute] = useState<string>("");
|
||||
function App() {
|
||||
const snackRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevIsDesktopRef.current === isDesktop)
|
||||
return;
|
||||
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
||||
snackRef.current?.setSnack(message, severity);
|
||||
}, [snackRef]);
|
||||
|
||||
if (menuOpen) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
prevIsDesktopRef.current = isDesktop;
|
||||
}, [isDesktop, setMenuOpen, menuOpen])
|
||||
|
||||
const setSnack = useCallback((message: string, severity?: SeverityType) => {
|
||||
snackRef.current?.setSnack(message, severity);
|
||||
}, [snackRef]);
|
||||
|
||||
const handleSubmitChatQuery = (prompt: string, tunables?: QueryOptions) => {
|
||||
console.log(`handleSubmitChatQuery: ${prompt} ${tunables || {}} -- `, chatRef.current ? ' sending' : 'no handler');
|
||||
chatRef.current?.submitQuery(prompt, tunables);
|
||||
setActiveTab(0);
|
||||
};
|
||||
|
||||
const tabs: BackstoryTabProps[] = useMemo(() => {
|
||||
const homeTab: BackstoryTabProps = {
|
||||
label: "",
|
||||
path: "",
|
||||
tabProps: {
|
||||
label: "Backstory",
|
||||
sx: { flexGrow: 1, fontSize: '1rem' },
|
||||
icon:
|
||||
<Avatar sx={{
|
||||
width: 24,
|
||||
height: 24
|
||||
}}
|
||||
variant="rounded"
|
||||
alt="Backstory logo"
|
||||
src="/logo192.png" />,
|
||||
iconPosition: "start"
|
||||
},
|
||||
children: <HomePage ref={chatRef} {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
|
||||
};
|
||||
|
||||
const resumeBuilderTab: BackstoryTabProps = {
|
||||
label: "Resume Builder",
|
||||
path: "resume-builder",
|
||||
children: <ResumeBuilderPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
|
||||
};
|
||||
|
||||
const contextVisualizerTab: BackstoryTabProps = {
|
||||
label: "Context Visualizer",
|
||||
path: "context-visualizer",
|
||||
children: <VectorVisualizerPage sx={{ p: 1 }} {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
|
||||
};
|
||||
|
||||
const aboutTab = {
|
||||
label: "About",
|
||||
path: "about",
|
||||
children: <AboutPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
|
||||
};
|
||||
|
||||
const controlsTab: BackstoryTabProps = {
|
||||
path: "controls",
|
||||
tabProps: {
|
||||
sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' },
|
||||
icon: <SettingsIcon />
|
||||
},
|
||||
children: (
|
||||
<Scrollable
|
||||
autoscroll={false}
|
||||
sx={{
|
||||
maxWidth: "1024px",
|
||||
height: "calc(100vh - 72px)",
|
||||
flexDirection: "column",
|
||||
margin: "0 auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{sessionId !== undefined &&
|
||||
<ControlsPage {...{ sessionId, setSnack, submitQuery: handleSubmitChatQuery, route: subRoute, setRoute: setSubRoute }} />
|
||||
}
|
||||
</Scrollable>
|
||||
)
|
||||
};
|
||||
|
||||
return [
|
||||
homeTab,
|
||||
resumeBuilderTab,
|
||||
contextVisualizerTab,
|
||||
aboutTab,
|
||||
controlsTab,
|
||||
];
|
||||
}, [sessionId, setSnack, subRoute]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined || activeTab > tabs.length - 1) { return; }
|
||||
console.log(`route - '${tabs[activeTab].path}', subRoute - '${subRoute}'`);
|
||||
|
||||
let path = tabs[activeTab].path ? `/${tabs[activeTab].path}` : '';
|
||||
if (subRoute) {
|
||||
path += `/${subRoute}`;
|
||||
}
|
||||
path += `/${sessionId}`;
|
||||
console.log('pushState: ', path);
|
||||
// window.history.pushState({}, '', path);
|
||||
}, [activeTab, sessionId, subRoute, tabs]);
|
||||
|
||||
const fetchSession = useCallback((async (pathParts?: string[]) => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/context`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw Error("Server is temporarily down.");
|
||||
}
|
||||
const new_session = (await response.json()).id;
|
||||
console.log(`Session created: ${new_session}`);
|
||||
|
||||
if (pathParts === undefined) {
|
||||
setSessionId(new_session);
|
||||
const newPath = `/${new_session}`;
|
||||
window.history.replaceState({}, '', newPath);
|
||||
} else {
|
||||
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
|
||||
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
|
||||
if (-1 === tabIndex) {
|
||||
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
|
||||
window.history.replaceState({}, '', `/${new_session}`);
|
||||
setActiveTab(0);
|
||||
} else {
|
||||
window.history.replaceState({}, '', `/${pathParts.join('/')}/${new_session}`);
|
||||
// tabs[tabIndex].route = pathParts[2] || "";
|
||||
setActiveTab(tabIndex);
|
||||
}
|
||||
setSessionId(new_session);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
setSnack("Server is temporarily down", "error");
|
||||
}
|
||||
}), [setSnack, tabs]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId]
|
||||
|
||||
if (pathParts.length < 1) {
|
||||
console.log("No session id or path -- creating new session");
|
||||
fetchSession();
|
||||
} else {
|
||||
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
|
||||
const path_session = pathParts.length < 2 ? pathParts[0] : pathParts[1];
|
||||
if (!isValidUUIDv4(path_session)) {
|
||||
console.log(`Invalid session id ${path_session}-- creating new session`);
|
||||
fetchSession();
|
||||
} else {
|
||||
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
|
||||
if (-1 === tabIndex) {
|
||||
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
|
||||
tabIndex = 0;
|
||||
}
|
||||
// tabs[tabIndex].route = pathParts[2] || ""
|
||||
setSessionId(path_session);
|
||||
setActiveTab(tabIndex);
|
||||
}
|
||||
}
|
||||
}, [setSessionId, setSnack, tabs, fetchSession]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setIsMenuClosing(true);
|
||||
setMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleMenuTransitionEnd = () => {
|
||||
setIsMenuClosing(false);
|
||||
};
|
||||
|
||||
const handleMenuToggle = () => {
|
||||
if (!isMenuClosing) {
|
||||
setMenuOpen(!menuOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
if (newValue > tabs.length) {
|
||||
return;
|
||||
}
|
||||
setActiveTab(newValue);
|
||||
const tabPath = tabs[newValue].path;
|
||||
let path = `/${sessionId}`;
|
||||
if (tabPath) {
|
||||
// if (openDocument) {
|
||||
// path = `/${tabPath}/${openDocument}/${sessionId}`;
|
||||
// } else {
|
||||
path = `/${tabPath}/${sessionId}`;
|
||||
// }
|
||||
}
|
||||
window.history.pushState({}, '', path);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
const url = new URL(window.location.href);
|
||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
|
||||
const session = pathParts.length < 2 ? pathParts[0] : pathParts[1];
|
||||
|
||||
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
|
||||
if (-1 === tabIndex) {
|
||||
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
|
||||
tabIndex = 0;
|
||||
}
|
||||
setSessionId(session);
|
||||
setActiveTab(tabIndex);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, [setSessionId, tabs]);
|
||||
|
||||
/* toolbar height is 64px + 8px margin-top */
|
||||
const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' }));
|
||||
|
||||
return (
|
||||
<Box className="App"
|
||||
sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
maxWidth: "100vw"
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
|
||||
{!isDesktop &&
|
||||
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
|
||||
<IconButton
|
||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={handleMenuToggle}
|
||||
>
|
||||
<Tooltip title="Navigation">
|
||||
<MenuIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Tooltip title="Backstory">
|
||||
<Box
|
||||
sx={{ m: 1, gap: 1, display: "flex", flexDirection: "row", alignItems: "center", fontWeight: "bold", fontSize: "1.0rem", cursor: "pointer" }}
|
||||
onClick={() => { setActiveTab(0); setMenuOpen(false); }}
|
||||
>
|
||||
<Avatar sx={{
|
||||
width: 24,
|
||||
height: 24
|
||||
}}
|
||||
variant="rounded"
|
||||
alt="Backstory logo"
|
||||
src="/logo192.png" />
|
||||
BACKSTORY
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
|
||||
{menuOpen === false && isDesktop &&
|
||||
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
||||
value={activeTab}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="fullWidth"
|
||||
allowScrollButtonsMobile
|
||||
onChange={handleTabChange}
|
||||
aria-label="Backstory navigation">
|
||||
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
|
||||
</Tabs>
|
||||
}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
</AppBar>
|
||||
|
||||
<Offset />
|
||||
|
||||
<Box
|
||||
sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }}
|
||||
>
|
||||
<Box
|
||||
component="nav"
|
||||
aria-label="mailbox folders"
|
||||
>
|
||||
<Drawer
|
||||
container={window.document.body}
|
||||
variant="temporary"
|
||||
open={menuOpen}
|
||||
onTransitionEnd={handleMenuTransitionEnd}
|
||||
onClose={handleMenuClose}
|
||||
sx={{
|
||||
display: 'block',
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box' },
|
||||
}}
|
||||
slotProps={{
|
||||
root: {
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Card className="MenuCard">
|
||||
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="scrollable"
|
||||
allowScrollButtonsMobile
|
||||
onChange={handleTabChange}
|
||||
aria-label="Backstory navigation">
|
||||
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Drawer>
|
||||
</Box>
|
||||
{
|
||||
tabs.map((tab: any, i: number) =>
|
||||
<BackstoryPage key={i} active={i === activeTab} path={tab.path}>{tab.children}</BackstoryPage>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Snack
|
||||
ref={snackRef}
|
||||
/>
|
||||
</Box >
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="*" element={<PathRouter setSnack={setSnack} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<Snack
|
||||
ref={snackRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -5,7 +5,7 @@ import { ChatSubmitQueryInterface } from './ChatQuery';
|
||||
import { SetSnackType } from './Snack';
|
||||
|
||||
interface BackstoryElementProps {
|
||||
sessionId: string | undefined,
|
||||
sessionId: string,
|
||||
setSnack: SetSnackType,
|
||||
submitQuery: ChatSubmitQueryInterface,
|
||||
sx?: SxProps<Theme>,
|
||||
|
@ -1,30 +1,32 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
type QueryOptions = {
|
||||
/* backstory/src/utils/message.py */
|
||||
type Tunables = {
|
||||
enable_rag?: boolean,
|
||||
enable_tools?: boolean,
|
||||
enable_context?: boolean,
|
||||
};
|
||||
|
||||
type ChatSubmitQueryInterface = (prompt: string, tunables?: QueryOptions) => void;
|
||||
/* backstory/src/server.py */
|
||||
type Query = {
|
||||
prompt: string,
|
||||
tunables?: Tunables,
|
||||
agent_options?: {},
|
||||
};
|
||||
|
||||
type ChatSubmitQueryInterface = (query: Query) => void;
|
||||
|
||||
interface ChatQueryInterface {
|
||||
prompt: string,
|
||||
tunables?: QueryOptions,
|
||||
query: Query,
|
||||
submitQuery?: ChatSubmitQueryInterface
|
||||
}
|
||||
|
||||
const ChatQuery = (props : ChatQueryInterface) => {
|
||||
const { prompt, submitQuery } = props;
|
||||
let tunables = props.tunables;
|
||||
|
||||
if (typeof (tunables) === "string") {
|
||||
tunables = JSON.parse(tunables);
|
||||
}
|
||||
const { query, submitQuery } = props;
|
||||
|
||||
if (submitQuery === undefined) {
|
||||
return (<Box>{prompt}</Box>);
|
||||
return (<Box>{query.prompt}</Box>);
|
||||
}
|
||||
return (
|
||||
<Button variant="outlined" sx={{
|
||||
@ -32,15 +34,15 @@ const ChatQuery = (props : ChatQueryInterface) => {
|
||||
borderColor: theme => theme.palette.custom.highlight,
|
||||
m: 1
|
||||
}}
|
||||
size="small" onClick={(e: any) => { submitQuery(prompt, tunables); }}>
|
||||
{prompt}
|
||||
size="small" onClick={(e: any) => { submitQuery(query); }}>
|
||||
{query.prompt}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export type {
|
||||
ChatQueryInterface,
|
||||
QueryOptions,
|
||||
Query,
|
||||
ChatSubmitQueryInterface,
|
||||
};
|
||||
|
||||
|
@ -86,7 +86,8 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
|
||||
return <div className="SystemInfo">{systemElements}</div>;
|
||||
};
|
||||
|
||||
const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
|
||||
const ControlsPage = (props: BackstoryPageProps) => {
|
||||
const { setSnack, sessionId } = props;
|
||||
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
@ -281,7 +282,7 @@ const ControlsPage = ({ sessionId, setSnack }: BackstoryPageProps) => {
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log("Server tunables: ", data);
|
||||
// console.log("Server tunables: ", data);
|
||||
setServerTunables(data);
|
||||
setSystemPrompt(data["system_prompt"]);
|
||||
setMessageHistoryLength(data["message_history_length"]);
|
||||
|
@ -13,7 +13,7 @@ import { Message, MessageList, BackstoryMessage } from './Message';
|
||||
import { ContextStatus } from './ContextStatus';
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { DeleteConfirmation } from './DeleteConfirmation';
|
||||
import { QueryOptions } from './ChatQuery';
|
||||
import { Query } from './ChatQuery';
|
||||
import './Conversation.css';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from './BackstoryTextField';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
@ -24,7 +24,7 @@ const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establi
|
||||
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
|
||||
|
||||
interface ConversationHandle {
|
||||
submitQuery: (prompt: string, options?: QueryOptions) => void;
|
||||
submitQuery: (query: Query) => void;
|
||||
fetchHistory: () => void;
|
||||
}
|
||||
|
||||
@ -46,26 +46,27 @@ interface ConversationProps extends BackstoryElementProps {
|
||||
onResponse?: ((message: BackstoryMessage) => void) | undefined, // Event called when a query completes (provides messages)
|
||||
};
|
||||
|
||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
actionLabel,
|
||||
className,
|
||||
defaultPrompts,
|
||||
defaultQuery,
|
||||
hideDefaultPrompts,
|
||||
hidePreamble,
|
||||
messageFilter,
|
||||
messages,
|
||||
onResponse,
|
||||
placeholder,
|
||||
preamble,
|
||||
resetAction,
|
||||
resetLabel,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
sx,
|
||||
type,
|
||||
}: ConversationProps, ref) => {
|
||||
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
|
||||
const {
|
||||
sessionId,
|
||||
actionLabel,
|
||||
className,
|
||||
defaultPrompts,
|
||||
defaultQuery,
|
||||
hideDefaultPrompts,
|
||||
hidePreamble,
|
||||
messageFilter,
|
||||
messages,
|
||||
onResponse,
|
||||
placeholder,
|
||||
preamble,
|
||||
resetAction,
|
||||
resetLabel,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
sx,
|
||||
type,
|
||||
} = props;
|
||||
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
@ -238,12 +239,15 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
};
|
||||
|
||||
const handleEnter = (value: string) => {
|
||||
sendQuery(value);
|
||||
const query: Query = {
|
||||
prompt: value
|
||||
}
|
||||
sendQuery(query);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submitQuery: (query: string, tunables?: QueryOptions) => {
|
||||
sendQuery(query, tunables);
|
||||
submitQuery: (query: Query) => {
|
||||
sendQuery(query);
|
||||
},
|
||||
fetchHistory: () => { return fetchHistory(); }
|
||||
}));
|
||||
@ -295,17 +299,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
stopRef.current = true;
|
||||
};
|
||||
|
||||
const sendQuery = async (request: string, options?: QueryOptions) => {
|
||||
request = request.trim();
|
||||
const sendQuery = async (query: Query) => {
|
||||
query.prompt = query.prompt.trim();
|
||||
|
||||
// If the request was empty, a default request was provided,
|
||||
// and there is no prompt for the user, send the default request.
|
||||
if (!request && defaultQuery && !prompt) {
|
||||
request = defaultQuery.trim();
|
||||
if (!query.prompt && defaultQuery && !prompt) {
|
||||
query.prompt = defaultQuery.trim();
|
||||
}
|
||||
|
||||
// Do not send an empty request.
|
||||
if (!request) {
|
||||
if (!query.prompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -318,7 +322,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
{
|
||||
role: 'user',
|
||||
origin: type,
|
||||
content: request,
|
||||
content: query.prompt,
|
||||
disableCopy: true
|
||||
}
|
||||
]);
|
||||
@ -337,20 +341,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
// Make the fetch request with proper headers
|
||||
let query;
|
||||
if (options) {
|
||||
query = {
|
||||
options: options,
|
||||
prompt: request.trim()
|
||||
}
|
||||
} else {
|
||||
query = {
|
||||
prompt: request.trim()
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(connectionBase + `/api/chat/${sessionId}/${type}`, {
|
||||
const response = await fetch(connectionBase + `/api/${type}/${sessionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -393,7 +384,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
prompt: ['done', 'partial'].includes(update.status) ? update.prompt : '',
|
||||
content: backstoryMessage.response || "",
|
||||
expanded: update.status === "done" ? true : false,
|
||||
expandable: true,
|
||||
expandable: update.status === "done" ? false : true,
|
||||
}] as MessageList);
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
@ -568,7 +559,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||
variant="contained"
|
||||
disabled={sessionId === undefined || processingMessage !== undefined}
|
||||
onClick={() => { sendQuery((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }}>
|
||||
onClick={() => { sendQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
|
||||
{actionLabel}<SendIcon />
|
||||
</Button>
|
||||
</span>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Message } from './Message';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { connectionBase } from './Global';
|
||||
|
||||
interface DocumentProps extends BackstoryElementProps {
|
||||
title: string;
|
||||
@ -13,7 +12,12 @@ interface DocumentProps extends BackstoryElementProps {
|
||||
}
|
||||
|
||||
const Document = (props: DocumentProps) => {
|
||||
const { setSnack, submitQuery, filepath, content, title, expanded, disableCopy, onExpand, sessionId } = props;
|
||||
const { sessionId, setSnack, submitQuery, filepath, content, title, expanded, disableCopy, onExpand } = props;
|
||||
const backstoryProps = {
|
||||
submitQuery,
|
||||
setSnack,
|
||||
sessionId
|
||||
}
|
||||
|
||||
const [document, setDocument] = useState<string>("");
|
||||
|
||||
@ -55,14 +59,11 @@ const Document = (props: DocumentProps) => {
|
||||
flexGrow: 0,
|
||||
},
|
||||
message: { role: 'content', title: title, content: document || content || "" },
|
||||
connectionBase,
|
||||
submitQuery,
|
||||
setSnack,
|
||||
expanded,
|
||||
disableCopy,
|
||||
onExpand,
|
||||
sessionId,
|
||||
}} />
|
||||
}}
|
||||
{...backstoryProps} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -32,10 +32,10 @@ What would you like to know about James?
|
||||
|
||||
const backstoryQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
<ChatQuery prompt="What is James Ketrenos' work history?" tunables={{ enable_tools: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery prompt="What programming languages has James used?" tunables={{ enable_tools: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery prompt="What are James' professional strengths?" tunables={{ enable_tools: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery prompt="What are today's headlines on CNBC.com?" tunables={{ enable_tools: true, enable_rag: false, enable_context: false }} submitQuery={submitQuery} />
|
||||
<ChatQuery query={{ prompt: "What is James Ketrenos' work history?", tunables: { enable_tools: false } }} submitQuery={submitQuery} />
|
||||
<ChatQuery query={{ prompt: "Provide an exhaustive list of programming languages James has used.", tunables: { enable_tools: false } }} submitQuery={submitQuery} />
|
||||
<ChatQuery query={{ prompt: "What are James' professional strengths?", tunables: { enable_tools: false } }} submitQuery={submitQuery} />
|
||||
<ChatQuery query={{ prompt: "What are today's headlines on CNBC.com?", tunables: { enable_tools: true, enable_rag: false, enable_context: false } }} submitQuery={submitQuery} />
|
||||
</Box>,
|
||||
<Box sx={{ p: 1 }}>
|
||||
<MuiMarkdown>
|
||||
|
301
frontend/src/Main.tsx
Normal file
301
frontend/src/Main.tsx
Normal file
@ -0,0 +1,301 @@
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Card from '@mui/material/Card';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Box from '@mui/material/Box';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
import { ConversationHandle } from './Conversation';
|
||||
import { Query } from './ChatQuery';
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { BackstoryPage, BackstoryTabProps } from './BackstoryTab';
|
||||
|
||||
import { HomePage } from './HomePage';
|
||||
import { ResumeBuilderPage } from './ResumeBuilderPage';
|
||||
import { VectorVisualizerPage } from './VectorVisualizer';
|
||||
import { AboutPage } from './AboutPage';
|
||||
import { ControlsPage } from './ControlsPage';
|
||||
import { SetSnackType } from './Snack';
|
||||
|
||||
import './Main.css';
|
||||
import './Conversation.css';
|
||||
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
interface MainProps {
|
||||
sessionId: string,
|
||||
setSnack: SetSnackType
|
||||
}
|
||||
const Main = (props: MainProps) => {
|
||||
const { sessionId } = props;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [isMenuClosing, setIsMenuClosing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const isDesktop = useMediaQuery('(min-width:650px)');
|
||||
const prevIsDesktopRef = useRef<boolean>(isDesktop);
|
||||
const chatRef = useRef<ConversationHandle>(null);
|
||||
const [subRoute, setSubRoute] = useState<string>("");
|
||||
const backstoryProps = useMemo(() => {
|
||||
const handleSubmitChatQuery = (query: Query) => {
|
||||
console.log(`handleSubmitChatQuery:`, query, chatRef.current ? ' sending' : 'no handler');
|
||||
chatRef.current?.submitQuery(query);
|
||||
setActiveTab(0);
|
||||
};
|
||||
return {...props, submitQuery: handleSubmitChatQuery};
|
||||
}, [props, setActiveTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevIsDesktopRef.current === isDesktop)
|
||||
return;
|
||||
|
||||
if (menuOpen) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
|
||||
prevIsDesktopRef.current = isDesktop;
|
||||
}, [isDesktop, setMenuOpen, menuOpen])
|
||||
|
||||
const tabs: BackstoryTabProps[] = useMemo(() => {
|
||||
const homeTab: BackstoryTabProps = {
|
||||
label: "",
|
||||
path: "",
|
||||
tabProps: {
|
||||
label: "Backstory",
|
||||
sx: { flexGrow: 1, fontSize: '1rem' },
|
||||
icon:
|
||||
<Avatar sx={{
|
||||
width: 24,
|
||||
height: 24
|
||||
}}
|
||||
variant="rounded"
|
||||
alt="Backstory logo"
|
||||
src="/logo192.png" />,
|
||||
iconPosition: "start"
|
||||
},
|
||||
children: <HomePage ref={chatRef} {...backstoryProps} />
|
||||
};
|
||||
|
||||
const resumeBuilderTab: BackstoryTabProps = {
|
||||
label: "Resume Builder",
|
||||
path: "resume-builder",
|
||||
children: <ResumeBuilderPage {...backstoryProps} />
|
||||
};
|
||||
|
||||
const contextVisualizerTab: BackstoryTabProps = {
|
||||
label: "Context Visualizer",
|
||||
path: "context-visualizer",
|
||||
children: <VectorVisualizerPage sx={{ p: 1 }} {...backstoryProps} />
|
||||
};
|
||||
|
||||
const aboutTab = {
|
||||
label: "About",
|
||||
path: "about",
|
||||
children: <AboutPage {...backstoryProps} />
|
||||
};
|
||||
|
||||
const controlsTab: BackstoryTabProps = {
|
||||
path: "controls",
|
||||
tabProps: {
|
||||
sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' },
|
||||
icon: <SettingsIcon />
|
||||
},
|
||||
children: (
|
||||
<Scrollable
|
||||
autoscroll={false}
|
||||
sx={{
|
||||
maxWidth: "1024px",
|
||||
height: "calc(100vh - 72px)",
|
||||
flexDirection: "column",
|
||||
margin: "0 auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<ControlsPage {...backstoryProps} />
|
||||
</Scrollable>
|
||||
)
|
||||
};
|
||||
|
||||
return [
|
||||
homeTab,
|
||||
resumeBuilderTab,
|
||||
contextVisualizerTab,
|
||||
aboutTab,
|
||||
controlsTab,
|
||||
];
|
||||
}, [backstoryProps]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setIsMenuClosing(true);
|
||||
setMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleMenuTransitionEnd = () => {
|
||||
setIsMenuClosing(false);
|
||||
};
|
||||
|
||||
const handleMenuToggle = () => {
|
||||
if (!isMenuClosing) {
|
||||
setMenuOpen(!menuOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
if (newValue > tabs.length) {
|
||||
console.log(`Invalid tab requested: ${newValue}`);
|
||||
return;
|
||||
}
|
||||
setActiveTab(newValue);
|
||||
|
||||
const tabPath = tabs[newValue].path;
|
||||
|
||||
// Preserve the subroute if one is set
|
||||
let path = tabPath ? `/${tabPath}` : '';
|
||||
if (subRoute) path += `/${subRoute}`;
|
||||
path += `/${sessionId}`;
|
||||
|
||||
console.log(`Pusing state ${path}`)
|
||||
navigate(path);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const pathParts = location.pathname.split('/').filter(Boolean);
|
||||
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
|
||||
const currentSubRoute = pathParts.length > 2 ? pathParts.slice(1, -1).join('/') : '';
|
||||
|
||||
let tabIndex = tabs.findIndex((tab) => tab.path === currentPath);
|
||||
if (tabIndex === -1) {
|
||||
console.log(`Invalid path "${currentPath}" -- redirecting to default`);
|
||||
tabIndex = 0;
|
||||
}
|
||||
setActiveTab(tabIndex);
|
||||
setSubRoute(currentSubRoute);
|
||||
}, [location, tabs]);
|
||||
|
||||
/* toolbar height is 64px + 8px margin-top */
|
||||
const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' }));
|
||||
|
||||
return (
|
||||
<Box className="App"
|
||||
sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
maxWidth: "100vw"
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
|
||||
{!isDesktop &&
|
||||
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "row" }}>
|
||||
<IconButton
|
||||
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={handleMenuToggle}
|
||||
>
|
||||
<Tooltip title="Navigation">
|
||||
<MenuIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Tooltip title="Backstory">
|
||||
<Box
|
||||
sx={{ m: 1, gap: 1, display: "flex", flexDirection: "row", alignItems: "center", fontWeight: "bold", fontSize: "1.0rem", cursor: "pointer" }}
|
||||
onClick={() => { setActiveTab(0); setMenuOpen(false); }}
|
||||
>
|
||||
<Avatar sx={{
|
||||
width: 24,
|
||||
height: 24
|
||||
}}
|
||||
variant="rounded"
|
||||
alt="Backstory logo"
|
||||
src="/logo192.png" />
|
||||
BACKSTORY
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
|
||||
{menuOpen === false && isDesktop &&
|
||||
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
||||
value={activeTab}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="fullWidth"
|
||||
allowScrollButtonsMobile
|
||||
onChange={handleTabChange}
|
||||
aria-label="Backstory navigation">
|
||||
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
|
||||
</Tabs>
|
||||
}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
</AppBar>
|
||||
|
||||
<Offset />
|
||||
|
||||
<Box sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }} >
|
||||
<Drawer
|
||||
container={window.document.body}
|
||||
variant="temporary"
|
||||
open={menuOpen}
|
||||
onTransitionEnd={handleMenuTransitionEnd}
|
||||
onClose={handleMenuClose}
|
||||
sx={{
|
||||
display: 'block',
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box' },
|
||||
}}
|
||||
slotProps={{
|
||||
root: {
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Card className="MenuCard">
|
||||
<Tabs sx={{ display: "flex", flexGrow: 1 }}
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="scrollable"
|
||||
allowScrollButtonsMobile
|
||||
onChange={handleTabChange}
|
||||
aria-label="Backstory navigation">
|
||||
{tabs.map((tab, index) => <Tab key={index} value={index} label={tab.label} {...tab.tabProps} />)}
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Drawer>
|
||||
{
|
||||
tabs.map((tab: any, i: number) =>
|
||||
<BackstoryPage key={i} active={i === activeTab} path={tab.path}>{tab.children}</BackstoryPage>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
</Box >
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
Main
|
||||
}
|
@ -15,7 +15,6 @@ import Button from '@mui/material/Button';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { ExpandMore } from './ExpandMore';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
@ -86,7 +85,6 @@ interface MessageMetaData {
|
||||
eval_duration: number,
|
||||
prompt_eval_count: number,
|
||||
prompt_eval_duration: number,
|
||||
sessionId?: string,
|
||||
connectionBase: string,
|
||||
setSnack: SetSnackType,
|
||||
}
|
||||
@ -244,9 +242,14 @@ const MessageMeta = (props: MessageMetaProps) => {
|
||||
};
|
||||
|
||||
const Message = (props: MessageProps) => {
|
||||
const { message, submitQuery, sx, className, onExpand, sessionId, setSnack } = props;
|
||||
const { message, submitQuery, sx, className, onExpand, setSnack, sessionId } = props;
|
||||
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
||||
const textFieldRef = useRef(null);
|
||||
const backstoryProps = {
|
||||
submitQuery,
|
||||
sessionId,
|
||||
setSnack
|
||||
};
|
||||
|
||||
const handleMetaExpandClick = () => {
|
||||
setMetaExpanded(!metaExpanded);
|
||||
@ -295,7 +298,7 @@ const Message = (props: MessageProps) => {
|
||||
overflow: "auto", /* Handles scrolling for the div */
|
||||
}}
|
||||
>
|
||||
<StyledMarkdown streaming={message.role === "streaming"} {...{ content: formattedContent, submitQuery, sessionId, setSnack }} />
|
||||
<StyledMarkdown streaming={message.role === "streaming"} content={formattedContent} {...backstoryProps} />
|
||||
</Scrollable>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", p: 0, m: 0 }}>
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { SxProps } from '@mui/material';
|
||||
|
||||
import { ChatQuery } from './ChatQuery';
|
||||
import { ChatQuery, Query } from './ChatQuery';
|
||||
import { MessageList, BackstoryMessage } from './Message';
|
||||
import { Conversation } from './Conversation';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
@ -19,12 +19,13 @@ import './ResumeBuilderPage.css';
|
||||
* A responsive component that displays job descriptions, generated resumes and fact checks
|
||||
* with different layouts for mobile and desktop views.
|
||||
*/
|
||||
const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
|
||||
sx,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
}) => {
|
||||
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const {
|
||||
sx,
|
||||
sessionId,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
} = props
|
||||
// State for editing job description
|
||||
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
||||
const [hasResume, setHasResume] = useState<boolean>(false);
|
||||
@ -42,18 +43,18 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
const handleJobQuery = (query: string) => {
|
||||
console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler');
|
||||
const handleJobQuery = (query: Query) => {
|
||||
console.log(`handleJobQuery: ${query.prompt} -- `, jobConversationRef.current ? ' sending' : 'no handler');
|
||||
jobConversationRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
const handleResumeQuery = (query: string) => {
|
||||
console.log(`handleResumeQuery: ${query} -- `, resumeConversationRef.current ? ' sending' : 'no handler');
|
||||
const handleResumeQuery = (query: Query) => {
|
||||
console.log(`handleResumeQuery: ${query.prompt} -- `, resumeConversationRef.current ? ' sending' : 'no handler');
|
||||
resumeConversationRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
const handleFactsQuery = (query: string) => {
|
||||
console.log(`handleFactsQuery: ${query} -- `, factsConversationRef.current ? ' sending' : 'no handler');
|
||||
const handleFactsQuery = (query: Query) => {
|
||||
console.log(`handleFactsQuery: ${query.prompt} -- `, factsConversationRef.current ? ' sending' : 'no handler');
|
||||
factsConversationRef.current?.submitQuery(query);
|
||||
};
|
||||
|
||||
@ -193,23 +194,26 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
|
||||
console.log('renderJobDescriptionView');
|
||||
const jobDescriptionQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="What are the key skills necessary for this position?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery query={{ prompt: "What are the key skills necessary for this position?", tunables: { enable_tools: false } }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery query={{ prompt: "How much should this position pay (accounting for inflation)?", tunables: { enable_tools: false } }} submitQuery={handleJobQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
const jobDescriptionPreamble: MessageList = [{
|
||||
role: 'info',
|
||||
content: `Once you paste a job description and press **Generate Resume**, the system will perform the following actions:
|
||||
content: `Once you paste a job description and press **Generate Resume**, Backstory will perform the following actions:
|
||||
|
||||
1. **RAG**: Collects information from the RAG database relavent to the job description
|
||||
2. **Isolated Analysis**: Three sub-stages
|
||||
1. **Job Analysis**: Extracts requirements from job description only
|
||||
2. **Candidate Analysis**: Catalogs qualifications from resume/context only
|
||||
3. **Mapping Analysis**: Identifies legitimate matches between requirements and qualifications
|
||||
3. **Resume Generation**: Uses mapping output to create a tailored resume with evidence-based content
|
||||
4. **Verification**: Performs fact-checking to catch any remaining fabrications
|
||||
1. **Re-generation**: If verification does not pass, a second attempt is made to correct any issues`,
|
||||
1. **Job Analysis**: LLM extracts requirements from '\`Job Description\`' to generate a list of desired '\`Skills\`'.
|
||||
2. **Candidate Analysis**: LLM determines candidate qualifications by performing skill assessments.
|
||||
|
||||
For each '\`Skill\`' from **Job Analysis** phase:
|
||||
|
||||
1. **RAG**: Retrieval Augmented Generation collection is queried for context related content for each '\`Skill\`'.
|
||||
2. **Evidence Creation**: LLM is queried to generate supporting evidence of '\`Skill\`' from the '\`RAG\`' and '\`Candidate Resume\`'.
|
||||
3. **Resume Generation**: LLM is provided the output from the **Candidate Analysis:Evidence Creation** phase and asked to generate a professional resume.
|
||||
|
||||
See [About > Resume Generation Architecture](/about/resume-generation) for more details.
|
||||
`,
|
||||
disableCopy: true
|
||||
}];
|
||||
|
||||
@ -262,8 +266,8 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
|
||||
const renderResumeView = useCallback((sx: SxProps) => {
|
||||
const resumeQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="Is this resume a good fit for the provided job description?" tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery query={{ prompt: "Is this resume a good fit for the provided job description?", tunables: { enable_tools: false } }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery query={{ prompt: "Provide a more concise resume.", tunables: { enable_tools: false } }} submitQuery={handleResumeQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
@ -311,7 +315,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = ({
|
||||
const renderFactCheckView = useCallback((sx: SxProps) => {
|
||||
const factsQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
|
||||
<ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enable_tools: false } }} submitQuery={handleFactsQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
|
65
frontend/src/SessionWrapper.tsx
Normal file
65
frontend/src/SessionWrapper.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { connectionBase } from './Global';
|
||||
import { SetSnackType } from './Snack';
|
||||
|
||||
const getSessionId = async () => {
|
||||
const response = await fetch(connectionBase + `/api/context`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw Error("Server is temporarily down.");
|
||||
}
|
||||
|
||||
const newSession = (await response.json()).id;
|
||||
console.log(`Session created: ${newSession}`);
|
||||
|
||||
return newSession;
|
||||
};
|
||||
|
||||
interface SessionWrapperProps {
|
||||
setSnack: SetSnackType;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SessionWrapper = ({ setSnack, children }: SessionWrapperProps) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||
const fetchingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const ensureSessionId = async () => {
|
||||
const parts = location.pathname.split("/").filter(Boolean);
|
||||
const pattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89ab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/i;
|
||||
const hasSession = parts.length !== 0 && pattern.test(parts[parts.length - 1]);
|
||||
|
||||
if (!hasSession) {
|
||||
let activeSession = sessionId;
|
||||
if (!activeSession) {
|
||||
activeSession = await getSessionId();
|
||||
setSessionId(activeSession);
|
||||
}
|
||||
|
||||
const newPath = [...parts, activeSession].join("/");
|
||||
navigate(`/${newPath}`, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (!fetchingRef.current) {
|
||||
fetchingRef.current = true;
|
||||
ensureSessionId().catch((e) => {
|
||||
console.error(e);
|
||||
setSnack("Backstory is temporarily unavailable.", "error");
|
||||
});
|
||||
}
|
||||
}, [location.pathname, navigate, setSnack, sessionId]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { SessionWrapper };
|
@ -21,7 +21,7 @@ interface StyledMarkdownProps extends BackstoryElementProps {
|
||||
};
|
||||
|
||||
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
|
||||
const { className, content, submitQuery, sx, streaming } = props;
|
||||
const { className, sessionId, content, submitQuery, sx, streaming } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const overrides: any = {
|
||||
@ -75,6 +75,16 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
||||
a: {
|
||||
component: Link,
|
||||
props: {
|
||||
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const href = event.currentTarget.getAttribute('href');
|
||||
console.log("StyledMarkdown onClick:", href, sessionId);
|
||||
if (href) {
|
||||
if (href.match(/^\//)) {
|
||||
event.preventDefault();
|
||||
window.history.replaceState({}, '', `${href}/${sessionId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
sx: {
|
||||
wordBreak: "break-all",
|
||||
color: theme.palette.secondary.main,
|
||||
@ -87,9 +97,16 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
||||
}
|
||||
},
|
||||
ChatQuery: {
|
||||
component: ChatQuery,
|
||||
props: {
|
||||
submitQuery,
|
||||
component: (props: { query: string }) => {
|
||||
const queryString = props.query.replace(/(\w+):/g, '"$1":');
|
||||
try {
|
||||
const query = JSON.parse(queryString);
|
||||
|
||||
return <ChatQuery submitQuery={submitQuery} query={query} />
|
||||
} catch (e) {
|
||||
console.log("StyledMarkdown error:", queryString, e);
|
||||
return props.query;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -108,7 +108,7 @@ const symbolMap: Record<string, string> = {
|
||||
};
|
||||
|
||||
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
|
||||
const { setSnack, rag, inline, sessionId, sx, submitQuery } = props;
|
||||
const { sessionId, setSnack, rag, inline, sx, submitQuery } = props;
|
||||
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
||||
const [newQuery, setNewQuery] = useState<string>('');
|
||||
const [newQueryEmbedding, setNewQueryEmbedding] = useState<ChromaResult | undefined>(undefined);
|
||||
|
22
src/pyproject.toml
Normal file
22
src/pyproject.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py312']
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.venv
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.vulture]
|
||||
min_confidence = 90
|
||||
ignore_decorators = [
|
||||
"@app.put", "@app.get", "@app.post", "@app.delete", "@app.patch",
|
||||
"@self.app.put", "@self.app.get", "@self.app.post", "@self.app.delete", "@self.app.patch",
|
||||
"@model_validator",
|
||||
"@override", "@classmethod"
|
||||
]
|
||||
exclude = ["tests/", "__pycache__/"]
|
@ -1,173 +1,110 @@
|
||||
accelerate==1.6.0
|
||||
aiofiles==24.1.0
|
||||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.11.16
|
||||
aiohttp==3.11.18
|
||||
aiosignal==1.3.2
|
||||
annotated-types==0.7.0
|
||||
ansi2html==1.9.2
|
||||
anthropic==0.49.0
|
||||
anyio==4.9.0
|
||||
appdirs==1.4.4
|
||||
argon2-cffi==23.1.0
|
||||
argon2-cffi-bindings==21.2.0
|
||||
arrow==1.3.0
|
||||
asgiref==3.8.1
|
||||
asttokens==3.0.0
|
||||
async-lru==2.0.5
|
||||
attrs==25.3.0
|
||||
babel==2.17.0
|
||||
azure-ai-documentintelligence==1.0.2
|
||||
azure-core==1.34.0
|
||||
azure-identity==1.23.0
|
||||
backoff==2.2.1
|
||||
bcrypt==4.3.0
|
||||
beautifulsoup4==4.13.4
|
||||
bigdl-core-xe-all==2.7.0b20250416
|
||||
bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.44.1.dev0-py3-none-manylinux_2_24_x86_64.whl#sha256=66deda2b99cee0d4e52a183d9bac5c8e8618cd9b4d4933ccf23b908622d6b879
|
||||
bleach==6.2.0
|
||||
bigdl-core-xe-all==2.7.0b20250514
|
||||
bitsandbytes @ https://github.com/bitsandbytes-foundation/bitsandbytes/releases/download/continuous-release_multi-backend-refactor/bitsandbytes-0.45.3.dev272-py3-none-manylinux_2_24_x86_64.whl#sha256=7000dfb05889b2304ffdf98b54ebb6283ec4ff4020a9acbd5c357ae35345879c
|
||||
blinker==1.9.0
|
||||
bs4==0.0.2
|
||||
build==1.2.2.post1
|
||||
cachetools==5.5.2
|
||||
certifi==2025.1.31
|
||||
certifi==2025.4.26
|
||||
cffi==1.17.1
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.1
|
||||
chroma-hnswlib==0.7.6
|
||||
chromadb==0.6.3
|
||||
charset-normalizer==3.4.2
|
||||
chromadb==1.0.9
|
||||
click==8.1.8
|
||||
cobble==0.1.4
|
||||
coloredlogs==15.0.1
|
||||
comm==0.2.2
|
||||
contourpy==1.3.2
|
||||
cryptography==44.0.2
|
||||
cycler==0.12.1
|
||||
dash==3.0.3
|
||||
dataclasses-json==0.6.7
|
||||
datasets==3.5.0
|
||||
debugpy==1.8.14
|
||||
decorator==5.2.1
|
||||
cryptography==44.0.3
|
||||
curl_cffi==0.11.1
|
||||
datasets==3.6.0
|
||||
defusedxml==0.7.1
|
||||
Deprecated==1.2.18
|
||||
diffusers==0.33.1
|
||||
dill==0.3.8
|
||||
distro==1.9.0
|
||||
dpcpp-cpp-rt==2025.0.4
|
||||
durationpy==0.9
|
||||
einops==0.8.1
|
||||
emoji==2.14.1
|
||||
eval_type_backport==0.2.2
|
||||
executing==2.2.0
|
||||
faiss-cpu==1.10.0
|
||||
et_xmlfile==2.0.0
|
||||
fastapi==0.115.9
|
||||
fastjsonschema==2.21.1
|
||||
feedparser==6.0.11
|
||||
ffmpy==0.5.0
|
||||
filelock==3.13.1
|
||||
filetype==1.2.0
|
||||
Flask==3.0.3
|
||||
Flask==3.1.1
|
||||
flask-cors==5.0.1
|
||||
flask-sock==0.7.0
|
||||
flatbuffers==25.2.10
|
||||
fonttools==4.57.0
|
||||
fqdn==1.5.1
|
||||
frozendict==2.4.6
|
||||
frozenlist==1.5.0
|
||||
frozenlist==1.6.0
|
||||
fsspec==2024.6.1
|
||||
gensim==4.3.3
|
||||
geographiclib==2.0
|
||||
geopy==2.4.1
|
||||
google-ai-generativelanguage==0.6.15
|
||||
google-api-core==2.24.2
|
||||
google-api-python-client==2.167.0
|
||||
google-auth==2.39.0
|
||||
google-auth-httplib2==0.2.0
|
||||
google-generativeai==0.8.4
|
||||
google-auth==2.40.1
|
||||
googleapis-common-protos==1.70.0
|
||||
gradio==5.25.2
|
||||
gradio_client==1.8.0
|
||||
greenlet==3.2.0
|
||||
groovy==0.1.2
|
||||
grpcio==1.71.0
|
||||
grpcio-status==1.71.0
|
||||
grpclib==0.4.7
|
||||
h11==0.14.0
|
||||
h2==4.2.0
|
||||
hpack==4.1.0
|
||||
html5lib==1.1
|
||||
httpcore==1.0.8
|
||||
httplib2==0.22.0
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httptools==0.6.4
|
||||
httpx==0.28.1
|
||||
httpx-sse==0.4.0
|
||||
huggingface-hub==0.30.2
|
||||
huggingface-hub==0.31.2
|
||||
humanfriendly==10.0
|
||||
hyperframe==6.1.0
|
||||
idna==3.10
|
||||
impi-devel==2021.14.1
|
||||
impi-rt==2021.14.1
|
||||
importlib_metadata==8.6.1
|
||||
importlib_resources==6.5.2
|
||||
ipykernel==6.29.5
|
||||
ipython==9.1.0
|
||||
ipython_pygments_lexers==1.1.1
|
||||
ipywidgets==8.1.6
|
||||
isoduration==20.11.0
|
||||
intel-cmplr-lib-rt==2025.0.4
|
||||
intel-cmplr-lib-ur==2025.0.4
|
||||
intel-cmplr-lic-rt==2025.0.4
|
||||
intel-opencl-rt==2025.0.4
|
||||
intel-openmp==2025.0.4
|
||||
intel-pti==0.10.1
|
||||
intel-sycl-rt==2025.0.4
|
||||
intel_extension_for_pytorch==2.6.10+xpu
|
||||
ipex-llm @ file:///opt/wheels/ipex_llm-2.2.0.dev0-py3-none-any.whl#sha256=a209a95606d6b4c6fcb57f96e519537742be54fa6728b603986776e3915991f5
|
||||
isodate==0.7.2
|
||||
itsdangerous==2.2.0
|
||||
jedi==0.19.2
|
||||
Jinja2==3.1.4
|
||||
jiter==0.9.0
|
||||
joblib==1.4.2
|
||||
json5==0.12.0
|
||||
joblib==1.5.0
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
jsonschema==4.23.0
|
||||
jsonschema-specifications==2024.10.1
|
||||
jupyter-dash==0.4.2
|
||||
jupyter-events==0.12.0
|
||||
jupyter-lsp==2.2.5
|
||||
jupyter_client==8.6.3
|
||||
jupyter_core==5.7.2
|
||||
jupyter_server==2.15.0
|
||||
jupyter_server_terminals==0.5.3
|
||||
jupyterlab==4.4.0
|
||||
jupyterlab_pygments==0.3.0
|
||||
jupyterlab_server==2.27.3
|
||||
jupyterlab_widgets==3.0.14
|
||||
kiwisolver==1.4.8
|
||||
jsonschema-specifications==2025.4.1
|
||||
kubernetes==32.0.1
|
||||
langchain==0.3.23
|
||||
langchain-chroma==0.2.3
|
||||
langchain-community==0.3.21
|
||||
langchain-core==0.3.52
|
||||
langchain-experimental==0.3.4
|
||||
langchain-core==0.3.59
|
||||
langchain-ollama==0.3.2
|
||||
langchain-openai==0.3.13
|
||||
langchain-text-splitters==0.3.8
|
||||
langdetect==1.0.9
|
||||
langsmith==0.3.31
|
||||
langsmith==0.3.42
|
||||
llvmlite==0.44.0
|
||||
lxml==5.3.2
|
||||
lxml==5.4.0
|
||||
magika==0.6.2
|
||||
mammoth==1.9.0
|
||||
markdown-it-py==3.0.0
|
||||
markdownify==1.1.0
|
||||
markitdown==0.1.1
|
||||
MarkupSafe==2.1.5
|
||||
marshmallow==3.26.1
|
||||
matplotlib==3.10.1
|
||||
matplotlib-inline==0.1.7
|
||||
mdurl==0.1.2
|
||||
mistune==3.1.3
|
||||
mkl==2025.0.1
|
||||
mkl-dpcpp==2025.0.1
|
||||
mmh3==5.1.0
|
||||
modal==0.74.4
|
||||
monotonic==1.6
|
||||
mpmath==1.3.0
|
||||
msal==1.32.3
|
||||
msal-extensions==1.3.1
|
||||
multidict==6.4.3
|
||||
multiprocess==0.70.16
|
||||
multitasking==0.0.11
|
||||
mypy-extensions==1.0.0
|
||||
narwhals==1.35.0
|
||||
nbclient==0.10.2
|
||||
nbconvert==7.16.6
|
||||
nbformat==5.10.4
|
||||
nest-asyncio==1.6.0
|
||||
networkx==3.3
|
||||
nltk==3.9.1
|
||||
notebook_shim==0.2.4
|
||||
numba==0.61.2
|
||||
numpy==1.26.4
|
||||
numpy==2.1.2
|
||||
oauthlib==3.2.2
|
||||
olefile==0.47
|
||||
ollama==0.4.8
|
||||
@ -182,150 +119,107 @@ onemkl-sycl-rng==2025.0.1
|
||||
onemkl-sycl-sparse==2025.0.1
|
||||
onemkl-sycl-stats==2025.0.1
|
||||
onemkl-sycl-vm==2025.0.1
|
||||
onnxruntime==1.21.0
|
||||
openai==1.75.0
|
||||
opentelemetry-api==1.32.1
|
||||
opentelemetry-exporter-otlp-proto-common==1.32.1
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.32.1
|
||||
opentelemetry-instrumentation==0.53b1
|
||||
opentelemetry-instrumentation-asgi==0.53b1
|
||||
opentelemetry-instrumentation-fastapi==0.53b1
|
||||
opentelemetry-proto==1.32.1
|
||||
opentelemetry-sdk==1.32.1
|
||||
opentelemetry-semantic-conventions==0.53b1
|
||||
opentelemetry-util-http==0.53b1
|
||||
orjson==3.10.16
|
||||
onnxruntime==1.22.0
|
||||
openpyxl==3.1.5
|
||||
opentelemetry-api==1.33.0
|
||||
opentelemetry-exporter-otlp-proto-common==1.33.0
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.33.0
|
||||
opentelemetry-instrumentation==0.54b0
|
||||
opentelemetry-instrumentation-asgi==0.54b0
|
||||
opentelemetry-instrumentation-fastapi==0.54b0
|
||||
opentelemetry-proto==1.33.0
|
||||
opentelemetry-sdk==1.33.0
|
||||
opentelemetry-semantic-conventions==0.54b0
|
||||
opentelemetry-util-http==0.54b0
|
||||
orjson==3.10.18
|
||||
overrides==7.7.0
|
||||
packaging==24.1
|
||||
packaging==24.2
|
||||
pandas==2.2.3
|
||||
pandocfilters==1.5.1
|
||||
parso==0.8.4
|
||||
peewee==3.17.9
|
||||
pdfminer.six==20250506
|
||||
peewee==3.18.1
|
||||
peft==0.15.2
|
||||
pexpect==4.9.0
|
||||
pillow==11.0.0
|
||||
platformdirs==4.3.7
|
||||
plotly==6.0.1
|
||||
posthog==3.25.0
|
||||
platformdirs==4.3.8
|
||||
posthog==4.0.1
|
||||
prometheus-fastapi-instrumentator==7.1.0
|
||||
prometheus_client==0.21.1
|
||||
prompt_toolkit==3.0.51
|
||||
propcache==0.3.1
|
||||
proto-plus==1.26.1
|
||||
protobuf==5.29.4
|
||||
psutil==7.0.0
|
||||
ptyprocess==0.7.0
|
||||
pure_eval==0.2.3
|
||||
pyarrow==19.0.1
|
||||
pyarrow==20.0.0
|
||||
pyasn1==0.6.1
|
||||
pyasn1_modules==0.4.2
|
||||
pycparser==2.22
|
||||
pydantic==2.11.3
|
||||
pydantic-settings==2.8.1
|
||||
pydantic_core==2.33.1
|
||||
pydantic==2.11.4
|
||||
pydantic_core==2.33.2
|
||||
pydub==0.25.1
|
||||
Pygments==2.19.1
|
||||
PyHyphen==4.0.4
|
||||
PyJWT==2.10.1
|
||||
pynndescent==0.5.13
|
||||
pyparsing==3.2.3
|
||||
pypdf==5.4.0
|
||||
PyPika==0.48.9
|
||||
pyproject_hooks==1.2.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.1.0
|
||||
python-iso639==2025.2.18
|
||||
python-json-logger==3.3.0
|
||||
python-magic==0.4.27
|
||||
python-multipart==0.0.20
|
||||
python-oxmsg==0.0.2
|
||||
pytorch-triton-xpu==3.2.0
|
||||
python-pptx==1.0.2
|
||||
pytorch-triton-xpu==3.3.0
|
||||
pytz==2025.2
|
||||
PyYAML==6.0.2
|
||||
pyzmq==26.4.0
|
||||
pyzt==0.0.2
|
||||
RapidFuzz==3.13.0
|
||||
referencing==0.36.2
|
||||
regex==2024.11.6
|
||||
requests==2.32.3
|
||||
requests-oauthlib==2.0.0
|
||||
requests-toolbelt==1.0.0
|
||||
retrying==1.3.4
|
||||
rfc3339-validator==0.1.4
|
||||
rfc3986-validator==0.1.1
|
||||
rich==14.0.0
|
||||
rpds-py==0.24.0
|
||||
rsa==4.9.1
|
||||
ruamel.yaml==0.18.10
|
||||
ruamel.yaml.clib==0.2.12
|
||||
ruff==0.11.5
|
||||
safehttpx==0.1.6
|
||||
safetensors==0.5.3
|
||||
scikit-learn==1.6.1
|
||||
scipy==1.13.1
|
||||
semantic-version==2.10.0
|
||||
Send2Trash==1.8.3
|
||||
scipy==1.15.3
|
||||
sentence-transformers==3.4.0
|
||||
sentencepiece==0.2.0
|
||||
sgmllib3k==1.0.0
|
||||
shellingham==1.5.4
|
||||
sigtools==4.0.1
|
||||
simple-websocket==1.1.0
|
||||
six==1.17.0
|
||||
smart-open==7.1.0
|
||||
sniffio==1.3.1
|
||||
soupsieve==2.6
|
||||
speedtest-cli==2.1.3
|
||||
SQLAlchemy==2.0.40
|
||||
stack-data==0.6.3
|
||||
soupsieve==2.7
|
||||
SpeechRecognition==3.14.3
|
||||
starlette==0.45.3
|
||||
sympy==1.13.1
|
||||
synchronicity==0.9.11
|
||||
sympy==1.13.3
|
||||
tbb==2022.1.0
|
||||
tcmlib==1.2.0
|
||||
tenacity==9.1.2
|
||||
terminado==0.18.1
|
||||
threadpoolctl==3.6.0
|
||||
tiktoken==0.9.0
|
||||
tinycss2==1.4.0
|
||||
tokenizers==0.21.1
|
||||
toml==0.10.2
|
||||
tomlkit==0.13.2
|
||||
torch==2.6.0+xpu
|
||||
torchaudio==2.6.0+xpu
|
||||
torchvision==0.21.0+xpu
|
||||
tornado==6.4.2
|
||||
torch==2.7.0+xpu
|
||||
torchaudio==2.7.0+xpu
|
||||
torchvision==0.22.0+xpu
|
||||
tqdm==4.67.1
|
||||
traitlets==5.14.3
|
||||
transformers==4.51.3
|
||||
typer==0.15.2
|
||||
types-certifi==2021.10.8.3
|
||||
types-python-dateutil==2.9.0.20241206
|
||||
types-toml==0.10.8.20240310
|
||||
typing-inspect==0.9.0
|
||||
typer==0.15.4
|
||||
typing-inspection==0.4.0
|
||||
typing_extensions==4.12.2
|
||||
tzdata==2025.2
|
||||
umap-learn==0.5.7
|
||||
umf==0.9.1
|
||||
unstructured==0.17.2
|
||||
unstructured-client==0.32.3
|
||||
uri-template==1.3.0
|
||||
uritemplate==4.1.1
|
||||
urllib3==2.4.0
|
||||
uvicorn==0.34.1
|
||||
uvicorn==0.34.2
|
||||
uvloop==0.21.0
|
||||
watchdog==6.0.0
|
||||
watchfiles==1.0.5
|
||||
wcwidth==0.2.13
|
||||
webcolors==24.11.1
|
||||
webencodings==0.5.1
|
||||
websocket-client==1.8.0
|
||||
websockets==15.0.1
|
||||
Werkzeug==3.0.6
|
||||
widgetsnbextension==4.0.14
|
||||
Werkzeug==3.1.3
|
||||
wrapt==1.17.2
|
||||
wsproto==1.2.0
|
||||
xlrd==2.0.1
|
||||
XlsxWriter==3.2.3
|
||||
xxhash==3.5.0
|
||||
yarl==1.19.0
|
||||
yfinance==0.2.55
|
||||
yarl==1.20.0
|
||||
yfinance==0.2.61
|
||||
youtube-transcript-api==1.0.3
|
||||
zipp==3.21.0
|
||||
zstandard==0.23.0
|
||||
|
@ -20,7 +20,6 @@ import re
|
||||
import math
|
||||
import warnings
|
||||
from typing import Any
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
from uuid import uuid4
|
||||
@ -48,15 +47,12 @@ try_import("prometheus_client")
|
||||
try_import("prometheus_fastapi_instrumentator")
|
||||
|
||||
import ollama
|
||||
import requests
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, BackgroundTasks # type: ignore
|
||||
from fastapi import FastAPI, Request # type: ignore
|
||||
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, RedirectResponse # type: ignore
|
||||
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
||||
import uvicorn # type: ignore
|
||||
import numpy as np # type: ignore
|
||||
import umap # type: ignore
|
||||
from sklearn.preprocessing import MinMaxScaler # type: ignore
|
||||
|
||||
# Prometheus
|
||||
from prometheus_client import Summary # type: ignore
|
||||
@ -76,8 +72,6 @@ from utils import (
|
||||
logger,
|
||||
)
|
||||
|
||||
CONTEXT_VERSION = 2
|
||||
|
||||
rags = [
|
||||
{
|
||||
"name": "JPK",
|
||||
@ -88,7 +82,7 @@ rags = [
|
||||
]
|
||||
|
||||
|
||||
class QueryOptions(BaseModel):
|
||||
class Query(BaseModel):
|
||||
prompt: str
|
||||
tunables: Tunables = Field(default_factory=Tunables)
|
||||
agent_options: Dict[str, Any] = Field(default={})
|
||||
@ -96,33 +90,6 @@ class QueryOptions(BaseModel):
|
||||
|
||||
REQUEST_TIME = Summary("request_processing_seconds", "Time spent processing request")
|
||||
|
||||
system_message_old = f"""
|
||||
Launched on {datetime.now().isoformat()}.
|
||||
|
||||
When answering queries, follow these steps:
|
||||
|
||||
1. First analyze the query to determine if real-time information might be helpful
|
||||
2. Even when <|context|> is provided, consider whether the tools would provide more current or comprehensive information
|
||||
3. Use the provided tools whenever they would enhance your response, regardless of whether context is also available
|
||||
4. When presenting weather forecasts, include relevant emojis immediately before the corresponding text. For example, for a sunny day, say \"☀️ Sunny\" or if the forecast says there will be \"rain showers, say \"🌧️ Rain showers\". Use this mapping for weather emojis: Sunny: ☀️, Cloudy: ☁️, Rainy: 🌧️, Snowy: ❄️
|
||||
4. When both <|context|> and tool outputs are relevant, synthesize information from both sources to provide the most complete answer
|
||||
5. Always prioritize the most up-to-date and relevant information, whether it comes from <|context|> or tools
|
||||
6. If <|context|> and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
|
||||
|
||||
Always use tools and <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so.
|
||||
""".strip()
|
||||
|
||||
|
||||
system_fact_check_QA = f"""
|
||||
Launched on {datetime.now().isoformat()}.
|
||||
|
||||
You are a professional resume fact checker.
|
||||
|
||||
You are provided with a <|resume|> which was generated by you, the <|context|> you used to generate that <|resume|>, and a <|fact_check|> generated by you when you analyzed <|context|> against the <|resume|> to identify dicrepancies.
|
||||
|
||||
Your task is to answer questions about the <|fact_check|> you generated based on the <|resume|> and <|context>.
|
||||
"""
|
||||
|
||||
|
||||
def get_installed_ram():
|
||||
try:
|
||||
@ -191,7 +158,6 @@ def get_cpu_info():
|
||||
except Exception as e:
|
||||
return f"Error retrieving CPU info: {e}"
|
||||
|
||||
|
||||
def system_info(model):
|
||||
return {
|
||||
"System RAM": get_installed_ram(),
|
||||
@ -202,13 +168,11 @@ def system_info(model):
|
||||
"Context length": defines.max_context,
|
||||
}
|
||||
|
||||
|
||||
# %%
|
||||
# Defaults
|
||||
OLLAMA_API_URL = defines.ollama_api_url
|
||||
MODEL_NAME = defines.model
|
||||
LOG_LEVEL = "info"
|
||||
USE_TLS = False
|
||||
WEB_HOST = "0.0.0.0"
|
||||
WEB_PORT = 8911
|
||||
DEFAULT_HISTORY_LENGTH = 5
|
||||
@ -216,15 +180,7 @@ DEFAULT_HISTORY_LENGTH = 5
|
||||
# %%
|
||||
# Globals
|
||||
|
||||
|
||||
def create_system_message(prompt):
|
||||
return [{"role": "system", "content": prompt}]
|
||||
|
||||
|
||||
tool_log = []
|
||||
command_log = []
|
||||
model = None
|
||||
client = None
|
||||
web_server = None
|
||||
|
||||
|
||||
@ -668,7 +624,7 @@ class WebServer:
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
query: QueryOptions = QueryOptions(**data)
|
||||
query: Query = Query(**data)
|
||||
except Exception as e:
|
||||
error = {"error": f"Attempt to parse request: {e}"}
|
||||
logger.info(error)
|
||||
@ -692,7 +648,7 @@ class WebServer:
|
||||
context=context,
|
||||
agent=agent,
|
||||
prompt=query.prompt,
|
||||
options=query.options,
|
||||
tunables=query.tunables,
|
||||
):
|
||||
if message.status != "done" and message.status != "partial":
|
||||
if message.status == "streaming":
|
||||
@ -1019,16 +975,18 @@ class WebServer:
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
async def generate_response(
|
||||
self, context: Context, agent: Agent, prompt: str, options: Tunables | None
|
||||
self, context: Context, agent: Agent, prompt: str, tunables: Tunables | None
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
|
||||
agent_type = agent.get_agent_type()
|
||||
logger.info(f"generate_response: type - {agent_type}")
|
||||
message = Message(prompt=prompt, options=agent.tunables)
|
||||
if options:
|
||||
message.tunables = options
|
||||
# Merge tunables to take agent defaults and override with user supplied settings
|
||||
agent_tunables = agent.tunables.model_dump() if agent.tunables else {}
|
||||
user_tunables = tunables.model_dump() if tunables else {}
|
||||
merged_tunables = {**agent_tunables, **user_tunables}
|
||||
message = Message(prompt=prompt, tunables=Tunables(**merged_tunables))
|
||||
|
||||
async for message in agent.prepare_message(message):
|
||||
# logger.info(f"{agent_type}.prepare_message: {value.status} - {value.response}")
|
||||
|
BIN
src/tests/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/tests/__pycache__/test-agent.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/test-agent.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/tests/__pycache__/test-chunker.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/test-chunker.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/tests/__pycache__/test-context-routing.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/test-context-routing.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/tests/__pycache__/test-context.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/test-context.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/tests/__pycache__/test-message.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/test-message.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/tests/__pycache__/test-metrics.cpython-311.pyc
Normal file
BIN
src/tests/__pycache__/test-metrics.cpython-311.pyc
Normal file
Binary file not shown.
14
src/tests/test-chunker.py
Normal file
14
src/tests/test-chunker.py
Normal file
@ -0,0 +1,14 @@
|
||||
# From /opt/backstory run:
|
||||
# python -m src.tests.test-chunker
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../utils")))
|
||||
from markdown_chunker import MarkdownChunker # type: ignore
|
||||
|
||||
chunker = MarkdownChunker()
|
||||
chunks = chunker.process_file("./src/tests/test.md") # docs/resume/resume.md")
|
||||
for chunk in chunks:
|
||||
print("*" * 50)
|
||||
print(json.dumps(chunk, indent=2))
|
19
src/tests/test.md
Normal file
19
src/tests/test.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Test header
|
||||
|
||||
Text under header
|
||||
|
||||
## Test nested header
|
||||
|
||||
Text under nested header
|
||||
|
||||
* Top level bullet list
|
||||
* Second level bullet list
|
||||
* Another item on second level bullet list
|
||||
* Third item bullet list
|
||||
|
||||
Paragraph under 'Test nested header'
|
||||
|
||||
1. Top level number list
|
||||
1. Second level number list
|
||||
2. Another item on second level number list
|
||||
1. Third item number list
|
@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, PrivateAttr, Field # type: ignore
|
||||
from pydantic import BaseModel, Field # type: ignore
|
||||
from typing import (
|
||||
Literal,
|
||||
get_args,
|
||||
|
@ -4,24 +4,22 @@ from typing import (
|
||||
Dict,
|
||||
Literal,
|
||||
ClassVar,
|
||||
Optional,
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
List,
|
||||
Optional
|
||||
# override
|
||||
) # NOTE: You must import Optional for late binding to work
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
import asyncio
|
||||
import time
|
||||
from collections import defaultdict
|
||||
import asyncio
|
||||
import numpy as np # type: ignore
|
||||
|
||||
from .base import Agent, agent_registry, LLMMessage
|
||||
from ..conversation import Conversation
|
||||
from ..message import Message
|
||||
from ..setup_logging import setup_logging
|
||||
|
||||
@ -31,9 +29,6 @@ logger = setup_logging()
|
||||
system_generate_resume = """
|
||||
""".strip()
|
||||
|
||||
system_job_description = f"""
|
||||
""".strip()
|
||||
|
||||
system_user_qualifications = """
|
||||
Answer questions about the job description.
|
||||
"""
|
||||
@ -55,6 +50,7 @@ class JobDescription(Agent):
|
||||
raise ValueError("Job description cannot be empty")
|
||||
return self
|
||||
|
||||
#@override
|
||||
async def prepare_message(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
|
||||
if not self.context:
|
||||
@ -69,6 +65,7 @@ class JobDescription(Agent):
|
||||
yield message
|
||||
return
|
||||
|
||||
#@override
|
||||
async def process_message(
|
||||
self, llm: Any, model: str, message: Message
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
@ -211,43 +208,6 @@ class JobDescription(Agent):
|
||||
|
||||
# Additional validation can be added here
|
||||
|
||||
def validate_candidate_qualifications(self, candidate_qualifications: Dict) -> None:
|
||||
"""Validate the structure of candidate qualifications."""
|
||||
required_keys = ["candidate_qualifications"]
|
||||
|
||||
if not all(key in candidate_qualifications for key in required_keys):
|
||||
missing = [
|
||||
key for key in required_keys if key not in candidate_qualifications
|
||||
]
|
||||
raise ValueError(
|
||||
f"Missing required keys in candidate qualifications: {missing}"
|
||||
)
|
||||
|
||||
# Additional validation can be added here
|
||||
|
||||
def validate_skills_mapping(self, skills_mapping: Dict) -> None:
|
||||
"""Validate the structure of skills mapping."""
|
||||
required_keys = ["skills_mapping", "resume_recommendations"]
|
||||
|
||||
if not all(key in skills_mapping for key in required_keys):
|
||||
missing = [key for key in required_keys if key not in skills_mapping]
|
||||
raise ValueError(f"Missing required keys in skills mapping: {missing}")
|
||||
|
||||
# Additional validation can be added here
|
||||
|
||||
def extract_header_from_resume(self, resume: str) -> str:
|
||||
"""Extract header information from the original resume."""
|
||||
# Simple implementation - in practice, you might want a more sophisticated approach
|
||||
lines = resume.strip().split("\n")
|
||||
# Take the first few non-empty lines as the header
|
||||
header_lines = []
|
||||
for line in lines[:10]: # Arbitrarily choose first 10 lines to search
|
||||
if line.strip():
|
||||
header_lines.append(line)
|
||||
if len(header_lines) >= 4: # Assume header is no more than 4 lines
|
||||
break
|
||||
return "\n".join(header_lines)
|
||||
|
||||
def generate_resume_from_skill_assessments(
|
||||
self,
|
||||
candidate_name,
|
||||
@ -854,113 +814,6 @@ IMPORTANT: Be factual and precise. If you cannot find strong evidence for this s
|
||||
metadata["error"] = message.response
|
||||
raise
|
||||
|
||||
def format_rag_context(self, rag_results: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Format RAG results from process_job_requirements into a structured string.
|
||||
|
||||
Args:
|
||||
rag_results: List of dictionaries from process_job_requirements.
|
||||
|
||||
Returns:
|
||||
A formatted string for inclusion in the prompt.
|
||||
"""
|
||||
if not rag_results:
|
||||
return "No additional context available."
|
||||
|
||||
# Group results by category and subcategory
|
||||
grouped_context = defaultdict(list)
|
||||
for result in rag_results:
|
||||
key = f"{result['category']}/{result['subcategory']}".strip("/")
|
||||
grouped_context[key].append(
|
||||
{
|
||||
"query": result["context"],
|
||||
"content": (
|
||||
result["content"][:500] + "..."
|
||||
if len(result["content"]) > 500
|
||||
else result["content"]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Format as a structured string
|
||||
context_lines = ["Additional Context from Document Retrieval:"]
|
||||
for category, items in grouped_context.items():
|
||||
context_lines.append(f"\nCategory: {category}")
|
||||
for item in items:
|
||||
context_lines.append(f"- Query: {item['query']}")
|
||||
context_lines.append(f" Relevant Document: {item['content']}")
|
||||
|
||||
return "\n".join(context_lines)
|
||||
|
||||
# Stage 1B: Candidate Analysis Implementation
|
||||
def create_candidate_analysis_prompt(
|
||||
self, resume: str, rag_results: List[Dict[str, Any]]
|
||||
) -> tuple[str, str]:
|
||||
"""Create the prompt for candidate qualifications analysis."""
|
||||
system_prompt = """\
|
||||
You are an objective resume analyzer. Create a concise inventory of the candidate's key skills, experiences, and qualifications based on their resume.
|
||||
|
||||
CORE PRINCIPLES:
|
||||
- Analyze ONLY the candidate's resume and provided context.
|
||||
- Focus on the most significant and relevant qualifications explicitly mentioned.
|
||||
- Limit your analysis to the most important items in each category.
|
||||
- Prioritize brevity and completeness over exhaustiveness.
|
||||
- Complete the entire analysis in one response without getting stuck on any section.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
{
|
||||
"candidate_qualifications": {
|
||||
"technical_skills": [
|
||||
// Include MAX 10 most important technical skills
|
||||
{
|
||||
"skill": "skill name",
|
||||
"evidence_location": "brief reference",
|
||||
"expertise_level": "stated level or 'unspecified'"
|
||||
}
|
||||
],
|
||||
"work_experience": [
|
||||
// Include MAX 5 most recent or relevant positions
|
||||
{
|
||||
"role": "job title",
|
||||
"company": "company name",
|
||||
"duration": "time period",
|
||||
"responsibilities": ["resp1", "resp2"], // MAX 3 key responsibilities
|
||||
"technologies_used": ["tech1", "tech2"], // MAX 5 technologies
|
||||
"achievements": ["achievement1"] // MAX 2 achievements
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
// Include ALL education entries (typically 1-3)
|
||||
{
|
||||
"degree": "degree name",
|
||||
"institution": "institution name",
|
||||
"completed": true/false
|
||||
}
|
||||
],
|
||||
"projects": [
|
||||
// Include MAX 3 most significant projects
|
||||
{
|
||||
"name": "project name",
|
||||
"description": "one sentence description",
|
||||
"technologies_used": ["tech1", "tech2"] // MAX 3 technologies
|
||||
}
|
||||
],
|
||||
"soft_skills": [
|
||||
// Include MAX 5 most prominent soft skills
|
||||
{
|
||||
"skill": "skill name",
|
||||
"context": "brief mention"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
IMPORTANT: If at any point you find yourself repeating items or getting stuck, STOP that section and move to the next. It's better to provide a partial analysis than to get stuck in a loop.
|
||||
"""
|
||||
context = self.format_rag_context(rag_results)
|
||||
prompt = f"Resume:\n{resume}\n\nAdditional Context:\n{context}"
|
||||
return system_prompt, prompt
|
||||
|
||||
async def call_llm(self, message: Message, system_prompt, prompt, temperature=0.7):
|
||||
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
|
||||
|
||||
@ -1018,581 +871,6 @@ IMPORTANT: If at any point you find yourself repeating items or getting stuck, S
|
||||
message.status = "done"
|
||||
yield message
|
||||
|
||||
async def analyze_candidate_qualifications(
|
||||
self,
|
||||
message: Message,
|
||||
resume: str,
|
||||
rag_context: List[Dict[str, Any]],
|
||||
metadata: Dict[str, Any],
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
"""Analyze candidate qualifications from resume and context."""
|
||||
try:
|
||||
system_prompt, prompt = self.create_candidate_analysis_prompt(
|
||||
resume, rag_context
|
||||
)
|
||||
metadata["system_prompt"] = system_prompt
|
||||
metadata["prompt"] = prompt
|
||||
async for message in self.call_llm(message, system_prompt, prompt):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
|
||||
# Extract JSON from response
|
||||
json_str = self.extract_json_from_text(message.response)
|
||||
candidate_qualifications = json.loads(json_str)
|
||||
|
||||
# Validate structure
|
||||
self.validate_candidate_qualifications(candidate_qualifications)
|
||||
|
||||
metadata["results"] = candidate_qualifications["candidate_qualifications"]
|
||||
message.status = "done"
|
||||
message.response = json.dumps(candidate_qualifications)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
metadata["error"] = message.response
|
||||
yield message
|
||||
raise
|
||||
|
||||
# Stage 1C: Mapping Analysis Implementation
|
||||
def create_mapping_analysis_prompt(
|
||||
self, job_requirements: Dict, candidate_qualifications: Dict
|
||||
) -> tuple[str, str]:
|
||||
"""Create the prompt for mapping analysis."""
|
||||
system_prompt = """
|
||||
You are an objective skills mapper. Your task is to identify legitimate matches between job requirements
|
||||
and candidate qualifications WITHOUT fabricating or stretching the truth.
|
||||
|
||||
## INSTRUCTIONS:
|
||||
|
||||
1. Use ONLY the structured job requirements and candidate qualifications provided.
|
||||
2. Create a mapping that shows where the candidate's actual skills and experiences align with job requirements.
|
||||
3. Identify gaps where the candidate lacks required qualifications.
|
||||
4. Suggest legitimate transferable skills ONLY when there is reasonable evidence.
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills_mapping": {
|
||||
"direct_matches": [
|
||||
{
|
||||
"job_requirement": "required skill",
|
||||
"candidate_qualification": "matching skill",
|
||||
"evidence": "exact quote from candidate materials"
|
||||
}
|
||||
],
|
||||
"transferable_skills": [
|
||||
{
|
||||
"job_requirement": "required skill",
|
||||
"candidate_qualification": "transferable skill",
|
||||
"reasoning": "explanation of legitimate transferability",
|
||||
"evidence": "exact quote from candidate materials"
|
||||
}
|
||||
],
|
||||
"gap_analysis": {
|
||||
"missing_required_skills": ["skill1", "skill2"],
|
||||
"missing_preferred_skills": ["skill1", "skill2"],
|
||||
"missing_experience": ["exp1", "exp2"]
|
||||
}
|
||||
},
|
||||
"resume_recommendations": {
|
||||
"highlight_points": [
|
||||
{
|
||||
"qualification": "candidate's qualification",
|
||||
"relevance": "why this is highly relevant to the job"
|
||||
}
|
||||
],
|
||||
"transferable_narratives": [
|
||||
{
|
||||
"from": "candidate's actual experience",
|
||||
"to": "job requirement",
|
||||
"suggested_framing": "how to honestly present the transfer"
|
||||
}
|
||||
],
|
||||
"honest_limitations": [
|
||||
"frank assessment of major qualification gaps"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
CRITICAL RULES:
|
||||
1. A "direct match" requires the EXACT SAME skill in both job requirements and candidate qualifications
|
||||
2. A "transferable skill" must have legitimate, defensible connection - do not stretch credibility
|
||||
3. All "missing_required_skills" MUST be acknowledged - do not ignore major gaps
|
||||
4. Every match or transfer claim must cite specific evidence from the candidate materials
|
||||
5. Be conservative in claiming transferability - when in doubt, list as missing
|
||||
"""
|
||||
|
||||
prompt = f"Job Requirements:\n{json.dumps(job_requirements, indent=2)}\n\n"
|
||||
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}"
|
||||
return system_prompt, prompt
|
||||
|
||||
async def create_skills_mapping(
|
||||
self,
|
||||
message,
|
||||
job_requirements: Dict,
|
||||
candidate_qualifications: Dict,
|
||||
metadata: Dict[str, Any],
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
"""Create mapping between job requirements and candidate qualifications."""
|
||||
json_str = ""
|
||||
try:
|
||||
system_prompt, prompt = self.create_mapping_analysis_prompt(
|
||||
job_requirements, candidate_qualifications
|
||||
)
|
||||
metadata["system_prompt"] = system_prompt
|
||||
metadata["prompt"] = prompt
|
||||
|
||||
async for message in self.call_llm(message, system_prompt, prompt):
|
||||
if message != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
|
||||
# Extract JSON from response
|
||||
json_str = self.extract_json_from_text(message.response)
|
||||
skills_mapping = json.loads(json_str)
|
||||
|
||||
# Validate structure
|
||||
self.validate_skills_mapping(skills_mapping)
|
||||
|
||||
metadata["skills_mapping"] = skills_mapping["skills_mapping"]
|
||||
|
||||
message.status = "done"
|
||||
message.response = json_str
|
||||
yield message
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
metadata["error"] = json_str
|
||||
yield message
|
||||
raise
|
||||
|
||||
# Stage 2: Resume Generation Implementation
|
||||
def create_resume_generation_prompt(
|
||||
self, skills_mapping: Dict, candidate_qualifications: Dict, original_header: str
|
||||
) -> tuple[str, str]:
|
||||
"""Create the prompt for resume generation."""
|
||||
system_prompt = """
|
||||
You are a professional resume writer whose primary concern is FACTUAL ACCURACY. Your task is to create
|
||||
a tailored resume that presents the candidate's actual qualifications in the most relevant way for this job,
|
||||
using ONLY information that has been verified in the skills mapping.
|
||||
|
||||
## INSTRUCTIONS:
|
||||
|
||||
1. Use ONLY the information provided in the skills mapping JSON
|
||||
2. Each skill, experience, or achievement you include MUST appear in either "direct_matches" or "transferable_skills"
|
||||
3. DO NOT include skills listed in "missing_required_skills" or "missing_preferred_skills"
|
||||
4. Format a professional resume with these sections:
|
||||
- Header with name and contact information (exactly as provided in original resume)
|
||||
- Professional Summary (focused on verified matching and transferable skills)
|
||||
- Skills (ONLY from "direct_matches" and "transferable_skills" sections)
|
||||
- Professional Experience (highlighting experiences referenced in the mapping)
|
||||
- Education (exactly as listed in the candidate qualifications)
|
||||
|
||||
5. Follow these principles:
|
||||
- Use the exact wording from "highlight_points" and "transferable_narratives" when describing experiences
|
||||
- Maintain original job titles, companies, and dates exactly as provided
|
||||
- Use achievement-oriented language that emphasizes results and impact
|
||||
- Prioritize experiences that directly relate to the job requirements
|
||||
|
||||
## EVIDENCE REQUIREMENT:
|
||||
|
||||
For each skill or qualification you include in the resume, you MUST be able to trace it to:
|
||||
1. A specific entry in "direct_matches" or "transferable_skills", AND
|
||||
2. The original evidence citation in the candidate qualifications
|
||||
|
||||
If you cannot meet both these requirements for any content, DO NOT include it.
|
||||
|
||||
## FORMAT REQUIREMENTS:
|
||||
|
||||
- Create a clean, professional resume format
|
||||
- Use consistent formatting for similar elements
|
||||
- Ensure readability with appropriate white space
|
||||
- Use bullet points for skills and achievements
|
||||
- Include a final note: "Note: Initial draft of the resume was generated using the Backstory application written by James Ketrenos."
|
||||
|
||||
## FINAL VERIFICATION:
|
||||
|
||||
Before completing the resume:
|
||||
1. Check that EVERY skill listed appears in either "direct_matches" or "transferable_skills"
|
||||
2. Verify that no skills from "missing_required_skills" are included
|
||||
3. Ensure all experience descriptions can be traced to evidence in candidate qualifications
|
||||
4. Confirm that transferable skills are presented honestly without exaggeration
|
||||
"""
|
||||
|
||||
prompt = f"Skills Mapping:\n{json.dumps(skills_mapping, indent=2)}\n\n"
|
||||
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}\n\n"
|
||||
prompt += f"Original Resume Header:\n{original_header}"
|
||||
return system_prompt, prompt
|
||||
|
||||
async def generate_tailored_resume(
|
||||
self,
|
||||
message,
|
||||
skills_mapping: Dict,
|
||||
candidate_qualifications: Dict,
|
||||
original_header: str,
|
||||
metadata: Dict[str, Any],
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
"""Generate a tailored resume based on skills mapping."""
|
||||
system_prompt, prompt = self.create_resume_generation_prompt(
|
||||
skills_mapping, candidate_qualifications, original_header
|
||||
)
|
||||
metadata["system_prompt"] = system_prompt
|
||||
metadata["prompt"] = prompt
|
||||
async for message in self.call_llm(
|
||||
message, system_prompt, prompt, temperature=0.4
|
||||
): # Slightly higher temperature for better writing
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
metadata["results"] = message.response
|
||||
yield message
|
||||
return
|
||||
|
||||
# Stage 3: Verification Implementation
|
||||
def create_verification_prompt(
|
||||
self,
|
||||
generated_resume: str,
|
||||
skills_mapping: Dict,
|
||||
candidate_qualifications: Dict,
|
||||
) -> tuple[str, str]:
|
||||
"""Create the prompt for resume verification."""
|
||||
system_prompt = """
|
||||
You are a critical resume fact-checker responsible for verifying the accuracy of a tailored resume.
|
||||
Your task is to identify and flag any fabricated or embellished information that does not appear in
|
||||
the candidate's original materials.
|
||||
|
||||
## INSTRUCTIONS:
|
||||
|
||||
1. Compare the tailored resume against:
|
||||
- The structured skills mapping
|
||||
- The candidate's original qualifications
|
||||
|
||||
2. Perform a line-by-line verification focusing on:
|
||||
- Skills claimed vs. skills verified in original materials
|
||||
- Experience descriptions vs. actual documented experience
|
||||
- Projects and achievements vs. documented accomplishments
|
||||
- Technical knowledge claims vs. verified technical background
|
||||
|
||||
3. Create a verification report with these sections:
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
|
||||
```json
|
||||
{
|
||||
"verification_results": {
|
||||
"factual_accuracy": {
|
||||
"status": "PASS/FAIL",
|
||||
"issues": [
|
||||
{
|
||||
"claim": "The specific claim in the resume",
|
||||
"issue": "Why this is problematic",
|
||||
"source_check": "Result of checking against source materials",
|
||||
"suggested_correction": "How to fix this issue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"skill_verification": {
|
||||
"status": "PASS/FAIL",
|
||||
"unverified_skills": ["skill1", "skill2"]
|
||||
},
|
||||
"experience_verification": {
|
||||
"status": "PASS/FAIL",
|
||||
"problematic_statements": [
|
||||
{
|
||||
"statement": "The problematic experience statement",
|
||||
"issue": "Why this is problematic",
|
||||
"suggested_correction": "How to fix this issue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overall_assessment": "APPROVED/NEEDS REVISION",
|
||||
"correction_instructions": "Specific instructions for correcting the resume"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CRITICAL VERIFICATION CRITERIA:
|
||||
|
||||
1. Any skill mentioned in the resume MUST appear verbatim in the skills mapping
|
||||
2. Any technology experience claimed MUST be explicitly documented in original materials
|
||||
3. Role descriptions must not imply expertise with technologies not listed in original materials
|
||||
4. "Transferable skills" must be reasonably transferable, not stretches or fabrications
|
||||
5. Job titles, dates, and companies must match exactly with original materials
|
||||
6. Professional summary must not imply experience with technologies from the job description that aren't in the candidate's background
|
||||
|
||||
## SPECIAL ATTENTION:
|
||||
|
||||
Pay particular attention to subtle fabrications such as:
|
||||
- Vague wording that implies experience ("worked with", "familiar with", "utilized") with technologies not in original materials
|
||||
- Reframing unrelated experience to falsely imply relevance to the job requirements
|
||||
- Adding technologies to project descriptions that weren't mentioned in the original materials
|
||||
- Exaggerating level of involvement or responsibility in projects or roles
|
||||
- Creating achievements that weren't documented in the original materials
|
||||
"""
|
||||
|
||||
prompt = f"Tailored Resume:\n{generated_resume}\n\n"
|
||||
prompt += f"Skills Mapping:\n{json.dumps(skills_mapping, indent=2)}\n\n"
|
||||
prompt += f"Candidate Qualifications:\n{json.dumps(candidate_qualifications, indent=2)}"
|
||||
return system_prompt, prompt
|
||||
|
||||
async def verify_resume(
|
||||
self,
|
||||
message: Message,
|
||||
generated_resume: str,
|
||||
skills_mapping: Dict,
|
||||
candidate_qualifications: Dict,
|
||||
metadata: Dict[str, Any],
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
"""Verify the generated resume for accuracy against original materials."""
|
||||
try:
|
||||
system_prompt, prompt = self.create_verification_prompt(
|
||||
generated_resume, skills_mapping, candidate_qualifications
|
||||
)
|
||||
metadata["system_prompt"] = system_prompt
|
||||
metadata["prompt"] = prompt
|
||||
async for message in self.call_llm(message, system_prompt, prompt):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
|
||||
# Extract JSON from response
|
||||
json_str = self.extract_json_from_text(message.response)
|
||||
metadata["results"] = json.loads(json_str)["verification_results"]
|
||||
message.status = "done"
|
||||
message.response = json_str
|
||||
yield message
|
||||
return
|
||||
except Exception as e:
|
||||
metadata["error"] = message.response
|
||||
yield message
|
||||
raise
|
||||
|
||||
async def correct_resume_issues(
|
||||
self,
|
||||
message: Message,
|
||||
generated_resume: str,
|
||||
verification_results: Dict,
|
||||
skills_mapping: Dict,
|
||||
candidate_qualifications: Dict,
|
||||
original_header: str,
|
||||
metadata: Dict[str, Any],
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
"""Correct issues in the resume based on verification results."""
|
||||
if (
|
||||
verification_results["verification_results"]["overall_assessment"]
|
||||
== "APPROVED"
|
||||
):
|
||||
message.status = "done"
|
||||
message.status = generated_resume
|
||||
yield message
|
||||
return
|
||||
|
||||
system_prompt = """\
|
||||
You are a professional resume editor with a focus on factual accuracy. Your task is to correct identified issues in a tailored resume according to the verification report.
|
||||
|
||||
## REFERENCE DATA:
|
||||
The following sections contain reference information for you to use when making corrections. This information should NOT be included in your output resume:
|
||||
|
||||
1. Original Resume - The resume you will correct
|
||||
2. Verification Results - Issues that need correction
|
||||
3. Skills Mapping - How candidate skills align with job requirements
|
||||
4. Candidate Qualifications - Verified information about the candidate's background
|
||||
5. Original Resume Header - The formatting of the resume header
|
||||
|
||||
## INSTRUCTIONS:
|
||||
|
||||
1. Make ONLY the changes specified in the verification report
|
||||
2. Ensure all corrections maintain factual accuracy based on the skills mapping
|
||||
3. Do not introduce any new claims or skills not present in the verification data
|
||||
4. Maintain the original format and structure of the resume
|
||||
5. Provide ONLY the fully corrected resume as your output
|
||||
6. DO NOT include any of the reference data sections in your output
|
||||
7. DO NOT include any additional comments, explanations, or notes in your output
|
||||
|
||||
## PROCESS:
|
||||
|
||||
1. For each issue in the verification report:
|
||||
- Identify the problematic text in the resume
|
||||
- Replace it with the suggested correction
|
||||
- Ensure the correction is consistent with the rest of the resume
|
||||
|
||||
2. After making all corrections:
|
||||
- Review the revised resume for consistency
|
||||
- Ensure no factual inaccuracies have been introduced
|
||||
- Check that all formatting remains professional
|
||||
|
||||
Your output should contain ONLY the corrected resume text with no additional explanations or context.
|
||||
"""
|
||||
prompt = """
|
||||
## REFERENCE DATA
|
||||
|
||||
### Original Resume:
|
||||
"""
|
||||
prompt += generated_resume
|
||||
prompt += """
|
||||
|
||||
### Verification Results:
|
||||
"""
|
||||
prompt += json.dumps(verification_results, indent=2)
|
||||
prompt += """
|
||||
|
||||
### Skills Mapping:
|
||||
"""
|
||||
prompt += json.dumps(skills_mapping, indent=2)
|
||||
prompt += """
|
||||
|
||||
### Candidate Qualifications:
|
||||
"""
|
||||
prompt += json.dumps(candidate_qualifications, indent=2)
|
||||
prompt += """
|
||||
|
||||
### Original Resume Header:
|
||||
"""
|
||||
prompt += generated_resume
|
||||
prompt += """
|
||||
|
||||
## TASK
|
||||
Based on the reference data above, please create a corrected version of the resume that addresses all issues identified in the verification report. Return ONLY the corrected resume.
|
||||
"""
|
||||
|
||||
metadata["system_prompt"] = system_prompt
|
||||
metadata["prompt"] = prompt
|
||||
|
||||
async for message in self.call_llm(
|
||||
message, prompt, system_prompt, temperature=0.3
|
||||
):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
metadata["results"] = message.response
|
||||
yield message
|
||||
|
||||
def process_job_requirements2(
|
||||
self, job_requirements: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Process job requirements JSON, gather RAG documents using find_similar, remove duplicates,
|
||||
and return top 20 results.
|
||||
|
||||
Args:
|
||||
job_requirements: Dictionary containing job requirements.
|
||||
retriever: Instance of RagRetriever with find_similar method.
|
||||
|
||||
Returns:
|
||||
List of up to 20 ChromaDB documents, sorted by combined importance and similarity score.
|
||||
"""
|
||||
if self.context is None or self.context.file_watcher is None:
|
||||
raise ValueError(f"context or file_watcher is None on {self.agent_type}")
|
||||
|
||||
retriever = self.context.file_watcher
|
||||
# Importance weights for each category
|
||||
importance_weights = {
|
||||
("technical_skills", "required"): 1.0,
|
||||
("technical_skills", "preferred"): 0.8,
|
||||
("experience_requirements", "required"): 0.95,
|
||||
("experience_requirements", "preferred"): 0.75,
|
||||
("education_requirements", ""): 0.7,
|
||||
("soft_skills", ""): 0.6,
|
||||
("industry_knowledge", ""): 0.65,
|
||||
("responsibilities", ""): 0.85,
|
||||
("company_values", ""): 0.5,
|
||||
}
|
||||
|
||||
# Store all RAG results with metadata
|
||||
all_results = []
|
||||
|
||||
def traverse_requirements(data: Any, category: str = "", subcategory: str = ""):
|
||||
"""
|
||||
Recursively traverse the job requirements and gather RAG documents.
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
new_subcategory = key if category else ""
|
||||
traverse_requirements(value, category or key, new_subcategory)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
# Determine the weight key
|
||||
weight_key = (
|
||||
(category, subcategory) if subcategory else (category, "")
|
||||
)
|
||||
weight = importance_weights.get(weight_key, 0.5) # Default weight
|
||||
|
||||
# Call find_similar for the item
|
||||
try:
|
||||
rag_results = retriever.find_similar(
|
||||
item, top_k=20, threshold=0.5
|
||||
) # Moderate matching
|
||||
# Process each result
|
||||
for doc_id, content, distance, metadata in zip(
|
||||
rag_results["ids"],
|
||||
rag_results["documents"],
|
||||
rag_results["distances"],
|
||||
rag_results["metadatas"],
|
||||
):
|
||||
# Convert cosine distance to similarity score (higher is better)
|
||||
similarity_score = (
|
||||
1 - distance
|
||||
) # Cosine distance to similarity
|
||||
all_results.append(
|
||||
{
|
||||
"id": doc_id,
|
||||
"content": content,
|
||||
"score": similarity_score,
|
||||
"weight": weight,
|
||||
"context": item,
|
||||
"category": category,
|
||||
"subcategory": subcategory,
|
||||
"metadata": metadata,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing context '{item}': {e}")
|
||||
|
||||
# Start traversal
|
||||
traverse_requirements(job_requirements)
|
||||
|
||||
# Remove duplicates based on document ID
|
||||
unique_results = []
|
||||
seen_ids = set()
|
||||
for result in all_results:
|
||||
if result["id"] not in seen_ids:
|
||||
seen_ids.add(result["id"])
|
||||
unique_results.append(result)
|
||||
|
||||
# Sort by combined score (weight * similarity score)
|
||||
sorted_results = sorted(
|
||||
unique_results, key=lambda x: x["weight"] * x["score"], reverse=True
|
||||
)
|
||||
|
||||
# Return top 10 results
|
||||
return sorted_results[:10]
|
||||
|
||||
async def generate_rag_content(
|
||||
self, message: Message, job_requirements: Dict[str, Any]
|
||||
) -> AsyncGenerator[Message, None]:
|
||||
results = self.process_job_requirements2(job_requirements=job_requirements)
|
||||
message.response = f"Retrieved {len(results)} documents."
|
||||
message.metadata["rag_context"] = results
|
||||
# for result in results:
|
||||
# message.response += f"""
|
||||
# ID: {result['id']}, Context: {result['context']}, \
|
||||
# Category: {result['category']}/{result['subcategory']}, \
|
||||
# Similarity Score: {result['score']:.3f}, \
|
||||
# Combined Score: {result['weight'] * result['score']:.3f}, \
|
||||
# Content: {result['content']}
|
||||
# """
|
||||
message.status = "done"
|
||||
yield message
|
||||
return
|
||||
|
||||
def rag_function(self, skill: str) -> tuple[str, list[Any]]:
|
||||
if self.context is None or self.context.file_watcher is None:
|
||||
raise ValueError("self.context or self.context.file_watcher is None")
|
||||
@ -1787,7 +1065,6 @@ Content: { content }
|
||||
}
|
||||
|
||||
# Extract header from original resume
|
||||
original_header = self.extract_header_from_resume(resume)
|
||||
candidate_info = {
|
||||
"name": "James Ketrenos",
|
||||
"contact_info": {
|
||||
@ -1819,81 +1096,5 @@ Content: { content }
|
||||
yield message
|
||||
return
|
||||
|
||||
# Stage 3: Verify resume
|
||||
message.status = "thinking"
|
||||
message.response = "Multi-stage RAG resume generation process: Stage 3: Verifying resume for accuracy"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
metadata["verify_resume"] = {"first_pass": {}}
|
||||
async for message in self.verify_resume(
|
||||
message=message,
|
||||
generated_resume=generated_resume,
|
||||
skills_mapping=skills_mapping,
|
||||
candidate_qualifications=candidate_qualifications,
|
||||
metadata=metadata["verify_resume"]["first_pass"],
|
||||
):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
|
||||
verification_results = json.loads(message.response)
|
||||
|
||||
# Handle corrections if needed
|
||||
if (
|
||||
verification_results["verification_results"]["overall_assessment"]
|
||||
== "NEEDS REVISION"
|
||||
):
|
||||
message.status = "thinking"
|
||||
message.response = "Correcting issues found in verification"
|
||||
logger.info(message.response)
|
||||
yield message
|
||||
|
||||
metadata["correct_resume_issues"] = {}
|
||||
async for message in self.correct_resume_issues(
|
||||
message=message,
|
||||
generated_resume=generated_resume,
|
||||
verification_results=verification_results,
|
||||
skills_mapping=skills_mapping,
|
||||
candidate_qualifications=candidate_qualifications,
|
||||
original_header=original_header,
|
||||
metadata=metadata["correct_resume_issues"],
|
||||
):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
|
||||
generated_resume = message.response
|
||||
|
||||
# Re-verify after corrections
|
||||
message.status = "thinking"
|
||||
message.response = "Re-verifying corrected resume"
|
||||
yield message
|
||||
|
||||
logger.info(message.response)
|
||||
metadata["verify_resume"]["second_pass"] = {}
|
||||
async for message in self.verify_resume(
|
||||
message=message,
|
||||
generated_resume=generated_resume,
|
||||
skills_mapping=skills_mapping,
|
||||
candidate_qualifications=candidate_qualifications,
|
||||
metadata=metadata["verify_resume"]["second_pass"],
|
||||
):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
if message.status == "error":
|
||||
return
|
||||
verification_results = json.loads(message.response)
|
||||
|
||||
# Return the final results
|
||||
message.status = "done"
|
||||
message.response = generated_resume
|
||||
yield message
|
||||
|
||||
logger.info("Resume generation process completed successfully")
|
||||
return
|
||||
|
||||
|
||||
# Register the base agent
|
||||
agent_registry.register(JobDescription._agent_type, JobDescription)
|
||||
|
@ -176,6 +176,7 @@ class Context(BaseModel):
|
||||
|
||||
raise ValueError(f"No agent class found for agent_type: {agent_type}")
|
||||
|
||||
@classmethod
|
||||
def add_agent(self, agent: AnyAgent) -> None:
|
||||
"""Add a Agent to the context, ensuring no duplicate agent_type."""
|
||||
if any(s.agent_type == agent.agent_type for s in self.agents):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, Field, PrivateAttr # type: ignore
|
||||
from pydantic import BaseModel, Field # type: ignore
|
||||
from typing import List
|
||||
from .message import Message
|
||||
|
||||
|
212
src/utils/markdown_chunker.py
Normal file
212
src/utils/markdown_chunker.py
Normal file
@ -0,0 +1,212 @@
|
||||
from typing import List, Dict, Any, Optional, TypedDict, Tuple
|
||||
from markdown_it import MarkdownIt
|
||||
from markdown_it.tree import SyntaxTreeNode
|
||||
import traceback
|
||||
import logging
|
||||
import json
|
||||
|
||||
|
||||
class Chunk(TypedDict):
|
||||
text: str
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
def clear_chunk(chunk: Chunk):
|
||||
chunk["text"] = ""
|
||||
chunk["metadata"] = {
|
||||
"doc_type": "unknown",
|
||||
"source_file": chunk["metadata"]["source_file"],
|
||||
"path": "", # This will be updated during processing
|
||||
"level": 0,
|
||||
}
|
||||
return chunk
|
||||
|
||||
|
||||
class MarkdownChunker:
|
||||
def __init__(self):
|
||||
# Initialize the Markdown parser
|
||||
self.md_parser = MarkdownIt("commonmark")
|
||||
|
||||
def process_file(self, file_path: str) -> Optional[List[Chunk]]:
|
||||
"""
|
||||
Process a single markdown file and return chunks.
|
||||
|
||||
Args:
|
||||
file_path: Path to the markdown file
|
||||
|
||||
Returns:
|
||||
List of chunks with metadata or None if file can't be processed
|
||||
"""
|
||||
try:
|
||||
# Read the markdown file
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse the markdown
|
||||
tokens = self.md_parser.parse(content)
|
||||
|
||||
ast = SyntaxTreeNode(tokens)
|
||||
|
||||
# Extract chunks with metadata
|
||||
chunks = self.extract_chunks(ast, file_path)
|
||||
|
||||
return chunks
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing {file_path}: {str(e)}")
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
return None
|
||||
|
||||
def extract_chunks(self, ast: SyntaxTreeNode, file_path: str) -> List[Chunk]:
|
||||
"""
|
||||
Extract logical chunks from the AST with appropriate metadata.
|
||||
|
||||
Args:
|
||||
ast: The Abstract Syntax Tree from markdown-it-py
|
||||
file_path: Path to the source file
|
||||
|
||||
Returns:
|
||||
List of chunks with metadata
|
||||
"""
|
||||
chunks: List[Chunk] = []
|
||||
current_headings: List[str] = [""] * 6 # Track h1-h6 headings
|
||||
|
||||
# Initialize a chunk structure
|
||||
chunk: Chunk = {
|
||||
"text": "",
|
||||
"metadata": {
|
||||
"source_file": file_path,
|
||||
},
|
||||
}
|
||||
clear_chunk(chunk)
|
||||
|
||||
# Process the AST recursively
|
||||
self._process_node(ast, current_headings, chunks, chunk, level=0)
|
||||
|
||||
return chunks
|
||||
|
||||
def _sanitize_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
k: ("" if v is None else v) for k, v in metadata.items() if v is not None
|
||||
}
|
||||
|
||||
def _extract_text_from_children(self, node: SyntaxTreeNode) -> str:
|
||||
lines = []
|
||||
for child in node.children:
|
||||
if child.type == "list_item":
|
||||
lines.append(f"- {self._extract_text_from_children(child).strip()}")
|
||||
elif child.type == "fence":
|
||||
info = f"{child.info}" if hasattr(child, "info") and child.info else ""
|
||||
lines.append(f"\n```{info}\n{child.content.strip()}\n```\n")
|
||||
elif child.children:
|
||||
lines.append(self._extract_text_from_children(child).strip())
|
||||
elif hasattr(child, "content"):
|
||||
lines.append(child.content.strip())
|
||||
return "\n".join(lines)
|
||||
|
||||
def _process_node(
|
||||
self,
|
||||
node: SyntaxTreeNode,
|
||||
current_headings: list[str],
|
||||
chunks: List[Chunk],
|
||||
chunk: Chunk,
|
||||
level: int,
|
||||
) -> int:
|
||||
is_list = False
|
||||
# Handle heading nodes
|
||||
if node.type == "heading":
|
||||
# print(f'{" " * level}{chunk["metadata"]["path"]} {node.type}')
|
||||
if node.is_nested and node.nester_tokens:
|
||||
opening, closing = node.nester_tokens
|
||||
level = int(opening.tag[1:]) - 1
|
||||
|
||||
heading_text = self._extract_text_from_children(node)
|
||||
|
||||
# Update lower heading states
|
||||
current_headings[level] = heading_text
|
||||
for i in range(level, 6):
|
||||
if i != level:
|
||||
current_headings[i] = ""
|
||||
|
||||
# Store previous chunk if it has text
|
||||
if chunk["text"].strip():
|
||||
path = " > ".join([h for h in current_headings if h])
|
||||
chunk["text"] = chunk["text"].strip()
|
||||
chunk["metadata"]["path"] = path
|
||||
chunk["metadata"]["level"] = level
|
||||
if node.nester_tokens:
|
||||
opening, closing = node.nester_tokens
|
||||
if opening and opening.map:
|
||||
(
|
||||
chunk["metadata"]["line_begin"],
|
||||
chunk["metadata"]["line_end"],
|
||||
) = opening.map
|
||||
chunks.append(chunk.copy())
|
||||
clear_chunk(chunk)
|
||||
|
||||
# Add code block directly to current chunk
|
||||
elif node.type == "fence":
|
||||
# print(f'{" " * level}{chunk["metadata"]["path"]} {node.type}')
|
||||
language = node.info.strip() if hasattr(node, "info") and node.info else ""
|
||||
code_block = f"\n```{language}\n{node.content.strip()}\n```\n"
|
||||
chunk["text"] += code_block
|
||||
|
||||
# Handle list structures
|
||||
# elif node.type in ["list", "list_item"]:
|
||||
# # print(node.nester_tokens)
|
||||
# # print(node.pretty(show_text=True))
|
||||
# is_list = True
|
||||
# list_chunk = []
|
||||
# for child in node.children:
|
||||
# level = self._process_node(child, current_headings, chunks, chunk, level=level)
|
||||
|
||||
# Handle paragraph
|
||||
elif node.type in ["paragraph"]: # , "list", "list_item"]:
|
||||
text = self._extract_text_from_children(node)
|
||||
# indented_text = "\n".join([f'{"-" * (level + 2)}{line}' for line in text.split('\n')])
|
||||
# print(f'{"-" * level}{chunk["metadata"]["path"]} {node.type}:\n{indented_text} {len(node.children)}\n')
|
||||
text = text.strip()
|
||||
if text:
|
||||
# indented_text = "\n".join([f'{"-" * (level + 2)}{line}' for line in text.split('\n')])
|
||||
# print(f'{" " * level}{chunk["metadata"]["path"]} {node.type}:\n{indented_text}\n')
|
||||
chunk["text"] += f"\n{text}\n"
|
||||
chunk["text"] = chunk["text"].strip()
|
||||
if chunk["text"]:
|
||||
path = " > ".join([h for h in current_headings if h])
|
||||
chunk["text"] = chunk["text"]
|
||||
chunk["metadata"]["path"] = path
|
||||
chunk["metadata"]["level"] = level
|
||||
if node.nester_tokens:
|
||||
opening, closing = node.nester_tokens
|
||||
if opening and opening.map:
|
||||
(
|
||||
chunk["metadata"]["line_begin"],
|
||||
chunk["metadata"]["line_end"],
|
||||
) = opening.map
|
||||
chunks.append(chunk.copy())
|
||||
clear_chunk(chunk)
|
||||
|
||||
# Recursively process children
|
||||
if not is_list:
|
||||
for child in node.children:
|
||||
level = self._process_node(
|
||||
child, current_headings, chunks, chunk, level=level
|
||||
)
|
||||
|
||||
# After root-level recursion, finalize any remaining chunk
|
||||
if node.type == "document":
|
||||
# print(node.type)
|
||||
if chunk["text"].strip():
|
||||
path = " > ".join([h for h in current_headings if h])
|
||||
chunk["metadata"]["path"] = path
|
||||
if node.nester_tokens:
|
||||
opening, closing = node.nester_tokens
|
||||
if opening and opening.map:
|
||||
(
|
||||
chunk["metadata"]["line_begin"],
|
||||
chunk["metadata"]["line_end"],
|
||||
) = opening.map
|
||||
chunks.append(chunk.copy())
|
||||
|
||||
return level
|
@ -1,8 +1,6 @@
|
||||
from pydantic import BaseModel, Field # type: ignore
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timezone
|
||||
from asyncio import Event
|
||||
|
||||
|
||||
class Tunables(BaseModel):
|
||||
enable_rag: bool = Field(default=True) # Enable RAG collection chromadb matching
|
||||
|
@ -1,7 +1,6 @@
|
||||
from prometheus_client import Counter, Gauge, Summary, Histogram, Info, Enum, CollectorRegistry # type: ignore
|
||||
from prometheus_client import Counter, Histogram # type: ignore
|
||||
from threading import Lock
|
||||
|
||||
|
||||
def singleton(cls):
|
||||
instance = None
|
||||
lock = Lock()
|
||||
|
@ -21,7 +21,7 @@ import chromadb
|
||||
import ollama
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
from watchdog.events import FileSystemEventHandler # type: ignore
|
||||
import umap # type: ignore
|
||||
import umap # type: ignore
|
||||
from markitdown import MarkItDown # type: ignore
|
||||
from chromadb.api.models.Collection import Collection # type: ignore
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user