Multi-user almost working
This commit is contained in:
parent
4bbaa15e09
commit
72219100ee
@ -21,11 +21,10 @@ services:
|
||||
ports:
|
||||
- 8912:8911 # FastAPI React server
|
||||
volumes:
|
||||
- ./cache:/root/.cache # Persist all models and GPU kernel cache
|
||||
- ./cache:/root/.cache # Persist all models and GPU kernel cache
|
||||
- ./sessions:/opt/backstory/sessions:rw # Persist sessions
|
||||
- ./chromadb:/opt/backstory/chromadb:rw # Persist ChromaDB
|
||||
- ./dev-keys:/opt/backstory/keys:ro # Developer keys
|
||||
- ./docs:/opt/backstory/docs:rw # Live mount of RAG content
|
||||
- ./users:/opt/backstory/users:rw # Live mount of user data
|
||||
- ./src:/opt/backstory/src:rw # Live mount server src
|
||||
cap_add: # used for running ze-monitor within container
|
||||
- CAP_DAC_READ_SEARCH # Bypass all filesystem read access checks
|
||||
@ -58,6 +57,7 @@ services:
|
||||
- ./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
|
||||
- ./users-prod:/opt/backstory/users:rw # Live mount of user data
|
||||
- ./frontend/deployed:/opt/backstory/frontend/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
|
||||
|
@ -137,7 +137,7 @@ const Main = (props: MainProps) => {
|
||||
)
|
||||
};
|
||||
|
||||
if (sessionId === undefined) {
|
||||
if (sessionId === undefined || !sessionId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
|
||||
return [loadingTab];
|
||||
} else {
|
||||
return [
|
||||
@ -183,6 +183,9 @@ const Main = (props: MainProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined || !sessionId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
|
||||
return;
|
||||
}
|
||||
const pathParts = window.location.pathname.split('/').filter(Boolean);
|
||||
const currentPath = pathParts.length < 2 ? '' : pathParts[0];
|
||||
let currentSubRoute = pathParts.length > 2 ? pathParts.slice(1, -1).join('/') : '';
|
||||
@ -197,7 +200,7 @@ const Main = (props: MainProps) => {
|
||||
setTab(tabs[tabIndex]);
|
||||
setSubRoute(currentSubRoute);
|
||||
console.log(`Initial load set to tab ${tabs[tabIndex].path} subRoute: ${currentSubRoute}`);
|
||||
}, [tabs]);
|
||||
}, [tabs, sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === undefined || sessionId === undefined) {
|
||||
|
@ -3,8 +3,12 @@ import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { connectionBase } from '../Global';
|
||||
import { SetSnackType } from '../Components/Snack';
|
||||
|
||||
const getSessionId = async () => {
|
||||
const response = await fetch(connectionBase + `/api/context`, {
|
||||
const getSessionId = async (userId?: string) => {
|
||||
const endpoint = userId
|
||||
? `/api/context/u/${encodeURIComponent(userId)}`
|
||||
: `/api/context`;
|
||||
|
||||
const response = await fetch(connectionBase + endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -34,9 +38,27 @@ const SessionWrapper = ({ setSnack, children }: SessionWrapperProps) => {
|
||||
const [retry, setRetry] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`SessionWrapper: ${location.pathname}`);
|
||||
|
||||
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;
|
||||
|
||||
// Case: path starts with "u/{USERID}"
|
||||
if (parts.length >= 2 && parts[0] === "u") {
|
||||
const userId = parts[1];
|
||||
|
||||
// Case: "u/{USERID}" - fetch session for this user
|
||||
const activeSession = await getSessionId(userId);
|
||||
setSessionId(activeSession);
|
||||
|
||||
// Append session to path
|
||||
const newPath = [...parts, activeSession].join("/");
|
||||
navigate(`/${activeSession}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Default case (original behavior)
|
||||
const hasSession = parts.length !== 0 && pattern.test(parts[parts.length - 1]);
|
||||
|
||||
if (!hasSession) {
|
||||
@ -53,18 +75,24 @@ const SessionWrapper = ({ setSnack, children }: SessionWrapperProps) => {
|
||||
|
||||
if (!fetchingRef.current) {
|
||||
fetchingRef.current = true;
|
||||
ensureSessionId().catch((e) => {
|
||||
console.error(e);
|
||||
setSnack("Backstory is temporarily unavailable. Retrying in 5 seconds.", "warning");
|
||||
setTimeout(() => {
|
||||
fetchingRef.current = false;
|
||||
setRetry(retry => retry + 1)
|
||||
}, 5000);
|
||||
});
|
||||
ensureSessionId()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setSnack("Backstory is temporarily unavailable. Retrying in 5 seconds.", "warning");
|
||||
setTimeout(() => {
|
||||
fetchingRef.current = false;
|
||||
setRetry(retry => retry + 1);
|
||||
}, 5000);
|
||||
})
|
||||
.finally(() => {
|
||||
if (fetchingRef.current) {
|
||||
fetchingRef.current = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [location.pathname, navigate, setSnack, sessionId, retry]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { SessionWrapper };
|
||||
export { SessionWrapper };
|
@ -117,8 +117,6 @@ const MessageMeta = (props: MessageMetaProps) => {
|
||||
} = props.metadata || {};
|
||||
const message: any = props.messageProps.message;
|
||||
|
||||
console.log(tools, rag);
|
||||
|
||||
let llm_submission: string = "<|system|>\n"
|
||||
llm_submission += message.system_prompt + "\n\n"
|
||||
llm_submission += message.context_prompt
|
||||
|
@ -163,7 +163,7 @@ const colorMap: Record<string, string> = {
|
||||
resume: '#4A7A7D', // Dusty Teal — secondary theme color
|
||||
projects: '#1A2536', // Midnight Blue — rich and deep
|
||||
news: '#D3CDBF', // Warm Gray — soft and neutral
|
||||
'performance-reviews': '#FFD0D0', // Light red
|
||||
'performance-reviews': '#8FD0D0', // Light red
|
||||
'jobs': '#F3aD8F', // Warm Gray — soft and neutral
|
||||
};
|
||||
|
||||
@ -214,7 +214,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
||||
const plotContainerRect = plotContainer.getBoundingClientRect();
|
||||
svgContainer.style.width = `${plotContainerRect.width}px`;
|
||||
svgContainer.style.height = `${plotContainerRect.height}px`;
|
||||
if (plotDimensions.width !== plotContainerRect.width || plotDimensions.height != plotContainerRect.height) {
|
||||
if (plotDimensions.width !== plotContainerRect.width || plotDimensions.height !== plotContainerRect.height) {
|
||||
setPlotDimensions({ width: plotContainerRect.width, height: plotContainerRect.height });
|
||||
}
|
||||
}
|
||||
@ -347,6 +347,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
||||
size: filtered_sizes,
|
||||
symbol: 'circle',
|
||||
color: filtered_colors,
|
||||
opacity: 1
|
||||
},
|
||||
text: filtered.ids,
|
||||
customdata: filtered.metadatas,
|
||||
@ -361,6 +362,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
||||
size: query_sizes,
|
||||
symbol: 'circle',
|
||||
color: query_colors,
|
||||
opacity: 1
|
||||
},
|
||||
text: query.ids,
|
||||
customdata: query.metadatas,
|
||||
@ -473,13 +475,6 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
|
||||
<Box className="VectorVisualizer"
|
||||
ref={boxRef}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
m: 0,
|
||||
p: 0,
|
||||
border: "none",
|
||||
...sx
|
||||
}}>
|
||||
<Box sx={{ p: 0, m: 0, gap: 0 }}>
|
||||
@ -513,7 +508,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
|
||||
control={<Switch checked={!view2D} />} onChange={() => setView2D(!view2D)} label="3D" />
|
||||
<Plot
|
||||
ref={plotlyRef}
|
||||
onClick={(event: any) => { console.log("click"); onNodeSelected(event.points[0].customdata); }}
|
||||
onClick={(event: any) => { onNodeSelected(event.points[0].customdata); }}
|
||||
data={plotData}
|
||||
useResizeHandler={true}
|
||||
config={config}
|
||||
|
@ -19,7 +19,6 @@ import { BackstoryPageProps } from '../Components/BackstoryTab';
|
||||
|
||||
interface ServerTunables {
|
||||
system_prompt: string,
|
||||
message_history_length: number,
|
||||
tools: Tool[],
|
||||
rags: Tool[]
|
||||
};
|
||||
@ -125,38 +124,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
|
||||
}, [systemPrompt, sessionId, setSnack, serverTunables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverTunables === undefined || messageHistoryLength === serverTunables.message_history_length || !messageHistoryLength || sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
const sendMessageHistoryLength = async (length: number) => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/tunables/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ "message_history_length": length }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const newLength = data["message_history_length"];
|
||||
if (newLength !== messageHistoryLength) {
|
||||
setMessageHistoryLength(newLength);
|
||||
setSnack("Message history length updated", "success");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
setSnack("Message history length update failed", "error");
|
||||
}
|
||||
};
|
||||
|
||||
sendMessageHistoryLength(messageHistoryLength);
|
||||
|
||||
}, [messageHistoryLength, setMessageHistoryLength, sessionId, setSnack, serverTunables]);
|
||||
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt" | "message_history_length")[], message: string = "Update successful.") => {
|
||||
const reset = async (types: ("rags" | "tools" | "history" | "system_prompt")[], message: string = "Update successful.") => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/reset/${sessionId}`, {
|
||||
method: 'PUT',
|
||||
@ -308,7 +276,6 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
// console.log("Server tunables: ", data);
|
||||
setServerTunables(data);
|
||||
setSystemPrompt(data["system_prompt"]);
|
||||
setMessageHistoryLength(data["message_history_length"]);
|
||||
setTools(data["tools"]);
|
||||
setRags(data["rags"]);
|
||||
} catch (error) {
|
||||
@ -453,7 +420,7 @@ const ControlsPage = (props: BackstoryPageProps) => {
|
||||
</Accordion>
|
||||
|
||||
{/* <Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Delete Backstory History</Button>
|
||||
<Button onClick={() => { reset(["rags", "tools", "system_prompt", "message_history_length"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button> */}
|
||||
<Button onClick={() => { reset(["rags", "tools", "system_prompt"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button> */}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, useEffect, useState } from 'react';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
@ -9,45 +9,85 @@ import { Conversation, ConversationHandle } from '../Components/Conversation';
|
||||
import { ChatQuery } from '../Components/ChatQuery';
|
||||
import { MessageList } from '../Components/Message';
|
||||
|
||||
import { connectionBase } from '../Global';
|
||||
|
||||
type UserData = {
|
||||
user_name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
full_name: string;
|
||||
contact_info: Record<string, string>;
|
||||
questions: string[];
|
||||
};
|
||||
|
||||
const HomePage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||
const { sessionId, setSnack, submitQuery } = props;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [preamble, setPreamble] = useState<MessageList>([]);
|
||||
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
||||
const [user, setUser] = useState<UserData | undefined>(undefined)
|
||||
|
||||
if (sessionId === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (user === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backstoryPreamble: MessageList = [
|
||||
{
|
||||
setPreamble([{
|
||||
role: 'content',
|
||||
title: 'Welcome to Backstory',
|
||||
disableCopy: true,
|
||||
content: `
|
||||
Backstory is a RAG enabled expert system with access to real-time data running self-hosted
|
||||
(no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs).
|
||||
It was written by James Ketrenos in order to provide answers to
|
||||
questions potential employers may have about his work history.
|
||||
|
||||
What would you like to know about James?
|
||||
This instances has been launched for ${user.full_name}.
|
||||
|
||||
What would you like to know about ${user.first_name}?
|
||||
`,
|
||||
}
|
||||
];
|
||||
}]);
|
||||
|
||||
const backstoryQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
<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} />
|
||||
setQuestions([
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
{user.questions.map((q: string, i: number) =>
|
||||
<ChatQuery key={i} query={{ prompt: q, tunables: { enable_tools: false } }} submitQuery={submitQuery} />
|
||||
)}
|
||||
</Box>,
|
||||
<Box sx={{ p: 1 }}>
|
||||
<MuiMarkdown>
|
||||
As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career,
|
||||
I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**.
|
||||
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${user.full_name}** if you have any questions.`}
|
||||
</MuiMarkdown>
|
||||
</Box>
|
||||
];
|
||||
</Box>]);
|
||||
}, [user, isMobile, submitQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const response = await fetch(connectionBase + `/api/user/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setUser(data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error getting user info:', error);
|
||||
setSnack("Unable to obtain user information.", "error");
|
||||
}
|
||||
};
|
||||
fetchUserData();
|
||||
}, [setSnack, sessionId]);
|
||||
|
||||
if (sessionId === undefined || user === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <Conversation
|
||||
sx={{
|
||||
@ -62,8 +102,8 @@ What would you like to know about James?
|
||||
resetLabel: "chat",
|
||||
sessionId,
|
||||
setSnack,
|
||||
preamble: backstoryPreamble,
|
||||
defaultPrompts: backstoryQuestions,
|
||||
preamble: preamble,
|
||||
defaultPrompts: questions,
|
||||
submitQuery,
|
||||
}}
|
||||
/>;
|
||||
|
272
src/server.py
272
src/server.py
@ -21,10 +21,8 @@ import subprocess
|
||||
import re
|
||||
import math
|
||||
import warnings
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
# from typing import Any
|
||||
import inspect
|
||||
from uuid import uuid4
|
||||
import time
|
||||
import traceback
|
||||
|
||||
@ -63,7 +61,7 @@ from prometheus_client import CollectorRegistry, Counter # type: ignore
|
||||
|
||||
from utils import (
|
||||
rag as Rag,
|
||||
ChromaDBGetResponse,
|
||||
RagEntry,
|
||||
tools as Tools,
|
||||
Context,
|
||||
Conversation,
|
||||
@ -72,26 +70,15 @@ from utils import (
|
||||
Metrics,
|
||||
Tunables,
|
||||
defines,
|
||||
User,
|
||||
check_serializable,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
rags : List[ChromaDBGetResponse] = [
|
||||
ChromaDBGetResponse(
|
||||
name="JPK",
|
||||
enabled=True,
|
||||
description="Expert data about James Ketrenos, including work history, personal hobbies, and projects.",
|
||||
),
|
||||
# { "name": "LKML", "enabled": False, "description": "Full associative data for entire LKML mailing list archive." },
|
||||
]
|
||||
|
||||
|
||||
class Query(BaseModel):
|
||||
prompt: str
|
||||
tunables: Tunables = Field(default_factory=Tunables)
|
||||
agent_options: Dict[str, Any] = Field(default={})
|
||||
|
||||
agent_options: Dict[str, str | int | float | Dict] = Field(default={}, exclude=True)
|
||||
|
||||
REQUEST_TIME = Summary("request_processing_seconds", "Time spent processing request")
|
||||
|
||||
@ -182,12 +169,6 @@ WEB_HOST = "0.0.0.0"
|
||||
WEB_PORT = 8911
|
||||
DEFAULT_HISTORY_LENGTH = 5
|
||||
|
||||
# %%
|
||||
# Globals
|
||||
|
||||
model = None
|
||||
web_server = None
|
||||
|
||||
|
||||
# %%
|
||||
# Cmd line overrides
|
||||
@ -246,19 +227,11 @@ def is_valid_uuid(value: str) -> bool:
|
||||
class WebServer:
|
||||
@asynccontextmanager
|
||||
async def lifespan(self, app: FastAPI):
|
||||
# Start the file watcher
|
||||
self.observer, self.file_watcher = Rag.start_file_watcher(
|
||||
llm=self.llm,
|
||||
watch_directory=defines.doc_dir,
|
||||
recreate=False, # Don't recreate if exists
|
||||
)
|
||||
logger.info(
|
||||
f"API started with {self.file_watcher.collection.count()} documents in the collection"
|
||||
)
|
||||
yield
|
||||
if self.observer:
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
for user in self.users:
|
||||
if user.observer:
|
||||
user.observer.stop()
|
||||
user.observer.join()
|
||||
logger.info("File watcher stopped")
|
||||
|
||||
def __init__(self, llm, model=MODEL_NAME):
|
||||
@ -276,13 +249,12 @@ class WebServer:
|
||||
# Expose the /metrics endpoint
|
||||
self.instrumentator.expose(self.app, endpoint="/metrics")
|
||||
|
||||
self.contexts = {}
|
||||
|
||||
self.llm = llm
|
||||
self.model = model
|
||||
self.processing = False
|
||||
self.file_watcher = None
|
||||
self.observer = None
|
||||
|
||||
self.users = []
|
||||
self.contexts = {}
|
||||
|
||||
self.ssl_enabled = os.path.exists(defines.key_path) and os.path.exists(
|
||||
defines.cert_path
|
||||
@ -308,7 +280,7 @@ class WebServer:
|
||||
def setup_routes(self):
|
||||
@self.app.get("/")
|
||||
async def root():
|
||||
context = self.create_context()
|
||||
context = self.create_context(username=defines.default_username)
|
||||
logger.info(f"Redirecting non-context to {context.id}")
|
||||
return RedirectResponse(url=f"/{context.id}", status_code=307)
|
||||
# return JSONResponse({"redirect": f"/{context.id}"})
|
||||
@ -317,15 +289,14 @@ class WebServer:
|
||||
async def get_umap(doc_id: str, context_id: str, request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
try:
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
|
||||
context = self.upsert_context(context_id)
|
||||
if not context:
|
||||
return JSONResponse(
|
||||
{"error": f"Invalid context: {context_id}"}, status_code=400
|
||||
)
|
||||
collection = self.file_watcher.umap_collection
|
||||
|
||||
user = context.user
|
||||
collection = user.umap_collection
|
||||
if not collection:
|
||||
return JSONResponse(
|
||||
{"error": "No UMAP collection found"}, status_code=404
|
||||
@ -336,7 +307,7 @@ class WebServer:
|
||||
for index, id in enumerate(collection.get("ids", [])):
|
||||
if id == doc_id:
|
||||
metadata = collection.get("metadatas", [])[index].copy()
|
||||
content = self.file_watcher.prepare_metadata(metadata)
|
||||
content = user.file_watcher.prepare_metadata(metadata)
|
||||
return JSONResponse(content)
|
||||
|
||||
return JSONResponse(f"Document id {doc_id} not found.", 404)
|
||||
@ -349,29 +320,25 @@ class WebServer:
|
||||
async def put_umap(context_id: str, request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
try:
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
|
||||
context = self.upsert_context(context_id)
|
||||
if not context:
|
||||
return JSONResponse(
|
||||
{"error": f"Invalid context: {context_id}"}, status_code=400
|
||||
)
|
||||
|
||||
user = context.user
|
||||
data = await request.json()
|
||||
|
||||
dimensions = data.get("dimensions", 2)
|
||||
collection = self.file_watcher.umap_collection
|
||||
collection = user.file_watcher.umap_collection
|
||||
if not collection:
|
||||
return JSONResponse(
|
||||
{"error": "No UMAP collection found"}, status_code=404
|
||||
)
|
||||
if dimensions == 2:
|
||||
logger.info("Returning 2D UMAP")
|
||||
umap_embedding = self.file_watcher.umap_embedding_2d
|
||||
umap_embedding = user.file_watcher.umap_embedding_2d
|
||||
else:
|
||||
logger.info("Returning 3D UMAP")
|
||||
umap_embedding = self.file_watcher.umap_embedding_3d
|
||||
umap_embedding = user.file_watcher.umap_embedding_3d
|
||||
|
||||
if len(umap_embedding) == 0:
|
||||
return JSONResponse(
|
||||
@ -382,26 +349,21 @@ class WebServer:
|
||||
"metadatas": collection.get("metadatas", []),
|
||||
"documents": collection.get("documents", []),
|
||||
"embeddings": umap_embedding.tolist(),
|
||||
"size": self.file_watcher.collection.count()
|
||||
"size": user.file_watcher.collection.count()
|
||||
}
|
||||
|
||||
return JSONResponse(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"put_umap error: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"put_umap error: {str(e)}")
|
||||
return JSONResponse({"error": str(e)}, 500)
|
||||
|
||||
@self.app.put("/api/similarity/{context_id}")
|
||||
async def put_similarity(context_id: str, request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
|
||||
if not is_valid_uuid(context_id):
|
||||
logger.warning(f"Invalid context_id: {context_id}")
|
||||
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
|
||||
|
||||
context = self.upsert_context(context_id)
|
||||
user = context.user
|
||||
try:
|
||||
data = await request.json()
|
||||
query = data.get("query", "")
|
||||
@ -417,7 +379,7 @@ class WebServer:
|
||||
status_code=400,
|
||||
)
|
||||
try:
|
||||
chroma_results = self.file_watcher.find_similar(
|
||||
chroma_results = user.file_watcher.find_similar(
|
||||
query=query, top_k=results, threshold=threshold
|
||||
)
|
||||
if not chroma_results:
|
||||
@ -428,14 +390,14 @@ class WebServer:
|
||||
).flatten() # Ensure correct shape
|
||||
logger.info(f"Chroma embedding shape: {chroma_embedding.shape}")
|
||||
|
||||
umap_2d = self.file_watcher.umap_model_2d.transform([chroma_embedding])[
|
||||
umap_2d = user.file_watcher.umap_model_2d.transform([chroma_embedding])[
|
||||
0
|
||||
].tolist()
|
||||
logger.info(
|
||||
f"UMAP 2D output: {umap_2d}, length: {len(umap_2d)}"
|
||||
) # Debug output
|
||||
|
||||
umap_3d = self.file_watcher.umap_model_3d.transform([chroma_embedding])[
|
||||
umap_3d = user.file_watcher.umap_model_3d.transform([chroma_embedding])[
|
||||
0
|
||||
].tolist()
|
||||
logger.info(
|
||||
@ -449,7 +411,7 @@ class WebServer:
|
||||
"query": query,
|
||||
"umap_embedding_2d": umap_2d,
|
||||
"umap_embedding_3d": umap_3d,
|
||||
"size": self.file_watcher.collection.count()
|
||||
"size": user.file_watcher.collection.count()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@ -478,7 +440,7 @@ class WebServer:
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
case "rags":
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
context.rags = [ r.model_copy() for r in rags]
|
||||
context.rags = [ r.model_copy() for r in context.user.rags]
|
||||
response["rags"] = [ r.model_dump(mode="json") for r in context.rags ]
|
||||
case "tools":
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
@ -511,10 +473,6 @@ class WebServer:
|
||||
tmp.conversation.reset()
|
||||
response["history"] = []
|
||||
response["context_used"] = agent.context_tokens
|
||||
case "message_history_length":
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
context.message_history_length = DEFAULT_HISTORY_LENGTH
|
||||
response["message_history_length"] = DEFAULT_HISTORY_LENGTH
|
||||
|
||||
if not response:
|
||||
return JSONResponse(
|
||||
@ -548,6 +506,7 @@ class WebServer:
|
||||
for k in data.keys():
|
||||
match k:
|
||||
case "tools":
|
||||
from typing import Any
|
||||
# { "tools": [{ "tool": tool?.name, "enabled": tool.enabled }] }
|
||||
tools: list[dict[str, Any]] = data[k]
|
||||
if not tools:
|
||||
@ -575,6 +534,7 @@ class WebServer:
|
||||
)
|
||||
|
||||
case "rags":
|
||||
from typing import Any
|
||||
# { "rags": [{ "tool": tool?.name, "enabled": tool.enabled }] }
|
||||
rag_configs: list[dict[str, Any]] = data[k]
|
||||
if not rag_configs:
|
||||
@ -603,11 +563,6 @@ class WebServer:
|
||||
agent.system_prompt = system_prompt
|
||||
self.save_context(context_id)
|
||||
return JSONResponse({"system_prompt": system_prompt})
|
||||
case "message_history_length":
|
||||
value = max(0, int(data[k]))
|
||||
context.message_history_length = value
|
||||
self.save_context(context_id)
|
||||
return JSONResponse({"message_history_length": value})
|
||||
case _:
|
||||
return JSONResponse(
|
||||
{"error": f"Unrecognized tunable {k}"}, status_code=404
|
||||
@ -616,6 +571,20 @@ class WebServer:
|
||||
logger.error(f"Error in put_tunables: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@self.app.get("/api/user/{context_id}")
|
||||
async def get_user(context_id: str, request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
user = self.upsert_context(context_id).user
|
||||
user_data = {
|
||||
"username": user.username,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"full_name": user.full_name,
|
||||
"contact_info": user.contact_info,
|
||||
"questions": user.user_questions,
|
||||
}
|
||||
return JSONResponse(user_data)
|
||||
|
||||
@self.app.get("/api/tunables/{context_id}")
|
||||
async def get_tunables(context_id: str, request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
@ -630,7 +599,6 @@ class WebServer:
|
||||
return JSONResponse(
|
||||
{
|
||||
"system_prompt": agent.system_prompt,
|
||||
"message_history_length": context.message_history_length,
|
||||
"rags": [ r.model_dump(mode="json") for r in context.rags ],
|
||||
"tools": [
|
||||
{
|
||||
@ -753,8 +721,8 @@ class WebServer:
|
||||
await asyncio.sleep(0)
|
||||
except Exception as e:
|
||||
context.processing = False
|
||||
logger.error(f"Error in generate_response: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"Error in generate_response: {e}")
|
||||
yield json.dumps({"status": "error", "response": str(e)}) + "\n"
|
||||
finally:
|
||||
# Save context on completion or error
|
||||
@ -775,16 +743,25 @@ class WebServer:
|
||||
logger.error(f"Error in post_chat_endpoint: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@self.app.post("/api/context")
|
||||
async def create_context():
|
||||
@self.app.post("/api/context/u/{username}")
|
||||
async def create_user_context(username: str, request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
try:
|
||||
context = self.create_context()
|
||||
logger.info(f"Generated new agent as {context.id}")
|
||||
if not User.exists(username):
|
||||
return JSONResponse({"error": f"User {username} not found."}, status_code=404)
|
||||
|
||||
context = self.create_context(username=username)
|
||||
logger.info(f"Generated new context {context.id} for {username}")
|
||||
return JSONResponse({"id": context.id})
|
||||
except Exception as e:
|
||||
logger.error(f"get_history error: {str(e)}")
|
||||
logger.error(f"create_user_context error: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return JSONResponse({"error": str(e)}, status_code=404)
|
||||
return JSONResponse({"error": f"User {username} not found."}, status_code=404)
|
||||
|
||||
@self.app.post("/api/context")
|
||||
async def create_context(request: Request):
|
||||
logger.info(f"{request.method} {request.url.path}")
|
||||
return self.app.create_user_context(defines.default_username, request)
|
||||
|
||||
@self.app.get("/api/history/{context_id}/{agent_type}")
|
||||
async def get_history(context_id: str, agent_type: str, request: Request):
|
||||
@ -802,10 +779,8 @@ class WebServer:
|
||||
)
|
||||
return agent.conversation
|
||||
except Exception as e:
|
||||
logger.error(f"get_history error: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"get_history error: {str(e)}")
|
||||
return JSONResponse({"error": str(e)}, status_code=404)
|
||||
|
||||
@self.app.get("/api/tools/{context_id}")
|
||||
@ -922,7 +897,7 @@ class WebServer:
|
||||
|
||||
return context_id
|
||||
|
||||
def load_or_create_context(self, context_id) -> Context:
|
||||
def load_or_create_context(self, context_id: str) -> Context:
|
||||
"""
|
||||
Load a context from a file in the context directory or create a new one if it doesn't exist.
|
||||
Args:
|
||||
@ -930,15 +905,12 @@ class WebServer:
|
||||
Returns:
|
||||
A Context object with the specified ID and default settings.
|
||||
"""
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
|
||||
file_path = os.path.join(defines.context_dir, context_id)
|
||||
|
||||
# Check if the file exists
|
||||
if not os.path.exists(file_path):
|
||||
logger.info(f"Context file {file_path} not found. Creating new context.")
|
||||
self.contexts[context_id] = self.create_context(context_id)
|
||||
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
|
||||
else:
|
||||
# Read and deserialize the data
|
||||
with open(file_path, "r") as f:
|
||||
@ -946,19 +918,25 @@ class WebServer:
|
||||
logger.info(
|
||||
f"Loading context from {file_path}, content length: {len(content)}"
|
||||
)
|
||||
import json
|
||||
|
||||
json_data = {}
|
||||
try:
|
||||
# Try parsing as JSON first to ensure valid JSON
|
||||
json_data = json.loads(content)
|
||||
logger.info("JSON parsed successfully, attempting model validation")
|
||||
|
||||
# Validate from JSON (no prometheus_collector or file_watcher)
|
||||
|
||||
context = Context.model_validate(json_data)
|
||||
|
||||
# Set excluded fields
|
||||
context.file_watcher = self.file_watcher
|
||||
context.prometheus_collector = self.prometheus_collector
|
||||
username = context.username
|
||||
if not User.exists(username):
|
||||
raise ValueError(f"Attempt to load context {context.id} with invalid user {username}")
|
||||
|
||||
matching_user = next((user for user in self.users if user.username == username), None)
|
||||
if matching_user:
|
||||
user = matching_user
|
||||
else:
|
||||
user = User(username=username, llm=self.llm)
|
||||
user.initialize(prometheus_collector=self.prometheus_collector)
|
||||
self.users.append(user)
|
||||
context.user = user
|
||||
|
||||
# Now set context on agents manually
|
||||
agent_types = [agent.agent_type for agent in context.agents]
|
||||
@ -972,19 +950,33 @@ class WebServer:
|
||||
self.contexts[context_id] = context
|
||||
|
||||
logger.info(f"Successfully loaded context {context_id}")
|
||||
except ValidationError as e:
|
||||
logger.error(e)
|
||||
logger.error(traceback.format_exc())
|
||||
for error in e.errors():
|
||||
print(f"Field: {error['loc'][0]}, Error: {error['msg']}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating context: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
for key in json_data:
|
||||
logger.info(f"{key} = {type(json_data[key])} {str(json_data[key])[:60] if json_data[key] else "None"}")
|
||||
logger.info("*" * 50)
|
||||
if len(self.users) == 0:
|
||||
user = User(username=defines.default_username, llm=self.llm)
|
||||
user.initialize(prometheus_collector=self.prometheus_collector)
|
||||
self.users.append(user)
|
||||
# Fallback to creating a new context
|
||||
user = self.users[0]
|
||||
self.contexts[context_id] = Context(
|
||||
id=context_id,
|
||||
file_watcher=self.file_watcher,
|
||||
prometheus_collector=self.prometheus_collector,
|
||||
user=user,
|
||||
rags=[ rag.model_copy() for rag in user.rags ],
|
||||
tools=Tools.enabled_tools(Tools.tools)
|
||||
)
|
||||
|
||||
return self.contexts[context_id]
|
||||
|
||||
def create_context(self, context_id=None) -> Context:
|
||||
def create_context(self, username: str, context_id=None) -> Context:
|
||||
"""
|
||||
Create a new context with a unique ID and default settings.
|
||||
Args:
|
||||
@ -992,16 +984,43 @@ class WebServer:
|
||||
Returns:
|
||||
A Context object with the specified ID and default settings.
|
||||
"""
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
if not context_id:
|
||||
context_id = str(uuid4())
|
||||
logger.info(f"Creating new context with ID: {context_id}")
|
||||
context = Context(
|
||||
id=context_id,
|
||||
file_watcher=self.file_watcher,
|
||||
prometheus_collector=self.prometheus_collector,
|
||||
)
|
||||
if not User.exists(username):
|
||||
raise ValueError(f"{username} does not exist.")
|
||||
|
||||
# If username
|
||||
matching_user = next((user for user in self.users if user.username == username), None)
|
||||
if matching_user:
|
||||
user = matching_user
|
||||
logger.info("Found matching user", user.model_dump(mode="json"))
|
||||
else:
|
||||
user = User(username=username, llm=self.llm)
|
||||
user.initialize(prometheus_collector=self.prometheus_collector)
|
||||
logger.info("Created new instance of user", user.model_dump(mode="json"))
|
||||
self.users.append(user)
|
||||
|
||||
logger.info(f"Creating context {context_id} with user", user.model_dump(mode='json'))
|
||||
try:
|
||||
if context_id:
|
||||
context = Context(
|
||||
id=context_id,
|
||||
user=user,
|
||||
rags=[ rag.model_copy() for rag in user.rags ],
|
||||
tools=Tools.enabled_tools(Tools.tools)
|
||||
)
|
||||
else:
|
||||
context = Context(
|
||||
user=user,
|
||||
rags=[ rag.model_copy() for rag in user.rags ],
|
||||
tools=Tools.enabled_tools(Tools.tools)
|
||||
)
|
||||
except ValidationError as e:
|
||||
logger.error(e)
|
||||
logger.error(traceback.format_exc())
|
||||
for error in e.errors():
|
||||
print(f"Field: {error['loc'][0]}, Error: {error['msg']}")
|
||||
exit(1)
|
||||
|
||||
logger.info(f"New context created with ID: {context.id}")
|
||||
|
||||
if os.path.exists(defines.resume_doc):
|
||||
context.user_resume = open(defines.resume_doc, "r").read()
|
||||
@ -1010,8 +1029,6 @@ class WebServer:
|
||||
# context.add_agent(Resume(system_prompt = system_generate_resume))
|
||||
# context.add_agent(JobDescription(system_prompt = system_job_description))
|
||||
# context.add_agent(FactCheck(system_prompt = system_fact_check))
|
||||
context.tools = Tools.enabled_tools(Tools.tools)
|
||||
context.rags = [ r.model_copy() for r in rags ]
|
||||
|
||||
logger.info(f"{context.id} created and added to contexts.")
|
||||
self.contexts[context.id] = context
|
||||
@ -1029,21 +1046,18 @@ class WebServer:
|
||||
|
||||
if not context_id:
|
||||
logger.warning("No context ID provided. Creating a new context.")
|
||||
return self.create_context()
|
||||
return self.create_context(username=defines.default_username)
|
||||
|
||||
if context_id in self.contexts:
|
||||
return self.contexts[context_id]
|
||||
|
||||
logger.info(f"Context {context_id} is not yet loaded.")
|
||||
return self.load_or_create_context(context_id)
|
||||
return self.load_or_create_context(context_id=context_id)
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
async def generate_response(
|
||||
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}")
|
||||
# Merge tunables to take agent defaults and override with user supplied settings
|
||||
@ -1090,10 +1104,12 @@ class WebServer:
|
||||
logger.info(f"Starting web server at http://{host}:{port}")
|
||||
uvicorn.run(self.app, host=host, port=port, log_config=None)
|
||||
except KeyboardInterrupt:
|
||||
if self.observer:
|
||||
self.observer.stop()
|
||||
if self.observer:
|
||||
self.observer.join()
|
||||
for user in self.users:
|
||||
if user.observer:
|
||||
user.observer.stop()
|
||||
for user in self.users:
|
||||
if user.observer:
|
||||
user.observer.join()
|
||||
|
||||
|
||||
# %%
|
||||
@ -1101,8 +1117,6 @@ class WebServer:
|
||||
|
||||
# Main function to run everything
|
||||
def main():
|
||||
global model
|
||||
|
||||
# Parse command-line arguments
|
||||
args = parse_args()
|
||||
|
||||
@ -1114,11 +1128,7 @@ def main():
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="umap.*")
|
||||
|
||||
llm = ollama.Client(host=args.ollama_server) # type: ignore
|
||||
model = args.ollama_model
|
||||
|
||||
web_server = WebServer(llm, model)
|
||||
|
||||
web_server = WebServer(llm, args.ollama_model)
|
||||
web_server.run(host=args.web_host, port=args.web_port, use_reloader=False)
|
||||
|
||||
|
||||
main()
|
||||
|
@ -1,22 +1,7 @@
|
||||
# From /opt/backstory run:
|
||||
# python -m src.tests.test-context
|
||||
import os
|
||||
from ..utils import Context
|
||||
|
||||
os.environ["TORCH_CPP_LOG_LEVEL"] = "ERROR"
|
||||
import warnings
|
||||
|
||||
import ollama
|
||||
|
||||
from ..utils import rag as Rag, Context, defines
|
||||
|
||||
import json
|
||||
|
||||
llm = ollama.Client(host=defines.ollama_api_url) # type: ignore
|
||||
|
||||
observer, file_watcher = Rag.start_file_watcher(
|
||||
llm=llm, watch_directory=defines.doc_dir, recreate=False # Don't recreate if exists
|
||||
)
|
||||
|
||||
context = Context(file_watcher=file_watcher)
|
||||
context = Context()
|
||||
json_data = context.model_dump(mode="json")
|
||||
context = Context.model_validate(json_data)
|
||||
|
@ -8,10 +8,11 @@ import importlib
|
||||
import json
|
||||
|
||||
from . import defines
|
||||
from .message import Message, Tunables, MessageMetaData
|
||||
from .user import User
|
||||
from .context import Context
|
||||
from .conversation import Conversation
|
||||
from .message import Message, Tunables, MessageMetaData
|
||||
from .rag import ChromaDBFileWatcher, ChromaDBGetResponse, start_file_watcher
|
||||
from .rag import ChromaDBFileWatcher, ChromaDBGetResponse, start_file_watcher, RagEntry
|
||||
from .setup_logging import setup_logging
|
||||
from .agents import class_registry, AnyAgent, Agent, __all__ as agents_all
|
||||
from .metrics import Metrics
|
||||
@ -25,11 +26,13 @@ __all__ = [
|
||||
"Context",
|
||||
"Conversation",
|
||||
"Metrics",
|
||||
"RagEntry",
|
||||
"ChromaDBFileWatcher",
|
||||
'ChromaDBGetResponse',
|
||||
"start_file_watcher",
|
||||
"check_serializable",
|
||||
"logger",
|
||||
"User",
|
||||
]
|
||||
|
||||
__all__.extend(agents_all) # type: ignore
|
||||
|
@ -157,7 +157,7 @@ class Agent(BaseModel, ABC):
|
||||
if message.tunables.enable_rag and message.prompt:
|
||||
# Gather RAG results, yielding each result
|
||||
# as it becomes available
|
||||
for message in self.context.generate_rag_results(message):
|
||||
for message in self.context.user.generate_rag_results(message):
|
||||
logger.info(f"RAG: {message.status} - {message.response}")
|
||||
if message.status == "error":
|
||||
yield message
|
||||
@ -172,7 +172,7 @@ class Agent(BaseModel, ABC):
|
||||
message.preamble = {}
|
||||
|
||||
if rag_context:
|
||||
message.preamble["context"] = rag_context
|
||||
message.preamble["context"] = f"The following is context information about {self.context.user.full_name}:\n{rag_context}"
|
||||
|
||||
if message.tunables.enable_context and self.context.user_resume:
|
||||
message.preamble["resume"] = self.context.user_resume
|
||||
|
@ -212,8 +212,7 @@ class JobDescription(Agent):
|
||||
|
||||
def generate_resume_from_skill_assessments(
|
||||
self,
|
||||
candidate_name,
|
||||
candidate_contact_info,
|
||||
candidate_info,
|
||||
skill_assessment_results,
|
||||
original_resume,
|
||||
):
|
||||
@ -273,19 +272,18 @@ class JobDescription(Agent):
|
||||
|
||||
# Format contact info
|
||||
contact_info_str = ""
|
||||
if candidate_contact_info:
|
||||
contact_items = []
|
||||
for key, value in candidate_contact_info.items():
|
||||
if value:
|
||||
contact_items.append(f"{key}: {value}")
|
||||
contact_info_str = "\n".join(contact_items)
|
||||
contact_items = []
|
||||
for key, value in candidate_info.get("contact_info", {}).items():
|
||||
if value:
|
||||
contact_items.append(f"{key}: {value}")
|
||||
contact_info_str = "\n".join(contact_items)
|
||||
|
||||
# Build the system prompt
|
||||
system_prompt = f"""You are a professional resume writer with expertise in highlighting candidate strengths and experiences.
|
||||
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided.
|
||||
|
||||
## CANDIDATE INFORMATION:
|
||||
Name: {candidate_name}
|
||||
Name: {candidate_info.full_name}
|
||||
{contact_info_str}
|
||||
|
||||
## SKILL ASSESSMENT RESULTS:
|
||||
@ -381,14 +379,11 @@ Provide the resume in clean markdown format, ready for the candidate to use.
|
||||
"""
|
||||
|
||||
# Extract candidate information
|
||||
candidate_name = candidate_info.get("name", "")
|
||||
candidate_contact = candidate_info.get("contact_info", {})
|
||||
original_resume = candidate_info.get("original_resume", "")
|
||||
|
||||
# Generate resume prompt
|
||||
system_prompt, prompt = self.generate_resume_from_skill_assessments(
|
||||
candidate_name,
|
||||
candidate_contact,
|
||||
candidate_info,
|
||||
skill_assessment_results.values(),
|
||||
original_resume,
|
||||
)
|
||||
@ -874,18 +869,18 @@ IMPORTANT: Be factual and precise. If you cannot find strong evidence for this s
|
||||
yield message
|
||||
|
||||
def retrieve_rag_content(self, skill: str) -> tuple[str, ChromaDBGetResponse]:
|
||||
if self.context is None or self.context.file_watcher is None:
|
||||
raise ValueError("self.context or self.context.file_watcher is None")
|
||||
if self.context is None or self.context.user is None or self.context.user.file_watcher is None:
|
||||
raise ValueError("self.context or self.context.user.file_watcher is None")
|
||||
|
||||
try:
|
||||
rag_results = ""
|
||||
rag_metadata = ChromaDBGetResponse()
|
||||
chroma_results = self.context.file_watcher.find_similar(query=skill, top_k=10, threshold=0.5)
|
||||
chroma_results = self.context.user.file_watcher.find_similar(query=skill, top_k=10, threshold=0.5)
|
||||
if chroma_results:
|
||||
query_embedding = np.array(chroma_results["query_embedding"]).flatten()
|
||||
|
||||
umap_2d = self.context.file_watcher.umap_model_2d.transform([query_embedding])[0]
|
||||
umap_3d = self.context.file_watcher.umap_model_3d.transform([query_embedding])[0]
|
||||
umap_2d = self.context.user.file_watcher.umap_model_2d.transform([query_embedding])[0]
|
||||
umap_3d = self.context.user.file_watcher.umap_model_3d.transform([query_embedding])[0]
|
||||
|
||||
rag_metadata = ChromaDBGetResponse(
|
||||
query=skill,
|
||||
@ -897,7 +892,7 @@ IMPORTANT: Be factual and precise. If you cannot find strong evidence for this s
|
||||
metadatas=chroma_results.get("metadatas", []),
|
||||
umap_embedding_2d=umap_2d.tolist(),
|
||||
umap_embedding_3d=umap_3d.tolist(),
|
||||
size=self.context.file_watcher.collection.count()
|
||||
size=self.context.user.file_watcher.collection.count()
|
||||
)
|
||||
|
||||
for index, metadata in enumerate(chroma_results["metadatas"]):
|
||||
@ -1057,15 +1052,9 @@ Content: { content }
|
||||
"result": skill_assessment_results[skill_name],
|
||||
}
|
||||
|
||||
# Extract header from original resume
|
||||
candidate_info = {
|
||||
"name": "James Ketrenos",
|
||||
"contact_info": {
|
||||
"email": "james@ketrenos.com",
|
||||
"phone": "(503) 501 8281",
|
||||
"location": "Beaverton, OR 97003",
|
||||
},
|
||||
"original_resume": resume,
|
||||
"full_name": self.context.user.full_name,
|
||||
"contact_info": self.context.user.contact_info
|
||||
}
|
||||
|
||||
# Stage 2: Generate tailored resume
|
||||
|
@ -1,48 +1,85 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, Field, model_validator # type: ignore
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Generator, ClassVar, Any
|
||||
from typing import List, Optional, Generator, ClassVar, Any, TYPE_CHECKING
|
||||
from typing_extensions import Annotated, Union
|
||||
import numpy as np # type: ignore
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from prometheus_client import CollectorRegistry, Counter # type: ignore
|
||||
import traceback
|
||||
|
||||
from .message import Message, Tunables
|
||||
from .rag import ChromaDBFileWatcher, ChromaDBGetResponse
|
||||
from . import defines
|
||||
from . rag import RagEntry
|
||||
from . import tools as Tools
|
||||
from .agents import AnyAgent
|
||||
from . import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .user import User
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Context(BaseModel):
|
||||
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher
|
||||
# Required fields
|
||||
file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
|
||||
prometheus_collector: Optional[CollectorRegistry] = Field(
|
||||
default=None, exclude=True
|
||||
)
|
||||
class Config:
|
||||
validate_by_name = True # Allow 'user' to be set via constructor
|
||||
arbitrary_types_allowed = True # Allow ChromaDBFileWatcher
|
||||
|
||||
# Optional fields
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid4()),
|
||||
pattern=r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||
)
|
||||
tools: List[dict]
|
||||
rags: List[RagEntry]
|
||||
username: str = "__invalid__"
|
||||
|
||||
# "user" is not serialized and must be set after construction
|
||||
Context__user: User = Field(
|
||||
default_factory=lambda: User(username="__invalid__", llm=None, rags=[]),
|
||||
alias="user",
|
||||
exclude=True)
|
||||
|
||||
# Optional fields
|
||||
user_resume: Optional[str] = None
|
||||
user_job_description: Optional[str] = None
|
||||
user_facts: Optional[str] = None
|
||||
tools: List[dict] = Tools.enabled_tools(Tools.tools)
|
||||
rags: List[ChromaDBGetResponse] = []
|
||||
message_history_length: int = 5
|
||||
|
||||
# Class managed fields
|
||||
agents: List[Annotated[Union[*Agent.__subclasses__()], Field(discriminator="agent_type")]] = Field( # type: ignore
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def set_user_and_username(self):
|
||||
if self.Context__user.username != "__invalid__":
|
||||
if self.username == "__invalid__":
|
||||
logger.info(f"Binding context {self.id} to user {self.Context__user.username}")
|
||||
self.username = self.Context__user.username
|
||||
else:
|
||||
raise ValueError("user can only be set once")
|
||||
return self
|
||||
|
||||
# Only allow dereference of 'user' if it has been set
|
||||
@property
|
||||
def user(self) -> User:
|
||||
if self.Context__user.username == "__invalid__":
|
||||
# After deserializing Context(), you must explicitly set the
|
||||
# user:
|
||||
#
|
||||
# context = Context(...)
|
||||
# context.user = <valid user>
|
||||
raise ValueError("Attempt to dereference default_factory constructed User")
|
||||
return self.Context__user
|
||||
|
||||
# Only allow setting of 'user' once
|
||||
@user.setter
|
||||
def user(self, new_user: User) -> User:
|
||||
if self.Context__user.username != "__invalid__":
|
||||
raise ValueError("user can only be set once")
|
||||
logger.info(f"Binding context {self.id} to user {new_user.username}")
|
||||
self.username = new_user.username
|
||||
self.Context__user = new_user
|
||||
return new_user
|
||||
|
||||
processing: bool = Field(default=False, exclude=True)
|
||||
|
||||
# @model_validator(mode="before")
|
||||
@ -64,74 +101,6 @@ class Context(BaseModel):
|
||||
# agent.set_context(self)
|
||||
return self
|
||||
|
||||
def generate_rag_results(
|
||||
self, message: Message, top_k=defines.default_rag_top_k, threshold=defines.default_rag_threshold
|
||||
) -> Generator[Message, None, None]:
|
||||
"""
|
||||
Generate RAG results for the given query.
|
||||
|
||||
Args:
|
||||
query: The query string to generate RAG results for.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries containing the RAG results.
|
||||
"""
|
||||
try:
|
||||
message.status = "processing"
|
||||
|
||||
entries: int = 0
|
||||
|
||||
if not self.file_watcher:
|
||||
message.response = "No RAG context available."
|
||||
message.status = "done"
|
||||
yield message
|
||||
return
|
||||
|
||||
for rag in self.rags:
|
||||
if not rag.enabled:
|
||||
continue
|
||||
message.response = f"Checking RAG context {rag.name}..."
|
||||
yield message
|
||||
chroma_results = self.file_watcher.find_similar(
|
||||
query=message.prompt, top_k=top_k, threshold=threshold
|
||||
)
|
||||
if chroma_results:
|
||||
query_embedding = np.array(chroma_results["query_embedding"]).flatten()
|
||||
|
||||
umap_2d = self.file_watcher.umap_model_2d.transform([query_embedding])[0]
|
||||
umap_3d = self.file_watcher.umap_model_3d.transform([query_embedding])[0]
|
||||
|
||||
rag_metadata = ChromaDBGetResponse(
|
||||
query=message.prompt,
|
||||
query_embedding=query_embedding.tolist(),
|
||||
name=rag.name,
|
||||
ids=chroma_results.get("ids", []),
|
||||
embeddings=chroma_results.get("embeddings", []),
|
||||
documents=chroma_results.get("documents", []),
|
||||
metadatas=chroma_results.get("metadatas", []),
|
||||
umap_embedding_2d=umap_2d.tolist(),
|
||||
umap_embedding_3d=umap_3d.tolist(),
|
||||
size=self.file_watcher.collection.count()
|
||||
)
|
||||
|
||||
message.metadata.rag.append(rag_metadata)
|
||||
message.response = f"Results from {rag.name} RAG: {len(chroma_results['documents'])} results."
|
||||
yield message
|
||||
|
||||
message.response = (
|
||||
f"RAG context gathered from results from {entries} documents."
|
||||
)
|
||||
message.status = "done"
|
||||
yield message
|
||||
return
|
||||
except Exception as e:
|
||||
message.status = "error"
|
||||
message.response = f"Error generating RAG results: {str(e)}"
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(message.response)
|
||||
yield message
|
||||
return
|
||||
|
||||
def get_or_create_agent(self, agent_type: str, **kwargs) -> Agent:
|
||||
"""
|
||||
Get or create and append a new agent of the specified type, ensuring only one agent per type exists.
|
||||
@ -200,7 +169,6 @@ class Context(BaseModel):
|
||||
summary += f"\nChat Name: {agent.name}\n"
|
||||
return summary
|
||||
|
||||
|
||||
from .agents import Agent
|
||||
|
||||
Context.model_rebuild()
|
||||
|
@ -2,6 +2,14 @@ import os
|
||||
|
||||
ollama_api_url = "http://ollama:11434" # Default Ollama local endpoint
|
||||
|
||||
user_dir = "/opt/backstory/users"
|
||||
user_info_file = "info.json" # Relative to "{user_dir}/{user}"
|
||||
default_username = "jketreno"
|
||||
rag_content_dir = "rag-content" # Relative to "{user_dir}/{user}"
|
||||
# Path to candidate full resume
|
||||
resume_doc = "rag-content/resume/resume.md" # Relative to "{user_dir}/{user}/" (does not have to be in docs)
|
||||
persist_directory = "db" # Relative to "{user_dir}/{user}"
|
||||
|
||||
# Model name License Notes
|
||||
# model = "deepseek-r1:7b" # MIT Tool calls don"t work
|
||||
# model = "gemma3:4b" # Gemma Requires newer ollama https://ai.google.dev/gemma/terms
|
||||
@ -23,9 +31,6 @@ max_context = 2048 * 8 * 2
|
||||
# Where to store session json files
|
||||
context_dir = "/opt/backstory/sessions"
|
||||
|
||||
# Path to candidate full resume
|
||||
resume_doc = "/opt/backstory/docs/resume/resume.md"
|
||||
|
||||
# Location of frontend container's build output mapped into the container
|
||||
static_content = "/opt/backstory/frontend/deployed"
|
||||
|
||||
@ -34,12 +39,10 @@ logging_level = os.getenv("LOGGING_LEVEL", "INFO").upper()
|
||||
# RAG and Vector DB settings
|
||||
## Where to read RAG content
|
||||
|
||||
persist_directory = os.getenv("PERSIST_DIR", "/opt/backstory/chromadb")
|
||||
doc_dir = "/opt/backstory/docs/"
|
||||
chunk_buffer = 5 # Number of lines before and after chunk beyond the portion used in embedding (to return to callers)
|
||||
|
||||
# Maximum number of entries for ChromaDB to find
|
||||
default_rag_top_k = 30
|
||||
default_rag_top_k = 80
|
||||
|
||||
# Cosine Distance Equivalent Similarity Retrieval Characteristics
|
||||
# 0.2 - 0.3 0.85 - 0.90 Very strict, highly precise results only
|
||||
|
@ -37,6 +37,11 @@ __all__ = ["ChromaDBFileWatcher", "start_file_watcher", "ChromaDBGetResponse"]
|
||||
DEFAULT_CHUNK_SIZE = 750
|
||||
DEFAULT_CHUNK_OVERLAP = 100
|
||||
|
||||
class RagEntry(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
enabled: bool = True
|
||||
|
||||
class ChromaDBGetResponse(BaseModel):
|
||||
name: str = ""
|
||||
size: int = 0
|
||||
@ -56,7 +61,7 @@ class ChromaDBGetResponse(BaseModel):
|
||||
@field_validator("embeddings", "query_embedding", "umap_embedding_2d", "umap_embedding_3d")
|
||||
@classmethod
|
||||
def validate_embeddings(cls, value, field):
|
||||
logging.info(f"Validating {field.field_name} with value: {type(value)} - {value}")
|
||||
# logging.info(f"Validating {field.field_name} with value: {type(value)} - {value}")
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, np.ndarray):
|
||||
@ -83,8 +88,8 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
llm,
|
||||
watch_directory,
|
||||
loop,
|
||||
persist_directory=None,
|
||||
collection_name="documents",
|
||||
persist_directory,
|
||||
collection_name,
|
||||
chunk_size=DEFAULT_CHUNK_SIZE,
|
||||
chunk_overlap=DEFAULT_CHUNK_OVERLAP,
|
||||
recreate=False,
|
||||
@ -125,11 +130,13 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
self.processing_files = set()
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
def collection(self) -> Collection:
|
||||
return self._collection
|
||||
|
||||
@property
|
||||
def umap_collection(self) -> ChromaDBGetResponse | None:
|
||||
def umap_collection(self) -> ChromaDBGetResponse:
|
||||
if not self._umap_collection:
|
||||
raise ValueError("initialize_collection has not been called")
|
||||
return self._umap_collection
|
||||
|
||||
@property
|
||||
@ -342,7 +349,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
|
||||
# During initialization
|
||||
logging.info(
|
||||
f"Updating 2D UMAP for {len(self._umap_collection['embeddings'])} vectors"
|
||||
f"Updating 2D {self.collection_name} UMAP for {len(self._umap_collection['embeddings'])} vectors"
|
||||
)
|
||||
vectors = np.array(self._umap_collection["embeddings"])
|
||||
self._umap_model_2d = umap.UMAP(
|
||||
@ -358,7 +365,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
# ) # Should be 2
|
||||
|
||||
logging.info(
|
||||
f"Updating 3D UMAP for {len(self._umap_collection['embeddings'])} vectors"
|
||||
f"Updating 3D {self.collection_name} UMAP for {len(self._umap_collection['embeddings'])} vectors"
|
||||
)
|
||||
self._umap_model_3d = umap.UMAP(
|
||||
n_components=3,
|
||||
@ -374,6 +381,10 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
|
||||
def _get_vector_collection(self, recreate=False) -> Collection:
|
||||
"""Get or create a ChromaDB collection."""
|
||||
# Create the directory if it doesn't exist
|
||||
if not os.path.exists(self.persist_directory):
|
||||
os.makedirs(self.persist_directory)
|
||||
|
||||
# Initialize ChromaDB client
|
||||
chroma_client = chromadb.PersistentClient( # type: ignore
|
||||
path=self.persist_directory,
|
||||
@ -402,10 +413,6 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
name=self.collection_name, metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
|
||||
def create_chunks_from_documents(self, docs):
|
||||
"""Split documents into chunks using the text splitter."""
|
||||
return self.text_splitter.split_documents(docs)
|
||||
|
||||
def get_embedding(self, text: str) -> np.ndarray:
|
||||
"""Generate and normalize an embedding for the given text."""
|
||||
|
||||
@ -673,8 +680,8 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
def start_file_watcher(
|
||||
llm,
|
||||
watch_directory,
|
||||
persist_directory=None,
|
||||
collection_name="documents",
|
||||
persist_directory,
|
||||
collection_name,
|
||||
initialize=False,
|
||||
recreate=False,
|
||||
):
|
||||
@ -693,7 +700,7 @@ def start_file_watcher(
|
||||
|
||||
file_watcher = ChromaDBFileWatcher(
|
||||
llm,
|
||||
watch_directory,
|
||||
watch_directory=watch_directory,
|
||||
loop=loop,
|
||||
persist_directory=persist_directory,
|
||||
collection_name=collection_name,
|
||||
@ -719,31 +726,3 @@ def start_file_watcher(
|
||||
|
||||
logging.info(f"Started watching directory: {watch_directory}")
|
||||
return observer, file_watcher
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# When running directly, use absolute imports
|
||||
import defines
|
||||
|
||||
# Initialize Ollama client
|
||||
llm = ollama.Client(host=defines.ollama_api_url) # type: ignore
|
||||
|
||||
# Start the file watcher (with initialization)
|
||||
observer, file_watcher = start_file_watcher(
|
||||
llm,
|
||||
defines.doc_dir,
|
||||
recreate=True, # Start fresh
|
||||
)
|
||||
|
||||
# Example query
|
||||
query = "Can you describe James Ketrenos' work history?"
|
||||
top_docs = file_watcher.find_similar(query, top_k=3)
|
||||
logging.info(top_docs)
|
||||
|
||||
try:
|
||||
# Keep the main thread running
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
|
216
src/utils/user.py
Normal file
216
src/utils/user.py
Normal file
@ -0,0 +1,216 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, Field, model_validator # type: ignore
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Generator, ClassVar, Any, Dict, TYPE_CHECKING
|
||||
|
||||
from typing_extensions import Annotated, Union
|
||||
import numpy as np # type: ignore
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from prometheus_client import CollectorRegistry, Counter # type: ignore
|
||||
import traceback
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from . rag import start_file_watcher, ChromaDBFileWatcher, ChromaDBGetResponse
|
||||
from . import defines
|
||||
from . import Message
|
||||
#from . import Context
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .rag import RagEntry
|
||||
|
||||
class User(BaseModel):
|
||||
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc
|
||||
|
||||
username: str
|
||||
llm: Any = Field(exclude=True)
|
||||
rags: List[RagEntry] = Field(default_factory=list)
|
||||
first_name: str = ""
|
||||
last_name: str = ""
|
||||
full_name: str = ""
|
||||
contact_info : Dict[str, str] = {}
|
||||
user_questions : List[str] = []
|
||||
|
||||
#context: Optional[List[Context]] = []
|
||||
# file_watcher : ChromaDBFileWatcher = set by initialize
|
||||
# observer: Any = set by initialize
|
||||
# prometheus_collector : CollectorRegistry = set by initialize
|
||||
|
||||
# Internal instance members
|
||||
User__observer: Optional[Any] = Field(default=None, exclude=True)
|
||||
User__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
|
||||
User__prometheus_collector: Optional[CollectorRegistry] = Field(
|
||||
default=None, exclude=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def exists(cls, username: str):
|
||||
# Validate username format (only allow safe characters)
|
||||
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
|
||||
return False # Invalid username characters
|
||||
|
||||
# Check for minimum and maximum length
|
||||
if not (3 <= len(username) <= 32):
|
||||
return False # Invalid username length
|
||||
|
||||
# Use Path for safe path handling and normalization
|
||||
user_dir = Path(defines.user_dir) / username
|
||||
user_info_path = user_dir / defines.user_info_file
|
||||
|
||||
# Ensure the final path is actually within the intended parent directory
|
||||
# to help prevent directory traversal attacks
|
||||
try:
|
||||
if not user_dir.resolve().is_relative_to(Path(defines.user_dir).resolve()):
|
||||
return False # Path traversal attempt detected
|
||||
except (ValueError, RuntimeError): # Potential exceptions from resolve()
|
||||
return False
|
||||
|
||||
# Check if file exists
|
||||
return user_info_path.is_file()
|
||||
|
||||
# Wrapper properties that map into file_watcher
|
||||
@property
|
||||
def umap_collection(self) -> ChromaDBGetResponse:
|
||||
if not self.User__file_watcher:
|
||||
raise ValueError("initialize() has not been called.")
|
||||
return self.User__file_watcher.umap_collection
|
||||
|
||||
# Fields managed by initialize()
|
||||
User__initialized: bool = Field(default=False, exclude=True)
|
||||
@property
|
||||
def file_watcher(self) -> ChromaDBFileWatcher:
|
||||
if not self.User__file_watcher:
|
||||
raise ValueError("initialize() has not been called.")
|
||||
return self.User__file_watcher
|
||||
|
||||
@property
|
||||
def prometheus_collector(self) -> CollectorRegistry:
|
||||
if not self.User__prometheus_collector:
|
||||
raise ValueError("initialize() has not been called.")
|
||||
return self.User__prometheus_collector
|
||||
|
||||
@property
|
||||
def observer(self) -> Any:
|
||||
if not self.User__observer:
|
||||
raise ValueError("initialize() has not been called.")
|
||||
return self.User__observer
|
||||
|
||||
def generate_rag_results(
|
||||
self, message: Message, top_k=defines.default_rag_top_k, threshold=defines.default_rag_threshold
|
||||
) -> Generator[Message, None, None]:
|
||||
"""
|
||||
Generate RAG results for the given query.
|
||||
|
||||
Args:
|
||||
query: The query string to generate RAG results for.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries containing the RAG results.
|
||||
"""
|
||||
try:
|
||||
message.status = "processing"
|
||||
|
||||
entries: int = 0
|
||||
|
||||
for rag in self.rags:
|
||||
if not rag.enabled:
|
||||
continue
|
||||
message.response = f"Checking RAG context {rag.name}..."
|
||||
yield message
|
||||
chroma_results = self.file_watcher.find_similar(
|
||||
query=message.prompt, top_k=top_k, threshold=threshold
|
||||
)
|
||||
if chroma_results:
|
||||
query_embedding = np.array(chroma_results["query_embedding"]).flatten()
|
||||
|
||||
umap_2d = self.file_watcher.umap_model_2d.transform([query_embedding])[0]
|
||||
umap_3d = self.file_watcher.umap_model_3d.transform([query_embedding])[0]
|
||||
|
||||
rag_metadata = ChromaDBGetResponse(
|
||||
query=message.prompt,
|
||||
query_embedding=query_embedding.tolist(),
|
||||
name=rag.name,
|
||||
ids=chroma_results.get("ids", []),
|
||||
embeddings=chroma_results.get("embeddings", []),
|
||||
documents=chroma_results.get("documents", []),
|
||||
metadatas=chroma_results.get("metadatas", []),
|
||||
umap_embedding_2d=umap_2d.tolist(),
|
||||
umap_embedding_3d=umap_3d.tolist(),
|
||||
size=self.file_watcher.collection.count()
|
||||
)
|
||||
|
||||
message.metadata.rag.append(rag_metadata)
|
||||
message.response = f"Results from {rag.name} RAG: {len(chroma_results['documents'])} results."
|
||||
yield message
|
||||
|
||||
message.response = (
|
||||
f"RAG context gathered from results from {entries} documents."
|
||||
)
|
||||
message.status = "done"
|
||||
yield message
|
||||
return
|
||||
except Exception as e:
|
||||
message.status = "error"
|
||||
message.response = f"Error generating RAG results: {str(e)}"
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(message.response)
|
||||
yield message
|
||||
return
|
||||
|
||||
def initialize(self, prometheus_collector):
|
||||
if self.User__initialized:
|
||||
# Initialization can only be attempted once; if there are multiple attempts, it means
|
||||
# a subsystem is failing or there is a logic bug in the code.
|
||||
#
|
||||
# NOTE: It is intentional that self.User__initialize = True regardless of whether it
|
||||
# succeeded. This prevents server loops on failure
|
||||
raise ValueError("initialize can only be attempted once")
|
||||
self.User__initialized = True
|
||||
|
||||
user_dir = os.path.join(defines.user_dir, self.username)
|
||||
user_info = os.path.join(user_dir, defines.user_info_file)
|
||||
persist_directory=os.path.join(user_dir, defines.persist_directory)
|
||||
watch_directory=os.path.join(user_dir, defines.rag_content_dir)
|
||||
logger.info(f"User(username={self.username}, user_dir={user_dir} persist_directory={persist_directory}, watch_directory={watch_directory}")
|
||||
|
||||
info = {}
|
||||
# Always re-initialize the user's name and contact data from the info file in case it is changed
|
||||
try:
|
||||
with open(user_info, "r") as f:
|
||||
info = json.loads(f.read())
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {user_info}: {e}")
|
||||
if info:
|
||||
logger.error(f"info={info}")
|
||||
|
||||
self.first_name = info.get("first_name", self.username)
|
||||
self.last_name = info.get("first_name", "")
|
||||
self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}")
|
||||
self.contact_info = info.get("contact_info", {})
|
||||
self.user_questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"])
|
||||
|
||||
os.makedirs(persist_directory, exist_ok=True)
|
||||
os.makedirs(watch_directory, exist_ok=True)
|
||||
|
||||
self.User__prometheus_collector = prometheus_collector
|
||||
self.User__observer, self.User__file_watcher = start_file_watcher(
|
||||
llm=self.llm,
|
||||
collection_name=self.username,
|
||||
persist_directory=persist_directory,
|
||||
watch_directory=watch_directory,
|
||||
recreate=False, # Don't recreate if exists
|
||||
)
|
||||
has_username_rag = any(item["name"] == self.username for item in self.rags)
|
||||
if not has_username_rag:
|
||||
self.rags.append(RagEntry(
|
||||
name=self.username,
|
||||
description=f"Expert data about {self.full_name}.",
|
||||
))
|
||||
|
||||
User.model_rebuild()
|
0
users-prod/.keep
Normal file
0
users-prod/.keep
Normal file
0
users/.keep
Normal file
0
users/.keep
Normal file
4
users/eliza/info.json
Normal file
4
users/eliza/info.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"first_name": "Eliza",
|
||||
"last_name": "Morgan"
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
# Plant Conservation Specialist
|
||||
|
||||
**Organization:** Oregon Botanical Gardens
|
||||
**Location:** Portland, Oregon
|
||||
**Duration:** April 2017 - May 2020
|
||||
|
||||
## Position Overview
|
||||
As Plant Conservation Specialist at the Oregon Botanical Gardens, I managed the institution's ex-situ conservation program for rare and endangered plant species native to the Pacific Northwest. This position bridged scientific research, hands-on horticulture, and public education.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
### Ex-situ Conservation Program
|
||||
- Coordinated conservation collections for 45 rare and endangered plant species
|
||||
- Developed and maintained comprehensive database of accession records, phenology data, and propagation histories
|
||||
- Established genetic management protocols to ensure maximum diversity in conservation collections
|
||||
- Collaborated with Center for Plant Conservation on national rare plant conservation initiatives
|
||||
|
||||
### Propagation & Cultivation
|
||||
- Designed specialized growing environments for challenging species with specific habitat requirements
|
||||
- Experimented with various propagation techniques including tissue culture, specialized seed treatments, and vegetative methods
|
||||
- Maintained detailed documentation of successful and unsuccessful propagation attempts
|
||||
- Achieved first-ever successful cultivation of three critically endangered Oregon wildflowers
|
||||
|
||||
### Reintroduction Planning
|
||||
- Collaborated with federal and state agencies on plant reintroduction strategies
|
||||
- Conducted site assessments to evaluate habitat suitability for reintroductions
|
||||
- Developed monitoring protocols to track survival and reproduction of reintroduced populations
|
||||
- Prepared detailed reintroduction plans for 8 endangered species
|
||||
|
||||
### Research Projects
|
||||
- Designed and implemented germination studies for 15 rare species with unknown propagation requirements
|
||||
- Conducted pollination biology investigations for several endangered plant species
|
||||
- Collaborated with university researchers on seed viability and longevity studies
|
||||
- Maintained comprehensive records of phenological patterns across multiple growing seasons
|
||||
|
||||
### Education & Outreach
|
||||
- Developed educational materials explaining the importance of plant conservation
|
||||
- Led specialized tours focusing on rare plant conservation for visitors and donors
|
||||
- Trained volunteers in proper care of sensitive plant collections
|
||||
- Created interpretive signage for conservation garden displays
|
||||
|
||||
## Notable Projects
|
||||
|
||||
1. **Willamette Valley Prairie Species Recovery**
|
||||
- Established seed bank of 25 declining prairie species
|
||||
- Developed germination protocols that improved propagation success from 30% to 75%
|
||||
- Produced over 5,000 plants for restoration projects throughout the region
|
||||
|
||||
2. **Alpine Rare Plant Conservation Initiative**
|
||||
- Created specialized growing facilities mimicking alpine conditions
|
||||
- Successfully propagated 8 high-elevation rare species never before cultivated
|
||||
- Documented critical temperature and moisture requirements for germination
|
||||
|
||||
3. **Serpentine Soils Conservation Collection**
|
||||
- Developed custom soil mixes replicating challenging serpentine conditions
|
||||
- Maintained living collection of 12 rare serpentine endemic species
|
||||
- Created public display educating visitors about specialized plant adaptations
|
||||
|
||||
## Achievements
|
||||
- Received "Conservation Innovation Award" from the American Public Gardens Association (2019)
|
||||
- Developed propagation protocol for Kincaid's lupine that doubled germination success rates
|
||||
- Established Oregon Botanical Gardens' first dedicated conservation nursery facility
|
||||
- Created seed banking protocols adopted by three other botanical institutions
|
75
users/eliza/rag-content/jobs/research-assistant.md
Normal file
75
users/eliza/rag-content/jobs/research-assistant.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Research Assistant
|
||||
|
||||
**Organization:** Institute for Applied Ecology
|
||||
**Location:** Corvallis, Oregon
|
||||
**Duration:** January 2015 - March 2017
|
||||
|
||||
## Position Overview
|
||||
As Research Assistant at the Institute for Applied Ecology, I supported multiple research projects focused on native plant ecology and restoration techniques. This position provided foundational experience in applying scientific methods to practical conservation challenges.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
### Field Surveys
|
||||
- Conducted comprehensive botanical surveys in diverse ecosystems throughout western Oregon
|
||||
- Documented population sizes, health metrics, and habitat conditions for threatened plant species
|
||||
- Established long-term monitoring plots using standardized protocols
|
||||
- Collected voucher specimens for herbarium collections following strict ethical guidelines
|
||||
- Mapped plant populations using GPS and GIS technologies
|
||||
|
||||
### Greenhouse Operations
|
||||
- Assisted with propagation of native plants for restoration experiments and projects
|
||||
- Maintained detailed records of seed treatments, germination rates, and growth parameters
|
||||
- Implemented and monitored experimental growing conditions for research projects
|
||||
- Managed irrigation systems and pest control for approximately 10,000 plants
|
||||
- Prepared plant materials for outplanting at restoration sites
|
||||
|
||||
### Data Collection & Analysis
|
||||
- Collected vegetation data using quadrat, transect, and plot-based sampling methods
|
||||
- Processed and organized large datasets for long-term monitoring studies
|
||||
- Performed statistical analyses using R to assess restoration treatment effectiveness
|
||||
- Created data visualization graphics for reports and publications
|
||||
- Maintained research databases ensuring data quality and accessibility
|
||||
|
||||
### Research Projects
|
||||
- **Prairie Restoration Techniques:**
|
||||
- Compared effectiveness of different site preparation methods on native plant establishment
|
||||
- Monitored post-treatment recovery of native species diversity
|
||||
- Documented invasive species response to various control techniques
|
||||
|
||||
- **Rare Plant Demography:**
|
||||
- Tracked population dynamics of three endangered Willamette Valley plant species
|
||||
- Monitored individual plant survival, growth, and reproductive output
|
||||
- Assessed impacts of management interventions on population trends
|
||||
|
||||
- **Seed Viability Studies:**
|
||||
- Tested germination requirements for 30+ native species
|
||||
- Evaluated effects of smoke, scarification, and stratification on dormancy
|
||||
- Documented optimal storage conditions for maintaining seed viability
|
||||
|
||||
### Publication Support
|
||||
- Co-authored three peer-reviewed publications on prairie restoration techniques
|
||||
- Prepared figures, tables, and data appendices for manuscripts
|
||||
- Conducted literature reviews on specialized ecological topics
|
||||
- Assisted with manuscript revisions based on peer review feedback
|
||||
|
||||
## Key Projects
|
||||
|
||||
1. **Willamette Valley Wet Prairie Restoration**
|
||||
- Implemented experimental plots testing 4 restoration techniques
|
||||
- Collected 3 years of post-treatment vegetation data
|
||||
- Documented successful establishment of 15 target native species
|
||||
|
||||
2. **Endangered Butterfly Habitat Enhancement**
|
||||
- Propagated host and nectar plants for Fender's blue butterfly habitat
|
||||
- Monitored plant-insect interactions in restoration sites
|
||||
- Assessed habitat quality improvements following restoration treatments
|
||||
|
||||
3. **Native Seed Production Research**
|
||||
- Tested cultivation methods for improving seed yields of 10 native species
|
||||
- Documented pollination requirements for optimal seed production
|
||||
- Developed harvest timing recommendations based on seed maturation patterns
|
||||
|
||||
## Publications
|
||||
- Johnson, T., **Morgan, E.**, et al. (2016). "Comparative effectiveness of site preparation techniques for prairie restoration." *Restoration Ecology*, 24(4), 472-481.
|
||||
- Williams, R., **Morgan, E.**, & Smith, B. (2016). "Germination requirements of Willamette Valley wet prairie species." *Native Plants Journal*, 17(2), 99-112.
|
||||
- **Morgan, E.**, Johnson, T., & Davis, A. (2017). "Long-term vegetation response to restoration treatments in degraded oak savanna." *Northwest Science*, 91(1), 27-39.
|
55
users/eliza/rag-content/jobs/restoration-botanist.md
Normal file
55
users/eliza/rag-content/jobs/restoration-botanist.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Senior Restoration Botanist
|
||||
|
||||
**Organization:** Pacific Northwest Conservation Alliance
|
||||
**Location:** Portland, Oregon
|
||||
**Duration:** June 2020 - Present
|
||||
|
||||
## Position Overview
|
||||
As Senior Restoration Botanist at the Pacific Northwest Conservation Alliance, I lead complex restoration projects aimed at preserving endangered plant communities throughout the Cascade Range. This role combines technical botanical expertise with project management and leadership responsibilities.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
### Project Leadership
|
||||
- Design and implement comprehensive restoration plans for degraded ecosystems with emphasis on rare plant conservation
|
||||
- Lead field operations across multiple concurrent restoration sites covering over 2,000 acres
|
||||
- Establish measurable success criteria and monitoring protocols for all restoration projects
|
||||
- Conduct regular site assessments to track progress and adapt management strategies
|
||||
|
||||
### Native Plant Propagation
|
||||
- Oversee native plant nursery operations producing 75,000+ plants annually
|
||||
- Develop specialized propagation protocols for difficult-to-grow rare species
|
||||
- Maintain detailed records of germination rates, growth metrics, and treatment effects
|
||||
- Coordinate seed collection expeditions throughout diverse ecosystems of the Pacific Northwest
|
||||
|
||||
### Team Management
|
||||
- Supervise a core team of 5 field botanists and up to 12 seasonal restoration technicians
|
||||
- Conduct staff training on plant identification, restoration techniques, and field safety
|
||||
- Facilitate weekly team meetings and monthly progress reviews
|
||||
- Mentor junior staff and provide professional development opportunities
|
||||
|
||||
### Funding & Partnerships
|
||||
- Secured $750,000 in grant funding for riparian habitat restoration projects
|
||||
- Authored major sections of successful proposals to state and federal agencies
|
||||
- Manage project budgets ranging from $50,000 to $250,000
|
||||
- Cultivate partnerships with government agencies, tribes, and conservation NGOs
|
||||
|
||||
### Notable Projects
|
||||
1. **Willamette Valley Prairie Restoration Initiative**
|
||||
- Restored 350 acres of native prairie habitat
|
||||
- Reintroduced 12 threatened plant species with 85% establishment success
|
||||
- Developed innovative seeding techniques that increased native diversity by 40%
|
||||
|
||||
2. **Mount Hood Meadow Rehabilitation**
|
||||
- Led post-wildfire recovery efforts in alpine meadow ecosystems
|
||||
- Implemented erosion control measures using native plant materials
|
||||
- Achieved 90% reduction in invasive species cover within treatment areas
|
||||
|
||||
3. **Columbia River Gorge Rare Plant Recovery**
|
||||
- Established new populations of 5 federally listed plant species
|
||||
- Developed habitat suitability models to identify optimal reintroduction sites
|
||||
- Created monitoring protocols adopted by multiple conservation organizations
|
||||
|
||||
## Achievements
|
||||
- Received Excellence in Ecological Restoration Award from the Society for Ecological Restoration, Northwest Chapter (2023)
|
||||
- Featured in Oregon Public Broadcasting documentary on native plant conservation (2022)
|
||||
- Published 2 peer-reviewed articles on restoration techniques developed during project work
|
BIN
users/eliza/rag-content/resume/Eliza_Morgan_Resume.docx
Executable file
BIN
users/eliza/rag-content/resume/Eliza_Morgan_Resume.docx
Executable file
Binary file not shown.
91
users/eliza/rag-content/resume/Eliza_Morgan_Resume.md
Normal file
91
users/eliza/rag-content/resume/Eliza_Morgan_Resume.md
Normal file
@ -0,0 +1,91 @@
|
||||
Eliza Morgan
|
||||
Portland, Oregon | eliza.morgan@email.com | (555) 123-4567
|
||||
linkedin.com/in/elizamorgan | ORCID: 0000-0002-XXXX-XXXX
|
||||
|
||||
**Professional Summary**
|
||||
|
||||
Conservation botanist with over a decade of experience leading ecological restoration projects, advancing rare plant propagation methods, and managing native plant programs across the Pacific Northwest. Proven record of scientific innovation, collaborative project leadership, and effective stakeholder engagement. Passionate about preserving botanical diversity through applied research, restoration, and public education.
|
||||
|
||||
**Professional Experience**
|
||||
|
||||
**Senior Restoration Botanist**
|
||||
|
||||
Pacific Northwest Conservation Alliance, Portland, OR | June 2020 – Present
|
||||
|
||||
- Directed restoration efforts across 2,000+ acres of degraded habitat with a focus on endangered plant communities.
|
||||
- Managed propagation and deployment of 75,000+ native plants annually.
|
||||
- Supervised and mentored a cross-functional team of botanists and technicians.
|
||||
- Secured $750,000+ in grant funding and led stakeholder engagement with tribal, governmental, and NGO partners.
|
||||
- Developed seeding and reintroduction techniques increasing native species diversity by 40%.
|
||||
|
||||
* Key Projects & Achievements:
|
||||
|
||||
- Willamette Valley Prairie Restoration: Reintroduced 12 threatened species; 85% establishment success.
|
||||
|
||||
- Mount Hood Meadow Rehabilitation: Reduced invasive cover by 90% post-wildfire.
|
||||
|
||||
- Columbia River Gorge Rare Plant Recovery: Created new populations of 5 federally listed species.
|
||||
|
||||
- Award: Excellence in Ecological Restoration, Society for Ecological Restoration (2023)
|
||||
|
||||
- Media: Featured in OPB documentary on native plant conservation (2022)
|
||||
|
||||
- Publications: 2 peer-reviewed articles on restoration innovations.
|
||||
|
||||
**Plant Conservation Specialist**
|
||||
|
||||
Oregon Botanical Gardens, Portland, OR | April 2017 – May 2020
|
||||
|
||||
- Led ex-situ conservation for 45 endangered Pacific Northwest plant species.
|
||||
- Developed genetic management protocols and created Oregon Botanical’s first dedicated conservation nursery.
|
||||
- Collaborated with the Center for Plant Conservation and state agencies on reintroduction planning.
|
||||
- Authored seed banking protocols adopted by three other institutions.
|
||||
|
||||
* Key Contributions:
|
||||
|
||||
- Propagated 3 species never before cultivated; pioneered tissue culture and seed treatments.
|
||||
|
||||
- Developed Kincaid’s lupine protocol doubling germination success.
|
||||
|
||||
- Designed alpine and serpentine plant displays for public education.
|
||||
|
||||
- Award: Conservation Innovation Award, American Public Gardens Association (2019)
|
||||
|
||||
**Research Assistant**
|
||||
|
||||
Institute for Applied Ecology, Corvallis, OR | January 2015 – March 2017
|
||||
|
||||
- Conducted botanical field surveys, greenhouse propagation, and data analysis for native plant research projects.
|
||||
- Co-authored 3 peer-reviewed studies on prairie restoration, seed viability, and plant demography.
|
||||
- Supported endangered species monitoring and habitat restoration for the Fender’s blue butterfly.
|
||||
|
||||
* Highlighted Projects:
|
||||
|
||||
- Wet Prairie Restoration: Established 15 native species across experimental plots.
|
||||
|
||||
- Seed Viability Studies: Tested dormancy-breaking treatments for 30+ species.
|
||||
|
||||
- Publications:
|
||||
|
||||
- Morgan, E. et al. (2017). \*Northwest Science\*
|
||||
|
||||
- Morgan, E. et al. (2016). \*Restoration Ecology\*, \*Native Plants Journal\*
|
||||
|
||||
**Education**
|
||||
|
||||
B.S. in Botany | Oregon State University, Corvallis, OR | Graduated: 2014
|
||||
|
||||
**Skills & Expertise**
|
||||
|
||||
- Native plant propagation & nursery management
|
||||
- Ecological restoration & reintroduction planning
|
||||
- Grant writing & budget management
|
||||
- GIS & GPS mapping | R statistical analysis
|
||||
- Team leadership & staff mentoring
|
||||
- Science communication & outreach
|
||||
|
||||
**Professional Affiliations**
|
||||
|
||||
- Society for Ecological Restoration (SER)
|
||||
- American Public Gardens Association (APGA)
|
||||
- Center for Plant Conservation (CPC)
|
Loading…
x
Reference in New Issue
Block a user