Compare commits

...

9 Commits

34 changed files with 1615 additions and 1728 deletions

View File

@ -1,3 +1,4 @@
* *
!src !src
!frontend
**/node_modules **/node_modules

View File

@ -292,14 +292,18 @@ RUN { \
echo ' echo "Generating self-signed certificate for HTTPS"'; \ 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"'; \ 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"'; \
echo ' fi' ; \ echo ' fi' ; \
echo ' declare once=0' ; \
echo ' while true; do'; \ echo ' while true; do'; \
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \ echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
echo ' echo "Launching Backstory server..."'; \ echo ' echo "Launching Backstory server..."'; \
echo ' python src/server.py "${@}" || echo "Backstory server died."'; \ echo ' python src/server.py "${@}" || echo "Backstory server died."'; \
echo ' echo "Sleeping for 3 seconds."'; \
echo ' else'; \ echo ' else'; \
echo ' echo "block-server file exists. Not launching."'; \ echo ' if [[ ${once} -eq 0 ]]; then' ; \
echo ' echo "/opt/backstory/block-server exists. Sleeping for 3 seconds."'; \
echo ' once=1' ; \
echo ' fi' ; \
echo ' fi' ; \ echo ' fi' ; \
echo ' echo "Sleeping for 3 seconds."'; \
echo ' sleep 3'; \ echo ' sleep 3'; \
echo ' done' ; \ echo ' done' ; \
echo 'fi'; \ echo 'fi'; \
@ -313,6 +317,11 @@ ENV SYCL_CACHE_PERSISTENT=1
ENV PATH=/opt/backstory:$PATH ENV PATH=/opt/backstory:$PATH
COPY /src/ /opt/backstory/src/ COPY /src/ /opt/backstory/src/
COPY /frontend/ /opt/backstory/frontend/
WORKDIR /opt/backstory/frontend
RUN npm install --force
WORKDIR /opt/backstory
ENTRYPOINT [ "/entrypoint.sh" ] ENTRYPOINT [ "/entrypoint.sh" ]

View File

@ -1,63 +0,0 @@
# 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

View File

@ -1,75 +0,0 @@
# 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.

View File

@ -1,55 +0,0 @@
# 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

View File

@ -1,78 +1,272 @@
Resume - Eliza Morgan # JAMES KETRENOS
software architect, designer, developer, and team lead
Beaverton, OR 97003
Professional Profile james@ketrenos.com
Dedicated botanist with 8 years of experience in plant conservation and ecological restoration. Specializing in native plant propagation and habitat rehabilitation for endangered flora species. (503) 501 8281
Contact Information
Email: eliza.morgan@botanist.com Seeking an opportunity to contribute to the advancement of energy efficient AI solutions,
Phone: (555) 782-3941 James is a driven problem solver, solution creator, technical leader, and skilled software
Address: 427 Maple Street, Portland, OR 97205 developer focused on rapid, high-quality results, with an eye toward bringing solutions to
the market.
## SUMMARY
Problem-solving: Trusted resource for executive leadership, able to identify opportunities to
bridge technical gaps, adopt new technologies, and improve efficiency and quality for internal
and external customers.
Proficient: Adept in compiled and interpreted languages, the software frameworks built around them,
and front- and backend infrastructure. Leveraging deep and varied experience to quickly find
solutions. Rapidly familiarizes and puts to use new and emerging technologies.
Experienced: 20+ years of experience as an end-to-end Linux software architect, team lead,
developer, system administrator, and user. Working with teams to bring together technologies
into existing ecosystems for a myriad of technologies.
Leader: Frequent project lead spanning all areas of development and phases of the product life
cycle from pre-silicon to post launch support. Capable change agent and mentor, providing
technical engineering guidance to multiple teams and organizations.
Communicates: Thrives on helping people solve problems, working to educate others to help them
better understand problems and work toward solutions.
## RECENT HISTORY
2024-2025: Present
* Developed 'backstory'
* An Large Language Model (LLM) pipeline allowing interactive queries about James' resume.
* Utilizing both Retrieval-Augmented Generation (RAG) and fine-tuned approaches, questions asked about James will use information from his resume and portfolio for answers.
* Includes a full-stack React web ui and backend.
* While developing Backstory, both Hugging Face and Ollama were used.
* While exploring ways to augment the reduced 7B parameter model, various prompts, RAG, and Parameter-Efficient Fine-Tuning (PEFT) via Quantized Low-Rank Adapter (QLORA) were used.
* Languages: Python, C/C++, JavaScript, TypeScript, React, HTML5, CSS, NodeJS, Markdown, git
* URL: https://github.com/jketreno/backstory
* Developed 'ze-monitor'
* A lightweight C++ Linux application leveraging Level Zero Sysman APIs to provide 'top' like device monitoring of Intel GPUs.
* URL: https://github.com/jketreno/ze-monitor
* Languages: C++, CMake, DEB, RPM, bash, git
* Provided a new release of Eikona, targetting the latest Android APIs.
* Tracked down the latest versions of various packages, updating to align with deprecated and new APIs.
* Languages: React Expo, Android SDK, Java, BitKeeper
2018-2024: Intel® Graphics Software Staff Architect and Lead
* Redefined how Intel approaches graphics enabling on Linux to meet customer and product timelines.
* Many internal facing presentations discussing goals, roadmap, and reasoning.
* Developed internal tools to help managers better forecast and look into where engineering developers were working in alignment with business objectives.
* Languages: DEB, RPM, GitHub Actions (GHA), HTML5, JavaScript, JIRA, SQL, NodeJS, Google Docs APIs
* Spearheaded internal projects to prove out the developer and customer deployment experience when using Intel graphics products with PyTorch, working to ensure all ingredients are available and consumable for success (from kernel driver integration, runtime, framework integration, up to containerized Python workload solution deployment.)
*
* Focused on improving the customer experience for Intel graphics software for Linux in the data center, high-performance compute clusters, and end users. Worked with several teams and business units to close gaps, improve our software, documentation, and release methodologies.
* Worked with hardware and firmware teams to scope and define architectural solutions for customer features.
* Worked with teams to add telemetry data available from Intel GPUs for use in Prometheus, Grafana, and collectd. Developed dashboards and infrastructure to manage node deployments using GitHub Actions, runners, and Docker.
1998-2018: Open Source Software Architect and Lead
* Defined software architecture for handheld devices, tablets, Internet of Things, smart appliances, and emerging technologies. Key resource to executive staff to investigate emerging technologies and drive solutions to close existing gaps
* James career at Intel has been diverse. His strongest skills are related to quickly ramping on technologies being utilized in the market, identifying gaps in existing solutions, and working with teams to close those gaps. He excels at adopting and fitting new technology trends as they materialize in the industry.
## PROLONGED HISTORY
The following are technical areas James has been an architect, team lead, and/or individual contributor:
* Linux release infrastructure overhaul: Identified bottlenecks in the CI/CD build pipeline, built proof-of-concept, and moved to production for generating releases of Intel graphics software (https://dgpu-docs.intel.com) as well as internal dashboards and infrastructure for tracking build and release pipelines. JavaScript, HTML, Markdown, RTD, bash/python, Linux packaging, Linux repositories, Linux OS release life cycles, sqlite3. Worked with multiple teams across Intel to meet Intels requirements for public websites as well as to integrate with existing build and validation methodologies while educating teams on tools and infrastructure available from the ecosystem (vs. roll-your-own).
* Board Explorer: Web app targeting developer ecosystem to utilize new single board computers, providing quick access to board details, circuits, and programming information. Delivered as a pure front-end service (no backend required) https://board-explorer.github.io/board-explorer/#quark_mcu_dev_kit_d2000. Tight coordination with UX design team. JavaScript, HTML, CSS, XML, hardware specs, programming specs.
* (internal) Travel Requisition: Internal HTML application and backend enabling internal organizations to request travel approval and a manager front end to track budgetary expenditures in order to determine approval/deny decisions. NodeJS, JavaScript, Polymer, SQL. Tight coordination with internal requirements providers and UX design teams.
* Developer Journey: Web infrastructure allowing engineers to document DIY processes. Front end for parsing, viewing, and following projects. Back end for managing content submitted (extended markdown) including images, videos, and screencasts. Tight coordination with UX design team.
* Robotics: Worked with teams aligning on a ROS (Robot OS) roadmap and alignment. Presented at Embedded Linux conference on the state of open source and robotics. LIDAR, Intel RealSense, opencv, python, C. Developed a robotic vision controlled stewart platform that could play the marble game labyrinth.
* Moblin and MeeGo architect: Focused on overall software architecture as well as moving forward multi-touch and the industry shift to resolution independent applications; all in a time before smart phones as we know them today. Qt, HTML5, EFL.
* Marblin: An HTML/WebGL graphical application simulating the 2D collision physics of marbles in a 3D rendered canvas.
* Linux Kernel: Developed and maintained initial Intel Pro Wireless 2100, 2200, and 3945 drivers in the Linux kernel. C, Software Defined Radios, IEEE 802.11, upstream kernel driver, team lead for team that took over the Intel wireless drivers, internal coordination regarding technical and legal issues surrounding the wireless stack.
* Open source at Intel: Built proof-of-concepts to illustrate to management the potential and opportunities for Intel by embracing open source and Linux.
* Intel Intercast Technology: Team lead for Intel Intercast software for Windows. Worked with 3rd party companies to integrate the technology into their solutions.
# Professional Projects
## 1995 - 1998: Intel Intercast Technology
* OS: Microsoft Windows Application, WinTV
* Languages: C++
* Role: Team lead and software architect
* Microsoft Media infrastructure
* Windows kernel driver work
* Worked with internal teams and external companies to expand compatible hardware and integrate with Windows
* Integration of Internet Explorer via COM embedding into the Intercast Viewer
## 1999 - 2024: Linux evangelist
* One of the initial members of Intel's Open Source Technology Center (OTC)
* Worked across Intel organizational boundaries to educate teams on the benefits and working model of the Linux open source ecosystem
* Deep understanding of licensing issues, political dynamics, community goals, and business needs
* Frequent resource for executive management and teams looking to leverage open source software
## 2000 - 2001: COM on Linux Prototype
* Distributed component object model
* Languages: C++, STL, Flex, Yacc, Bison
* Role: Team lead and architect
* Evaluated key performance differences between Microsoft Component Object Model's (COM) IUnknown (QueryInterface, AddRef, Release) vs. the Component Object Request Broker Architecture (CORBA) for both in-process and distributed cross-process and remote communication.
* Developed prototype tool-chain and functional code providing a Linux compatible implementation of COM
## 1998 - 2000: Intel Dot Station
* Languages: Java, C
* Designed and built a "visual lens" Java plugin for Netscape Navigator
* Role: Software architect
## 2000 - 2002: Carrier Grade Linux
* OS distribution work
* Contributed to the Linux System Base specification
* Role: Team lead and software architect working with internal and external collaborators
## 2004 - 2006: Intel Wireless Linux Kernel Driver
* Languages: C
* Authored original ipw2100, ipw2200, and ipw3945 Linux kernel drivers
* Built IEEE 802.11 wireless subsystem
* Hosted Wireless Birds-of-a-Feather talk at the Ottawa Linux Symposium
* Maintained SourceForge web presence, IRC channel, and community
## 2015 - 2018: Robotics
* Languages: C, Python, NodeJS
* "Maker" blogs on developing a Stewart Platform
*
* Image recognition and tracking
* Presented at Embedded Linux Conference
## 2012 - 2017: RT24 - crosswalk
* Chromium based native web application host
* Role: Team lead and software architect
* Worked with WebGL, Web Assembly, Native Client (NaCl)
* Several internal presentations at various corporate events
## 2007 - 2009: Moblin
* Tablet targetting OS distribution
* Role: Team lead and software architect and requirements
* Technology evaluation: Cairo, EFL, GTK, Clutter
* Languages: C, C++, OpenGL
## 2012 - Web Sys Info
* W3C
* Tizen Working Group
## 2007 - 2017: Marblin
* An interactive graphical stress test of rendering contexts
* Ported to each framework being used for OS development
* Originally written in C and using Clutter, ported to WebGL and EFL
## 2009 - 2011: MeeGo
* The merging of Linux Foundation's Moblin with Nokia's Maemo
* Coordinated and worked across business groups at Intel and Nokia
* Role: Team lead and software architect
* Focused on:
* Resolution independent user interfaces
* Multi-touch enabling in X
* Educated teams on the interface paradigm shift to "mobile first"
* Presented at MeeGo Conference
* Languages: C++, QT, HTML5
## Android on Intel
## 2011 - 2013: Tizen
* Rendering framework: Enlightenment Foundation Library (EFL)
* Focused on: API specifications
* Languages: JavaScript, HTML, C
## 2019 - 2024: Intel Graphics Architect
* Technologies: C, JavaScript, HTML5, React, Markdown, bash, GitHub, GitHub Actions, Docker, Clusters, Data Center, Machine Learning, git
* Role:
* Set strategic direction for working with open source ecosystem
* Worked with hardware and software architects to plan, execute, and support features
* Set strategic direction for overhauling the customer experience for Intel graphics on Linux
# Personal Projects
1995 - 2023: Photo Management Software
* Languages: C, JavaScript, PHP, HTML5, CSS, Polymer, React, SQL
* Role: Personal photo management software, including facial recognition
* Image classification, clustering, and identity
2020 - 2025: Eikona Android App
* OS: Android
* Languages: Java, Expo, React
* Role: Maintainer for Android port
2019 - 2023: Peddlers of Ketran
* Languages: JavaScript, React, NodeJS, HTML5, CSS
* Features: Audio, Video, and Text chat. Full game plus expansions.
* Role: Self-hosted online multiplayer clone of Settlers of Catan
2025: Ze-Monitor
* C++ utility leveraging Level Zero API to monitor GPUs
* https://github.com/jketreno/ze-monitor
Education:
* Studied computer science at University of California San Diego, Oregon State University, and Portland State University
* In his senior year of completing a bachelors degree, James left college to work full time for Intel Corporation
JAMES KETRENOS
software architect, designer, developer, and team lead
Beaverton, OR 97003
james@ketrenos.com
(503) 501 8281
Professional Summary
James Ketrenos is an experienced software architect, designer, developer, and team lead with over two decades of expertise in Linux-based systems.
Leveraging his strong problem-solving skills and deep technical knowledge across various domains including LLMs (Large Language Models),
RAG (Relevance-Augmented Generation), and AI development, James brings a unique blend of industry experience to the table. His latest
projects include 'Backstory,' an interactive GPT application that offers potential employers with insights into candidates.
Skills
Languages: C, C++, Python, Assembly, HTML, CSS, JavaScript, Typescript, Java
Continuous Integration/Continuous Deployment, Build and Release, and Packaging Systems spanning multiple project domains and infrastructures
SDKs and APIs for Internal and External Products (W3C, Tizen, MeeGo)
Large Language Model (LLM) evaluation, selection, and deployment
Retrieval-Augmented Generation (RAG) including data curation, processing, and searching
Fine-Tuning LLMs
Extending LLMs with tool capabilities (llama3.2, qwen2.5)
Professional Experience
Backstory | Software Architect | 2025 - Present
Developed an interactive LLM application that streamlines content generation and information processing for job candidates. Built a full-stack React web UI and backend using RAG (Retrieval-Augmented Generation) techniques.
Utilized Python, C/C++, JavaScript, TypeScript, React, HTML5, CSS, NodeJS, Markdown, git to create the project.
Contributed significantly in developing the project using both Retrieval-Augmented Generation (RAG) and fine-tuned approaches.
Intel Graphics Software Staff Architect and Lead | Software Engineer, Intel Corporation | 2018 - 2024
Redefined how Intel approaches graphics enabling on Linux to meet customer and product timelines. Spearheaded internal projects to prove out the developer and customer deployment experience when using Intel graphics products on Linux.
Worked closely with hardware and firmware teams to scope and define architectural solutions for features such as firmware deployment and telemetry data collection from Intel GPUs in the data center.
Deployed internal LLM instances for developer access to those capabilities without exposing content to the cloud
Architected, prototyped, built, and deployed build and release infrastructure that improved efficiency and developer insight
Collaborated across various business units and geographies, integrating multiple development and validation methodologies aligned with executive objectives.
Worked with hardware architects to plan, execute, and support features for Intel graphics software.
Led multiple internal development projects involving customer-facing features for the Linux data center environment.
Open Source Evangelist | Software Architect, Intel Corporation | 2000 - 2018
* Set strategic direction for working within the open-source ecosystem as part of the Intel Open Source Technology Center (OTC). Educated teams on the benefits and working model of Linux.
* Contributed to the development of various open-source software projects such as MeeGo and Marblin. Played a key role in defining software architecture for handheld devices, tablets, IoT, smart appliances, and emerging technologies.
* Worked across Intel organizational boundaries to educate teams on Linux's benefits and working model.
* Built proof-of-concepts to illustrate potential and opportunities for open source software adoption within the company. Contributed to Linux kernel driver work, including development of initial drivers for wireless hardware.
* Frequent resource to help businesses understand various open source licensing models and how they can be used
Developer Journey Web Infrastructure Lead | Software Architect, Intel Corporation | 2004 - 2017
* Designed and built a comprehensive web infrastructure for engineers to document DIY processes, including the use of React and Polymer frameworks. Led a team in managing content submitted via extended markdown formats such as images and videos.
Robotics Engineer | Software Architect, Intel Corporation | 1998 - 2003
* Developed a robotic vision-controlled Stewart platform capable of playing the marble Labyrinth game. Worked on image recognition and tracking applications using Python and NodeJS.
Education Education
University of Washington, Seattle * Attended University of California San Diego, Oregon State University, and Portland State University
Master of Science in Botany - 2015 * During senior year in college, James began working full time at Intel and stopped pursuing a degree.
Bachelor of Science in Environmental Science - 2013
Thesis: "Propagation Techniques for Endangered Willamette Valley Prairie Species"
Professional Experience
Senior Restoration Botanist
Pacific Northwest Conservation Alliance — Portland, OR
June 2020 - Present
Lead restoration projects for endangered plant communities in the Cascade Range Personal Projects
Develop and implement protocols for native seed collection and propagation
Manage a team of 5 field botanists and 12 seasonal restoration technicians
Secured $750,000 in grant funding for riparian habitat restoration projects
Plant Conservation Specialist Eikona Android App | 2016 - 2024
Oregon Botanical Gardens — Portland, OR * Developed Android port of Eikona application utilizing Java, Expo, and React technologies.
April 2017 - May 2020 * Maintains new releases targeting the latest Android APIs.
Coordinated ex-situ conservation program for 45 rare and endangered plant species System Administrator | 1995 - present
Maintained detailed documentation of propagation techniques and germination rates * Maintains a small cluster of servers providing email, photo management, game servers, and generative AI deployments.
Collaborated with regional agencies on plant reintroduction strategies
Developed educational materials for public outreach on native plant conservation
Research Assistant
Institute for Applied Ecology — Corvallis, OR
January 2015 - March 2017
Conducted field surveys of threatened plant populations throughout western Oregon
Assisted with greenhouse propagation of native plants for restoration projects
Collected and analyzed data for long-term vegetation monitoring studies
Co-authored three peer-reviewed publications on prairie restoration techniques
Technical Skills
Native plant identification and taxonomy
Seed collection and propagation methods
Vegetation monitoring and data analysis
Habitat assessment and restoration planning
GIS mapping (ArcGIS, QGIS)
Floristic surveys and botanical inventories
Professional Certifications
Certified Ecological Restoration Practitioner (CERP) - 2019
Wetland Delineation Certification - 2016
Wilderness First Responder - 2018
Professional Affiliations
Society for Ecological Restoration
Botanical Society of America
Native Plant Society of Oregon, Board Member
Publications & Presentations
Morgan, E., et al. (2023). "Restoration success of reintroduced Kincaid's lupine populations." Journal of Plant Conservation.
Morgan, E., & Johnson, T. (2021). "Seed dormancy patterns in Pacific Northwest prairie species." Restoration Ecology.
"Climate adaptation strategies for rare plant species," Annual Conference of the Society for Ecological Restoration, 2022.
Languages
English (Native)
Spanish (Intermediate)
References
Available upon request

View File

@ -38,6 +38,7 @@
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0",
"@types/plotly.js": "^2.35.5" "@types/plotly.js": "^2.35.5"
} }
}, },
@ -1974,12 +1975,35 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"peer": true "peer": true
}, },
"node_modules/@craco/craco": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-7.1.0.tgz",
"integrity": "sha512-oRAcPIKYrfPXp9rSzlsDNeOaVtDiKhoyqSXUoqiK24jCkHr4T8m/a2f74yXIzCbIheoUWDOIfWZyRgFgT+cpqA==",
"dev": true,
"dependencies": {
"autoprefixer": "^10.4.12",
"cosmiconfig": "^7.0.1",
"cosmiconfig-typescript-loader": "^1.0.0",
"cross-spawn": "^7.0.3",
"lodash": "^4.17.21",
"semver": "^7.3.7",
"webpack-merge": "^5.8.0"
},
"bin": {
"craco": "dist/bin/craco.js"
},
"engines": {
"node": ">=6"
},
"peerDependencies": {
"react-scripts": "^5.0.0"
}
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"optional": true, "devOptional": true,
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "0.3.9" "@jridgewell/trace-mapping": "0.3.9"
}, },
@ -1991,8 +2015,7 @@
"version": "0.3.9", "version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"optional": true, "devOptional": true,
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
@ -4406,29 +4429,25 @@
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/@tsconfig/node12": { "node_modules/@tsconfig/node12": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/@tsconfig/node14": { "node_modules/@tsconfig/node14": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/@tsconfig/node16": { "node_modules/@tsconfig/node16": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/@turf/area": { "node_modules/@turf/area": {
"version": "7.2.0", "version": "7.2.0",
@ -6799,6 +6818,20 @@
"wrap-ansi": "^7.0.0" "wrap-ansi": "^7.0.0"
} }
}, },
"node_modules/clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
"integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
"dev": true,
"dependencies": {
"is-plain-object": "^2.0.4",
"kind-of": "^6.0.2",
"shallow-clone": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -7246,6 +7279,25 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/cosmiconfig-typescript-loader": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.9.tgz",
"integrity": "sha512-tRuMRhxN4m1Y8hP9SNYfz7jRwt8lZdWxdjg/ohg5esKmsndJIn4yT96oJVcf5x0eA11taXl+sIp+ielu529k6g==",
"dev": true,
"dependencies": {
"cosmiconfig": "^7",
"ts-node": "^10.7.0"
},
"engines": {
"node": ">=12",
"npm": ">=6"
},
"peerDependencies": {
"@types/node": "*",
"cosmiconfig": ">=7",
"typescript": ">=3"
}
},
"node_modules/country-regex": { "node_modules/country-regex": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz",
@ -7256,8 +7308,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@ -8138,8 +8189,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"optional": true, "devOptional": true,
"peer": true,
"engines": { "engines": {
"node": ">=0.3.1" "node": ">=0.3.1"
} }
@ -9886,6 +9936,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
"dev": true,
"bin": {
"flat": "cli.js"
}
},
"node_modules/flat-cache": { "node_modules/flat-cache": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@ -11907,6 +11966,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"dev": true,
"dependencies": {
"isobject": "^3.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-potential-custom-element-name": { "node_modules/is-potential-custom-element-name": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@ -12104,6 +12175,15 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
}, },
"node_modules/isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@ -13527,8 +13607,7 @@
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/makeerror": { "node_modules/makeerror": {
"version": "1.0.12", "version": "1.0.12",
@ -18703,6 +18782,18 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
}, },
"node_modules/shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
"dev": true,
"dependencies": {
"kind-of": "^6.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shallow-copy": { "node_modules/shallow-copy": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz",
@ -20156,8 +20247,7 @@
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"optional": true, "devOptional": true,
"peer": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@ -20200,8 +20290,7 @@
"version": "8.3.4", "version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"optional": true, "devOptional": true,
"peer": true,
"dependencies": { "dependencies": {
"acorn": "^8.11.0" "acorn": "^8.11.0"
}, },
@ -20213,8 +20302,7 @@
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/tsconfig-paths": { "node_modules/tsconfig-paths": {
"version": "3.15.0", "version": "3.15.0",
@ -20737,8 +20825,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"optional": true, "devOptional": true
"peer": true
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
"version": "8.1.1", "version": "8.1.1",
@ -21081,6 +21168,20 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/webpack-merge": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
"integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
"dev": true,
"dependencies": {
"clone-deep": "^4.0.1",
"flat": "^5.0.2",
"wildcard": "^2.0.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/webpack-sources": { "node_modules/webpack-sources": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
@ -21267,6 +21368,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/wildcard": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"dev": true
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -21718,8 +21825,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"optional": true, "devOptional": true,
"peer": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }

View File

@ -56,7 +56,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/plotly.js": "^2.35.5", "@craco/craco": "^7.1.0",
"@craco/craco": "^0.0.0" "@types/plotly.js": "^2.35.5"
} }
} }

View File

@ -23,10 +23,10 @@ The backstory about Backstory...
## Some questions I've been asked ## Some questions I've been asked
Q. <ChatQuery text="Why aren't you providing this as a Platform As a Service (PaaS) application?"/> Q. <ChatQuery 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... 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 text="Why can't I just ask Backstory these questions?"/> Q. <ChatQuery prompt="Why can't I just ask Backstory these questions?" tunables={{ "enable_tools": false }} />
A. Try it. See what you find out :) A. Try it. See what you find out :)

View File

@ -19,11 +19,12 @@ import { SxProps } from '@mui/material';
import { ResumeBuilder } from './ResumeBuilder'; import { ResumeBuilder } from './ResumeBuilder';
import { Message, ChatQuery, MessageList } from './Message'; import { Message, MessageList } from './Message';
import { Snack, SeverityType } from './Snack'; import { Snack, SeverityType } from './Snack';
import { VectorVisualizer } from './VectorVisualizer'; import { VectorVisualizer } from './VectorVisualizer';
import { Controls } from './Controls'; import { Controls } from './Controls';
import { Conversation, ConversationHandle } from './Conversation'; import { Conversation, ConversationHandle } from './Conversation';
import { ChatQuery, QueryOptions } from './ChatQuery';
import { Scrollable } from './AutoScroll'; import { Scrollable } from './AutoScroll';
import { BackstoryTab } from './BackstoryTab'; import { BackstoryTab } from './BackstoryTab';
@ -112,9 +113,9 @@ const App = () => {
fetchAbout(); fetchAbout();
}, [about, setAbout]) }, [about, setAbout])
const handleSubmitChatQuery = (query: string) => { const handleSubmitChatQuery = (prompt: string, tunables?: QueryOptions) => {
console.log(`handleSubmitChatQuery: ${query} -- `, chatRef.current ? ' sending' : 'no handler'); console.log(`handleSubmitChatQuery: ${prompt} ${tunables || {}} -- `, chatRef.current ? ' sending' : 'no handler');
chatRef.current?.submitQuery(query); chatRef.current?.submitQuery(prompt, tunables);
setActiveTab(0); setActiveTab(0);
}; };
@ -137,10 +138,10 @@ const App = () => {
const backstoryQuestions = [ const backstoryQuestions = [
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}> <Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} /> <ChatQuery prompt="What is James Ketrenos' work history?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} /> <ChatQuery prompt="What programming languages has James used?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} /> <ChatQuery prompt="What are James' professional strengths?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} /> <ChatQuery prompt="What are today's headlines on CNBC.com?" tunables={{ enable_tools: true, enable_rag: false, enable_context: false }} submitQuery={handleSubmitChatQuery} />
</Box>, </Box>,
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<MuiMarkdown> <MuiMarkdown>
@ -255,11 +256,20 @@ const App = () => {
icon: <SettingsIcon /> icon: <SettingsIcon />
}, },
children: ( children: (
<Box className="ChatBox"> <Scrollable
autoscroll={false}
sx={{
maxWidth: "1024px",
height: "calc(100vh - 72px)",
flexDirection: "column",
margin: "0 auto",
p: 1,
}}
>
{sessionId !== undefined && {sessionId !== undefined &&
<Controls {...{ sessionId, setSnack, connectionBase }} /> <Controls {...{ sessionId, setSnack, connectionBase }} />
} }
</Box > </Scrollable>
) )
}]; }];
}, [about, connectionBase, sessionId, setSnack, isMobile]); }, [about, connectionBase, sessionId, setSnack, isMobile]);

View File

@ -6,8 +6,10 @@ import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails'; import AccordionDetails from '@mui/material/AccordionDetails';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
import { MessageRoles } from './Message'; import { MessageRoles } from './Message';
import { ErrorOutline, InfoOutline, Memory, Psychology, /* Stream, */ } from '@mui/icons-material';
interface ChatBubbleProps { interface ChatBubbleProps {
role: MessageRoles, role: MessageRoles,
@ -20,7 +22,7 @@ interface ChatBubbleProps {
} }
function ChatBubble(props: ChatBubbleProps) { function ChatBubble(props: ChatBubbleProps) {
const { role, isFullWidth, children, sx, className, title } = props; const { role, isFullWidth, children, sx, className, title }: ChatBubbleProps = props;
const theme = useTheme(); const theme = useTheme();
const defaultRadius = '16px'; const defaultRadius = '16px';
@ -116,6 +118,28 @@ function ChatBubble(props: ChatBubbleProps) {
lineHeight: '1.3', // More compact line height lineHeight: '1.3', // More compact line height
fontFamily: theme.typography.fontFamily, // Consistent font with your theme fontFamily: theme.typography.fontFamily, // Consistent font with your theme
}, },
'thinking': {
...defaultStyle
},
'streaming': {
...defaultStyle
},
'processing': {
...defaultStyle
},
};
styles["thinking"] = styles["status"]
styles["streaming"] = styles["assistant"]
styles["processing"] = styles["status"]
const icons: any = {
"searching": <Memory />,
"thinking": <Psychology />,
// "streaming": <Stream />,
"tooling": <LocationSearchingIcon />,
"processing": <LocationSearchingIcon />,
"error": <ErrorOutline color='error' />,
"info": <InfoOutline color='info' />,
}; };
if (role === 'content' && title) { if (role === 'content' && title) {
@ -139,8 +163,11 @@ function ChatBubble(props: ChatBubbleProps) {
} }
return ( return (
<Box className={className} sx={{ ...styles[role], ...sx }}> <Box className={className} sx={{ ...(styles[role] !== undefined ? styles[role] : styles["status"]), gap: 1, display: "flex", ...sx, flexDirection: "row" }}>
{children} {icons[role] !== undefined && icons[role]}
<Box sx={{ p: 0, m: 0, gap: 0, display: "flex", flexGrow: 1, flexDirection: "column" }}>
{children}
</Box>
</Box> </Box>
); );
} }

View File

@ -0,0 +1,48 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
type QueryOptions = {
enable_rag?: boolean,
enable_tools?: boolean,
enable_context?: boolean,
};
interface ChatQueryInterface {
prompt: string,
tunables?: QueryOptions,
submitQuery?: (prompt: string, tunables?: QueryOptions) => void
}
const ChatQuery = (props : ChatQueryInterface) => {
const { prompt, submitQuery } = props;
let tunables = props.tunables;
if (typeof (tunables) === "string") {
tunables = JSON.parse(tunables);
}
console.log(tunables);
if (submitQuery === undefined) {
return (<Box>{prompt}</Box>);
}
return (
<Button variant="outlined" sx={{
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
borderColor: theme => theme.palette.custom.highlight,
m: 1
}}
size="small" onClick={(e: any) => { submitQuery(prompt, tunables); }}>
{prompt}
</Button>
);
}
export type {
ChatQueryInterface,
QueryOptions,
};
export {
ChatQuery,
};

View File

@ -223,7 +223,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
}, [systemInfo, setSystemInfo, connectionBase, setSnack, sessionId]) }, [systemInfo, setSystemInfo, connectionBase, setSnack, sessionId])
useEffect(() => { useEffect(() => {
setEditSystemPrompt(systemPrompt); setEditSystemPrompt(systemPrompt.trim());
}, [systemPrompt, setEditSystemPrompt]); }, [systemPrompt, setEditSystemPrompt]);
const toggleRag = async (tool: Tool) => { const toggleRag = async (tool: Tool) => {
@ -314,39 +314,36 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
const handleKeyPress = (event: any) => { const handleKeyPress = (event: any) => {
if (event.key === 'Enter' && event.ctrlKey) { if (event.key === 'Enter' && event.ctrlKey) {
switch (event.target.id) { setSystemPrompt(editSystemPrompt);
case 'SystemPromptInput':
setSystemPrompt(editSystemPrompt);
break;
}
} }
}; };
return (<div className="Controls"> return (<div className="Controls">
<Typography component="span" sx={{ mb: 1 }}> {/* <Typography component="span" sx={{ mb: 1 }}>
You can change the information available to the LLM by adjusting the following settings: You can change the information available to the LLM by adjusting the following settings:
</Typography> </Typography>
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography component="span">System Prompt</Typography> <Typography component="span">System Prompt</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionActions style={{ flexDirection: "column" }}> <AccordionActions style={{ display: "flex", flexDirection: "column" }}>
<TextField <TextField
variant="outlined" variant="outlined"
fullWidth fullWidth
multiline multiline
slotProps={{
htmlInput: { style: { fontSize: "0.85rem", lineHeight: "1.25rem" } }
}}
type="text" type="text"
value={editSystemPrompt} value={editSystemPrompt}
onChange={(e) => setEditSystemPrompt(e.target.value)} onChange={(e) => setEditSystemPrompt(e.target.value)}
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder="Enter the new system prompt.." placeholder="Enter the new system prompt.."
id="SystemPromptInput"
/> />
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}> <Box sx={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
<Button variant="contained" disabled={editSystemPrompt === systemPrompt} onClick={() => { setSystemPrompt(editSystemPrompt); }}>Set</Button> <Button variant="contained" disabled={editSystemPrompt.trim() === systemPrompt.trim()} onClick={() => { setSystemPrompt(editSystemPrompt.trim()); }}>Set</Button>
<Button variant="outlined" onClick={() => { reset(["system_prompt"], "System prompt reset."); }} color="error">Reset</Button> <Button variant="outlined" onClick={() => { reset(["system_prompt"], "System prompt reset."); }} color="error">Reset</Button>
</div> </Box>
</AccordionActions> </AccordionActions>
</Accordion> </Accordion>
@ -417,7 +414,8 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
) )
}</FormGroup> }</FormGroup>
</AccordionActions> </AccordionActions>
</Accordion> </Accordion> */}
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography component="span">System Information</Typography> <Typography component="span">System Information</Typography>
@ -429,8 +427,9 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
<SystemInfoComponent systemInfo={systemInfo} /> <SystemInfoComponent systemInfo={systemInfo} />
</AccordionActions> </AccordionActions>
</Accordion> </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 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> */}
</div>); </div>);
} }

View File

@ -13,7 +13,7 @@ import { SetSnackType } from './Snack';
import { ContextStatus } from './ContextStatus'; import { ContextStatus } from './ContextStatus';
import { useAutoScrollToBottom } from './AutoScroll'; import { useAutoScrollToBottom } from './AutoScroll';
import { DeleteConfirmation } from './DeleteConfirmation'; import { DeleteConfirmation } from './DeleteConfirmation';
import { QueryOptions } from './ChatQuery';
import './Conversation.css'; import './Conversation.css';
const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." }; const loadingMessage: MessageData = { "role": "status", "content": "Establishing connection with server..." };
@ -21,7 +21,7 @@ const loadingMessage: MessageData = { "role": "status", "content": "Establishing
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check'; type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
interface ConversationHandle { interface ConversationHandle {
submitQuery: (query: string) => void; submitQuery: (prompt: string, options?: QueryOptions) => void;
} }
interface BackstoryMessage { interface BackstoryMessage {
@ -54,6 +54,7 @@ interface ConversationProps {
setSnack: SetSnackType, // Callback to display snack popups setSnack: SetSnackType, // Callback to display snack popups
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
defaultQuery?: string, // Default text to populate the TextField input defaultQuery?: string, // Default text to populate the TextField input
emptyPrompt?: string, // If input is not shown and an action is taken, send this prompt
preamble?: MessageList, // Messages to display at start of Conversation until Action has been invoked preamble?: MessageList, // Messages to display at start of Conversation until Action has been invoked
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked
@ -67,6 +68,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
className, className,
type, type,
prompt, prompt,
emptyPrompt,
actionLabel, actionLabel,
resetAction, resetAction,
multiline, multiline,
@ -201,17 +203,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
// isProcessing?: boolean, // isProcessing?: boolean,
// metadata?: MessageMetaData // metadata?: MessageMetaData
// }; // };
setConversation(backstoryMessages.flatMap((message: BackstoryMessage) => [{ setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => [{
role: 'user', role: 'user',
content: message.prompt || "", content: backstoryMessage.prompt || "",
}, { }, {
...backstoryMessage,
role: 'assistant', role: 'assistant',
prompt: message.prompt || "", content: backstoryMessage.response || "",
preamble: message.preamble || {},
full_content: message.full_content || "",
content: message.response || "",
metadata: message.metadata,
actions: message.actions,
}] as MessageList)); }] as MessageList));
setNoInteractions(false); setNoInteractions(false);
} }
@ -260,8 +258,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
}; };
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submitQuery: (query: string) => { submitQuery: (query: string, tunables?: QueryOptions) => {
sendQuery(query); sendQuery(query, tunables);
} }
})); }));
@ -307,38 +305,34 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
} }
}; };
const sendQuery = async (query: string) => { const sendQuery = async (request: string, options?: QueryOptions) => {
query = query.trim(); request = request.trim();
// If the query was empty, a default query was provided, // If the query was empty, a default query was provided,
// and there is no prompt for the user, send the default query. // and there is no prompt for the user, send the default query.
if (!query && defaultQuery && !prompt) { if (!request && defaultQuery && !prompt) {
query = defaultQuery.trim(); request = defaultQuery.trim();
} }
// If the query is empty, and a prompt was provided, do not // Do not send an empty query.
// send an empty query. if (!request) {
if (!query && prompt) {
return; return;
} }
setNoInteractions(false); setNoInteractions(false);
if (query) { setConversation([
setConversation([ ...conversationRef.current,
...conversationRef.current, {
{ role: 'user',
role: 'user', origin: type,
origin: type, content: request,
content: query, disableCopy: true
disableCopy: true }
} ]);
]);
}
// Add a small delay to ensure React has time to update the UI // Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
console.log(conversation);
// Clear input // Clear input
setQuery(''); setQuery('');
@ -357,13 +351,25 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
// Make the fetch request with proper headers // 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/chat/${sessionId}/${type}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
}, },
body: JSON.stringify({ role: 'user', content: query.trim() }), body: JSON.stringify(query)
}); });
// We'll guess that the response will be around 500 tokens... // We'll guess that the response will be around 500 tokens...
@ -387,39 +393,23 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
let buffer = ''; let buffer = '';
const process_line = async (line: string) => { const process_line = async (line: string) => {
const update = JSON.parse(line); let update = JSON.parse(line);
switch (update.status) { switch (update.status) {
case 'searching':
case 'processing':
case 'thinking':
// Force an immediate state update based on the message type
// Update processing message with immediate re-render
setProcessingMessage({ role: 'status', content: update.response });
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
break;
case 'done': case 'done':
console.log('Done processing:', update); console.log('Done processing:', update);
// Replace processing message with final result // Replace processing message with final result
if (onResponse) { if (onResponse) {
update.message = onResponse(update); update = onResponse(update);
} }
setProcessingMessage(undefined); setProcessingMessage(undefined);
const backstoryMessage: BackstoryMessage = update; const backstoryMessage: BackstoryMessage = update;
setConversation([ setConversation([
...conversationRef.current, { ...conversationRef.current, {
// role: 'user', ...backstoryMessage,
// content: backstoryMessage.prompt || "",
// }, {
role: 'assistant', role: 'assistant',
origin: type, origin: type,
content: backstoryMessage.response || "", content: backstoryMessage.response || "",
prompt: backstoryMessage.prompt || "",
preamble: backstoryMessage.preamble || {},
full_content: backstoryMessage.full_content || "",
metadata: backstoryMessage.metadata,
actions: backstoryMessage.actions,
}] as MessageList); }] as MessageList);
// Add a small delay to ensure React has time to update the UI // Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
@ -443,6 +433,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
// Add a small delay to ensure React has time to update the UI // Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
break; break;
default:
// Force an immediate state update based on the message type
// Update processing message with immediate re-render
setProcessingMessage({ role: update.status, content: update.response });
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
break;
} }
} }
@ -464,6 +461,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
await process_line(line); await process_line(line);
} catch (e) { } catch (e) {
setSnack("Error processing query", "error") setSnack("Error processing query", "error")
console.error(e);
} }
} }
} }
@ -474,6 +472,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
await process_line(buffer); await process_line(buffer);
} catch (e) { } catch (e) {
setSnack("Error processing query", "error") setSnack("Error processing query", "error")
console.error(e);
} }
} }
@ -516,7 +515,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
mb: 1, m: 1,
}}> }}>
<PropagateLoader <PropagateLoader
size="10px" size="10px"
@ -592,7 +591,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
export type { export type {
ConversationProps, ConversationProps,
ConversationHandle ConversationHandle,
}; };
export { export {

View File

@ -28,12 +28,11 @@ import { VectorVisualizer } from './VectorVisualizer';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
type MessageRoles = 'info' | 'user' | 'assistant' | 'system' | 'status' | 'error' | 'content'; type MessageRoles = 'info' | 'user' | 'assistant' | 'system' | 'status' | 'error' | 'content' | 'thinking' | 'processing';
type MessageData = { type MessageData = {
role: MessageRoles, role: MessageRoles,
content: string, content: string,
full_content?: string,
disableCopy?: boolean, disableCopy?: boolean,
user?: string, user?: string,
@ -52,7 +51,9 @@ interface MessageMetaData {
}, },
origin: string, origin: string,
rag: any, rag: any,
tools: any[], tools?: {
tool_calls: any[],
},
eval_count: number, eval_count: number,
eval_duration: number, eval_duration: number,
prompt_eval_count: number, prompt_eval_count: number,
@ -75,11 +76,6 @@ interface MessageProps {
className?: string, className?: string,
}; };
interface ChatQueryInterface {
text: string,
submitQuery?: (text: string) => void
}
interface MessageMetaProps { interface MessageMetaProps {
metadata: MessageMetaData, metadata: MessageMetaData,
messageProps: MessageProps messageProps: MessageProps
@ -99,59 +95,49 @@ const MessageMeta = (props: MessageMetaProps) => {
const message = props.messageProps.message; const message = props.messageProps.message;
return (<> return (<>
<Box sx={{ fontSize: "0.8rem", mb: 1 }}>
Below is the LLM performance of this query. Note that if tools are called, the
entire context is processed for each separate tool request by the LLM. This
can dramatically increase the total time for a response.
</Box>
<TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
<Table aria-label="prompt stats" size="small">
<TableHead>
<TableRow>
<TableCell></TableCell>
<TableCell align="right" >Tokens</TableCell>
<TableCell align="right">Time (s)</TableCell>
<TableCell align="right">TPS</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow key="prompt" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">Prompt</TableCell>
<TableCell align="right">{prompt_eval_count}</TableCell>
<TableCell align="right">{Math.round(prompt_eval_duration / 10 ** 7) / 100}</TableCell>
<TableCell align="right">{Math.round(prompt_eval_count * 10 ** 9 / prompt_eval_duration)}</TableCell>
</TableRow>
<TableRow key="response" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">Response</TableCell>
<TableCell align="right">{eval_count}</TableCell>
<TableCell align="right">{Math.round(eval_duration / 10 ** 7) / 100}</TableCell>
<TableCell align="right">{Math.round(eval_count * 10 ** 9 / eval_duration)}</TableCell>
</TableRow>
<TableRow key="total" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">Total</TableCell>
<TableCell align="right">{prompt_eval_count + eval_count}</TableCell>
<TableCell align="right">{Math.round((prompt_eval_duration + eval_duration) / 10 ** 7) / 100}</TableCell>
<TableCell align="right">{Math.round((prompt_eval_count + eval_count) * 10 ** 9 / (prompt_eval_duration + eval_duration))}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
{ {
message.full_content !== undefined && prompt_eval_duration !== 0 && eval_duration !== 0 && <>
<Accordion> <Box sx={{ fontSize: "0.8rem", mb: 1 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> Below is the LLM performance of this query. Note that if tools are called, the
<Box sx={{ fontSize: "0.8rem" }}> entire context is processed for each separate tool request by the LLM. This
Full Query can dramatically increase the total time for a response.
</Box> </Box>
</AccordionSummary> <TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
<AccordionDetails> <Table aria-label="prompt stats" size="small">
<pre style={{ "display": "block", "position": "relative" }}><CopyBubble content={message.full_content?.trim()} />{message.full_content?.trim()}</pre> <TableHead>
</AccordionDetails> <TableRow>
</Accordion> <TableCell></TableCell>
<TableCell align="right" >Tokens</TableCell>
<TableCell align="right">Time (s)</TableCell>
<TableCell align="right">TPS</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow key="prompt" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">Prompt</TableCell>
<TableCell align="right">{prompt_eval_count}</TableCell>
<TableCell align="right">{Math.round(prompt_eval_duration / 10 ** 7) / 100}</TableCell>
<TableCell align="right">{Math.round(prompt_eval_count * 10 ** 9 / prompt_eval_duration)}</TableCell>
</TableRow>
<TableRow key="response" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">Response</TableCell>
<TableCell align="right">{eval_count}</TableCell>
<TableCell align="right">{Math.round(eval_duration / 10 ** 7) / 100}</TableCell>
<TableCell align="right">{Math.round(eval_count * 10 ** 9 / eval_duration)}</TableCell>
</TableRow>
<TableRow key="total" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">Total</TableCell>
<TableCell align="right">{prompt_eval_count + eval_count}</TableCell>
<TableCell align="right">{Math.round((prompt_eval_duration + eval_duration) / 10 ** 7) / 100}</TableCell>
<TableCell align="right">{Math.round((prompt_eval_count + eval_count) * 10 ** 9 / (prompt_eval_duration + eval_duration))}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</>
} }
{ {
tools !== undefined && tools.length !== 0 && tools !== undefined && tools.tool_calls && tools.tool_calls.length !== 0 &&
<Accordion sx={{ boxSizing: "border-box" }}> <Accordion sx={{ boxSizing: "border-box" }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: "0.8rem" }}>
@ -159,26 +145,24 @@ const MessageMeta = (props: MessageMetaProps) => {
</Box> </Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{tools.map((tool: any, index: number) => <Box key={index}> {
{index !== 0 && <Divider />} tools.tool_calls.map((tool: any, index: number) =>
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 0.5 }}> <Box key={index} sx={{ m: 0, p: 1, pt: 0, display: "flex", flexDirection: "column", border: "1px solid #e0e0e0" }}>
<div style={{ display: "flex", paddingRight: "1rem", whiteSpace: "nowrap" }}> {index !== 0 && <Divider />}
{tool.tool} <Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 1, mb: 1, fontWeight: "bold" }}>
</div> {tool.name}
<div style={{ </Box>
display: "flex", <JsonView displayDataTypes={false} objectSortKeys={true} collapsed={1} value={JSON.parse(tool.content)} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }}>
padding: "3px", <JsonView.String
whiteSpace: "pre-wrap", render={({ children, ...reset }) => {
flexGrow: 1, if (typeof (children) === "string" && children.match("\n")) {
border: "1px solid #E0E0E0", return <pre {...reset} style={{ display: "flex", border: "none", ...reset.style }}>{children}</pre>
wordBreak: "break-all", }
maxHeight: "5rem", }}
overflow: "auto" />
}}> </JsonView>
{JSON.stringify(tool.result, null, 2)} </Box>)
</div> }
</Box>
</Box>)}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
} }
@ -216,46 +200,24 @@ const MessageMeta = (props: MessageMetaProps) => {
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: "0.8rem" }}>
All response fields Full Response Details
</Box> </Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{Object.entries(message) <JsonView displayDataTypes={false} objectSortKeys={true} collapsed={1} value={message} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }}>
.filter(([key, value]) => key !== undefined && value !== undefined) <JsonView.String
.map(([key, value]) => (typeof (value) !== "string" || value?.trim() !== "") && render={({ children, ...reset }) => {
<Accordion key={key}> if (typeof (children) === "string" && children.match("\n")) {
<AccordionSummary sx={{ fontSize: "1rem", fontWeight: "bold" }} expandIcon={<ExpandMoreIcon />}> return <pre {...reset} style={{ display: "inline", border: "none", ...reset.style }}>{children.trim()}</pre>
{key} }
</AccordionSummary> }}
<AccordionDetails> />
{typeof (value) === "string" ? </JsonView>
<pre>{value}</pre> :
<JsonView collapsed={1} value={value as any} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }} />
}
</AccordionDetails>
</Accordion>
)}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
</>); </>);
}; };
const ChatQuery = ({ text, submitQuery }: ChatQueryInterface) => {
if (submitQuery === undefined) {
return (<Box>{text}</Box>);
}
return (
<Button variant="outlined" sx={{
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
borderColor: theme => theme.palette.custom.highlight,
m: 1
}}
size="small" onClick={(e: any) => { submitQuery(text); }}>
{text}
</Button>
);
}
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, submitQuery, isFullWidth, sx, className } = props; const { message, submitQuery, isFullWidth, sx, className } = props;
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
@ -340,14 +302,12 @@ const Message = (props: MessageProps) => {
export type { export type {
MessageProps, MessageProps,
MessageList, MessageList,
ChatQueryInterface,
MessageData, MessageData,
MessageRoles MessageRoles
}; };
export { export {
Message, Message,
ChatQuery,
MessageMeta MessageMeta
}; };

View File

@ -18,7 +18,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { ChatQuery } from './Message'; import { ChatQuery } from './ChatQuery';
import { MessageList, MessageData } from './Message'; import { MessageList, MessageData } from './Message';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
import { Conversation } from './Conversation'; import { Conversation } from './Conversation';
@ -97,6 +97,12 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
if (messages === undefined || messages.length === 0) { if (messages === undefined || messages.length === 0) {
return []; return [];
} }
console.log("filterJobDescriptionMessages disabled")
if (messages.length > 1) {
setHasResume(true);
}
return messages;
let reduced = messages.filter((m, i) => { let reduced = messages.filter((m, i) => {
const keep = (m.metadata?.origin || m.origin || "no origin") === 'job_description'; const keep = (m.metadata?.origin || m.origin || "no origin") === 'job_description';
@ -135,6 +141,11 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
if (messages === undefined || messages.length === 0) { if (messages === undefined || messages.length === 0) {
return []; return [];
} }
console.log("filterResumeMessages disabled")
if (messages.length > 3) {
setHasFacts(true);
}
return messages;
let reduced = messages.filter((m, i) => { let reduced = messages.filter((m, i) => {
const keep = (m.metadata?.origin || m.origin || "no origin") === 'resume'; const keep = (m.metadata?.origin || m.origin || "no origin") === 'resume';
@ -182,6 +193,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
if (messages === undefined || messages.length === 0) { if (messages === undefined || messages.length === 0) {
return []; return [];
} }
console.log("filterFactsMessages disabled")
return messages;
// messages.forEach((m, i) => console.log(`filterFactsMessages: ${i + 1}:`, m)) // messages.forEach((m, i) => console.log(`filterFactsMessages: ${i + 1}:`, m))
const reduced = messages.filter(m => { const reduced = messages.filter(m => {
@ -240,8 +254,8 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
console.log('renderJobDescriptionView'); console.log('renderJobDescriptionView');
const jobDescriptionQuestions = [ const jobDescriptionQuestions = [
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}> <Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
<ChatQuery text="What are the key skills necessary for this position?" submitQuery={handleJobQuery} /> <ChatQuery prompt="What are the key skills necessary for this position?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
<ChatQuery text="How much should this position pay (accounting for inflation)?" submitQuery={handleJobQuery} /> <ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
</Box>, </Box>,
]; ];
@ -289,8 +303,8 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
const renderResumeView = useCallback((small: boolean) => { const renderResumeView = useCallback((small: boolean) => {
const resumeQuestions = [ const resumeQuestions = [
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}> <Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
<ChatQuery text="Is this resume a good fit for the provided job description?" submitQuery={handleResumeQuery} /> <ChatQuery prompt="Is this resume a good fit for the provided job description?" tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
<ChatQuery text="Provide a more concise resume." submitQuery={handleResumeQuery} /> <ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
</Box>, </Box>,
]; ];
@ -298,9 +312,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
return <Conversation return <Conversation
ref={resumeConversationRef} ref={resumeConversationRef}
{...{ {...{
actionLabel: "Fact Check",
multiline: true,
type: "resume", type: "resume",
actionLabel: "Fact Check",
defaultQuery: "Fact check the resume.",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`, resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterResumeMessages, messageFilter: filterResumeMessages,
onResponse: resumeResponse, onResponse: resumeResponse,
@ -319,12 +333,12 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
prompt: "Ask a question about this job resume...", prompt: "Ask a question about this job resume...",
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`, resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
messageFilter: filterResumeMessages, messageFilter: filterResumeMessages,
defaultPrompts: resumeQuestions,
resetAction: resetResume,
onResponse: resumeResponse, onResponse: resumeResponse,
resetAction: resetResume,
sessionId, sessionId,
connectionBase, connectionBase,
setSnack, setSnack,
defaultPrompts: resumeQuestions,
}} }}
/> />
} }
@ -336,7 +350,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
const renderFactCheckView = useCallback((small: boolean) => { const renderFactCheckView = useCallback((small: boolean) => {
const factsQuestions = [ const factsQuestions = [
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}> <Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
<ChatQuery text="Rewrite the resume to address any discrepancies." submitQuery={handleFactsQuery} /> <ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
</Box>, </Box>,
]; ];

View File

@ -2,12 +2,12 @@ import React from 'react';
import { MuiMarkdown } from 'mui-markdown'; import { MuiMarkdown } from 'mui-markdown';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { Link } from '@mui/material'; import { Link } from '@mui/material';
import { ChatQuery } from './Message'; import { ChatQuery, QueryOptions } from './ChatQuery';
interface StyledMarkdownProps { interface StyledMarkdownProps {
className?: string, className?: string,
content: string, content: string,
submitQuery?: (query: string) => void, submitQuery?: (prompt: string, tunables?: QueryOptions) => void,
[key: string]: any, // For any additional props [key: string]: any, // For any additional props
}; };
@ -38,7 +38,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = ({ className, content, sub
options.overrides.ChatQuery = { options.overrides.ChatQuery = {
component: ChatQuery, component: ChatQuery,
props: { props: {
submitQuery submitQuery,
}, },
}; };
} }

View File

@ -158,7 +158,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
useEffect(() => { useEffect(() => {
if (!result || !result.embeddings) return; if (!result || !result.embeddings) return;
if (result.embeddings.length === 0) return; if (result.embeddings.length === 0) return;
console.log('Result:', result);
const vectors: (number[])[] = [...result.embeddings]; const vectors: (number[])[] = [...result.embeddings];
const documents = [...result.documents || []]; const documents = [...result.documents || []];
const metadatas = [...result.metadatas || []]; const metadatas = [...result.metadatas || []];

View File

@ -1,6 +1,6 @@
from utils import logger from utils import logger
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar from typing import AsyncGenerator
# %% # %%
# Imports [standard] # Imports [standard]
@ -17,6 +17,10 @@ import re
import math import math
import warnings import warnings
from typing import Any from typing import Any
from collections import deque
from datetime import datetime
from uuid import uuid4
def try_import(module_name, pip_name=None): def try_import(module_name, pip_name=None):
try: try:
@ -28,7 +32,6 @@ def try_import(module_name, pip_name=None):
# Third-party modules with import checks # Third-party modules with import checks
try_import("ollama") try_import("ollama")
try_import("requests") try_import("requests")
try_import("bs4", "beautifulsoup4")
try_import("fastapi") try_import("fastapi")
try_import("uvicorn") try_import("uvicorn")
try_import("numpy") try_import("numpy")
@ -37,29 +40,23 @@ try_import("sklearn")
import ollama import ollama
import requests import requests
from bs4 import BeautifulSoup
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, BackgroundTasks from fastapi import FastAPI, Request, BackgroundTasks # type: ignore
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, RedirectResponse from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, RedirectResponse # type: ignore
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware # type: ignore
import uvicorn import uvicorn # type: ignore
import numpy as np import numpy as np # type: ignore
import umap import umap # type: ignore
from sklearn.preprocessing import MinMaxScaler from sklearn.preprocessing import MinMaxScaler # type: ignore
from utils import ( from utils import (
rag as Rag, rag as Rag,
tools as Tools,
Context, Conversation, Message, Context, Conversation, Message,
Agent, Agent,
Tunables,
defines, defines,
logger logger,
)
from tools import (
DateTime,
WeatherForecast,
TickerValue,
tools
) )
CONTEXT_VERSION=2 CONTEXT_VERSION=2
@ -69,71 +66,25 @@ rags = [
# { "name": "LKML", "enabled": False, "description": "Full associative data for entire LKML mailing list archive." }, # { "name": "LKML", "enabled": False, "description": "Full associative data for entire LKML mailing list archive." },
] ]
system_message = f""" system_message_old = f"""
Launched on {DateTime()}. Launched on {datetime.now().isoformat()}.
When answering queries, follow these steps: When answering queries, follow these steps:
- First analyze the query to determine if real-time information might be helpful 1. First analyze the query to determine if real-time information might be helpful
- Even when <|context|> is provided, consider whether the tools would provide more current or comprehensive information 2. Even when <|context|> is provided, consider whether the tools would provide more current or comprehensive information
- Use the provided tools whenever they would enhance your response, regardless of whether context is also available 3. Use the provided tools whenever they would enhance your response, regardless of whether context is also available
- 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 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: ❄️
- When both <|context|> and tool outputs are relevant, synthesize information from both sources to provide the most complete answer 4. When both <|context|> and tool outputs are relevant, synthesize information from both sources to provide the most complete answer
- Always prioritize the most up-to-date and relevant information, whether it comes from <|context|> or tools 5. Always prioritize the most up-to-date and relevant information, whether it comes from <|context|> or tools
- If <|context|> and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data 6. If <|context|> and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
- If there is information in the <|context|>, <|job_description|>, or <|context|> sections to enhance the answer, incorporate it seamlessly and refer to it as 'the latest information' or 'recent data' instead of mentioning '<|context|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, or <|context|> tags.
Always use tools and <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so. Always use tools and <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so.
"""
system_generate_resume = f"""
Launched on {DateTime()}.
You are a professional resume writer. Your task is to write a concise, polished, and tailored resume for a specific job based only on the individual's <|context|>.
When answering queries, follow these steps:
- You must not invent or assume any inforation not explicitly present in the <|context|>.
- Analyze the <|job_description|> to identify skills required for the job.
- Use the <|job_description|> provided to guide the focus, tone, and relevant skills or experience to highlight from the <|context|>.
- Identify and emphasize the experiences, achievements, and responsibilities from the <|context|> that best align with the <|job_description|>.
- Only provide information from <|context|> items if it is relevant to the <|job_description|>.
- Do not use the <|job_description|> skills unless listed in <|context|>.
- Do not include any information unless it is provided in <|context|>.
- Use the <|context|> to create a polished, professional resume.
- Do not list any locations or mailing addresses in the resume.
- If there is information in the <|context|>, <|job_description|>, <|context|>, or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|job_description|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, or <|context|> tags.
- Ensure the langauge is clear, concise, and aligned with industry standards for professional resumes.
Structure the resume professionally with the following sections where applicable:
* Name: Use full name
* Professional Summary: A 2-4 sentence overview tailored to the job.
* Skills: A bullet list of key skills derived from the work history and relevant to the job.
* Professional Experience: A detailed list of roles, achievements, and responsibilities from <|context|> that relate to the <|job_description|>.
* Education: Include only if available in the work history.
* Notes: Indicate the initial draft of the resume was generated using the Backstory application.
""".strip() """.strip()
system_fact_check = f"""
Launched on {DateTime()}.
You are a professional resume fact checker. Your task is to identify any inaccuracies in the <|resume|> based on the individual's <|context|>.
If there are inaccuracies, list them in a bullet point format.
When answering queries, follow these steps:
- You must not invent or assume any information not explicitly present in the <|context|>.
- Analyze the <|resume|> to identify any discrepancies or inaccuracies based on the <|context|>.
- If there is information in the <|context|>, <|job_description|>, <|context|>, or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|job_description|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, <|resume|>, or <|context|> tags.
""".strip()
system_fact_check_QA = f""" system_fact_check_QA = f"""
Launched on {DateTime()}. Launched on {datetime.now().isoformat()}.
You are a professional resume fact checker. You are a professional resume fact checker.
@ -142,18 +93,6 @@ You are provided with a <|resume|> which was generated by you, the <|context|> y
Your task is to answer questions about the <|fact_check|> you generated based on the <|resume|> and <|context>. Your task is to answer questions about the <|fact_check|> you generated based on the <|resume|> and <|context>.
""" """
system_job_description = f"""
Launched on {DateTime()}.
You are a hiring and job placing specialist. Your task is to answers about a job description.
When answering queries, follow these steps:
- Analyze the <|job_description|> to provide insights for the asked question.
- If any financial information is requested, be sure to account for inflation.
- If there is information in the <|context|>, <|job_description|>, <|context|>, or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|job_description|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, <|resume|>, or <|context|> tags.
""".strip()
def get_installed_ram(): def get_installed_ram():
try: try:
with open("/proc/meminfo", "r") as f: with open("/proc/meminfo", "r") as f:
@ -261,66 +200,6 @@ def parse_args():
# %% # %%
async def AnalyzeSite(llm, model: str, url : str, question : str):
"""
Fetches content from a URL, extracts the text, and uses Ollama to summarize it.
Args:
url (str): The URL of the website to summarize
Returns:
str: A summary of the website content
"""
try:
# Fetch the webpage
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
logger.info(f"Fetching {url}")
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
logger.info(f"{url} returned. Processing...")
# Parse the HTML
soup = BeautifulSoup(response.text, "html.parser")
# Remove script and style elements
for script in soup(["script", "style"]):
script.extract()
# Get text content
text = soup.get_text(separator=" ", strip=True)
# Clean up text (remove extra whitespace)
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = " ".join(chunk for chunk in chunks if chunk)
# Limit text length if needed (Ollama may have token limits)
max_chars = 100000
if len(text) > max_chars:
text = text[:max_chars] + "..."
# Create Ollama client
# logger.info(f"Requesting summary of: {text}")
# Generate summary using Ollama
prompt = f"CONTENTS:\n\n{text}\n\n{question}"
response = llm.generate(model=model,
system="You are given the contents of {url}. Answer the question about the contents",
prompt=prompt)
#logger.info(response["response"])
return {
"source": "summarizer-llm",
"content": response["response"],
"metadata": DateTime()
}
except requests.exceptions.RequestException as e:
return f"Error fetching the URL: {str(e)}"
except Exception as e:
return f"Error processing the website content: {str(e)}"
# %% # %%
@ -332,14 +211,7 @@ def is_valid_uuid(value):
except (ValueError, TypeError): except (ValueError, TypeError):
return False return False
def default_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [{**tool, "enabled": True} for tool in tools]
def find_summarize_tool(tools):
return [{**tool, "enabled": True} for tool in tools if tool.get("name", "") == "AnalyzeSite"]
def llm_tools(tools):
return [tool for tool in tools if tool.get("enabled", False) == True]
@ -496,25 +368,27 @@ class WebServer:
match reset_operation: match reset_operation:
case "system_prompt": case "system_prompt":
logger.info(f"Resetting {reset_operation}") logger.info(f"Resetting {reset_operation}")
match agent_type: # match agent_type:
case "chat": # case "chat":
prompt = system_message # prompt = system_message
case "job_description": # case "job_description":
prompt = system_generate_resume # prompt = system_generate_resume
case "resume": # case "resume":
prompt = system_generate_resume # prompt = system_generate_resume
case "fact_check": # case "fact_check":
prompt = system_message # prompt = system_message
# case _:
# prompt = system_message
agent.system_prompt = prompt # agent.system_prompt = prompt
response["system_prompt"] = { "system_prompt": prompt } # response["system_prompt"] = { "system_prompt": prompt }
case "rags": case "rags":
logger.info(f"Resetting {reset_operation}") logger.info(f"Resetting {reset_operation}")
context.rags = rags.copy() context.rags = rags.copy()
response["rags"] = context.rags response["rags"] = context.rags
case "tools": case "tools":
logger.info(f"Resetting {reset_operation}") logger.info(f"Resetting {reset_operation}")
context.tools = default_tools(tools) context.tools = Tools.enabled_tools(Tools.tools)
response["tools"] = context.tools response["tools"] = context.tools
case "history": case "history":
reset_map = { reset_map = {
@ -633,32 +507,69 @@ class WebServer:
@self.app.post("/api/chat/{context_id}/{agent_type}") @self.app.post("/api/chat/{context_id}/{agent_type}")
async def post_chat_endpoint(context_id: str, agent_type: str, request: Request): async def post_chat_endpoint(context_id: str, agent_type: str, request: Request):
logger.info(f"{request.method} {request.url.path}") logger.info(f"{request.method} {request.url.path}")
if not is_valid_uuid(context_id):
logger.warning(f"Invalid context_id: {context_id}")
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
try: try:
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) context = self.upsert_context(context_id)
try: try:
data = await request.json()
agent = context.get_agent(agent_type) agent = context.get_agent(agent_type)
if not agent and agent_type == "job_description":
logger.info(f"Agent {agent_type} not found. Returning empty history.")
# Create a new agent if it doesn't exist
agent = context.get_or_create_agent("job_description", system_prompt=system_generate_resume, job_description=data["content"])
except Exception as e: except Exception as e:
logger.info(f"Attempt to create agent type: {agent_type} failed", e) logger.info(f"Attempt to create agent type: {agent_type} failed", e)
return JSONResponse({ "error": f"{agent_type} is not recognized", "context": context.id }, status_code=404) return JSONResponse({ "error": f"{agent_type} is not recognized", "context": context.id }, status_code=404)
query = await request.json()
prompt = query["prompt"]
if not isinstance(prompt, str) or len(prompt) == 0:
logger.info(f"Prompt is empty")
return JSONResponse({"error": "Prompt can not be empty"}, status_code=400)
try:
options = Tunables(**query["options"]) if "options" in query else None
except Exception as e:
logger.info(f"Attempt to set tunables failed: {query['options']}.", e)
return JSONResponse({"error": f"Invalid options: {query['options']}"}, status_code=400)
if not agent:
# job_description is the only agent that is dynamically generated from a
# Rest API endpoint.
# - 'chat' is created on context creation.
# - 'resume' is created on actions by 'job_description'
# - 'fact_check' is created on ations by 'fact_check'
match agent_type:
case "job_description":
logger.info(f"Agent {agent_type} not found. Returning empty history.")
agent = context.get_or_create_agent("job_description", job_description=prompt)
case _:
logger.info(f"Invalid agent creation sequence for {agent_type}. Returning error.")
return JSONResponse({ "error": f"{agent_type} is not recognized", "context": context.id }, status_code=404)
# Create a custom generator that ensures flushing # Create a custom generator that ensures flushing
async def flush_generator(): async def flush_generator():
async for message in self.generate_response(context=context, agent=agent, content=data["content"]): logging.info(f"Message starting. Streaming partial results.")
async for message in self.generate_response(context=context, agent=agent, prompt=prompt, options=options):
if message.status != "done":
result = {
"status": message.status,
"response": message.response
}
else:
logging.info(f"Message complete. Providing full response.")
try:
result = message.model_dump(by_alias=True, mode='json')
except Exception as e:
result = { "status": "error", "response": e }
exit(1)
# Convert to JSON and add newline # Convert to JSON and add newline
yield json.dumps(message.model_dump(mode='json')) + "\n" result = json.dumps(result) + "\n"
# Save the history as its generated message.network_packets += 1
self.save_context(context_id) message.network_bytes += len(result)
yield result
# Explicitly flush after each yield # Explicitly flush after each yield
await asyncio.sleep(0) # Allow the event loop to process the write await asyncio.sleep(0) # Allow the event loop to process the write
# Save the history once completed
self.save_context(context_id)
# Return StreamingResponse with appropriate headers # Return StreamingResponse with appropriate headers
return StreamingResponse( return StreamingResponse(
@ -676,9 +587,15 @@ class WebServer:
@self.app.post("/api/context") @self.app.post("/api/context")
async def create_context(): async def create_context():
context = self.create_context() try:
logger.info(f"Generated new agent as {context.id}") context = self.create_context()
return JSONResponse({ "id": context.id }) logger.info(f"Generated new agent as {context.id}")
return JSONResponse({ "id": context.id })
except Exception as e:
logger.error(f"get_history error: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return JSONResponse({"error": str(e)}, status_code=404)
@self.app.get("/api/history/{context_id}/{agent_type}") @self.app.get("/api/history/{context_id}/{agent_type}")
async def get_history(context_id: str, agent_type: str, request: Request): async def get_history(context_id: str, agent_type: str, request: Request):
@ -689,7 +606,7 @@ class WebServer:
if not agent: if not agent:
logger.info(f"Agent {agent_type} not found. Returning empty history.") logger.info(f"Agent {agent_type} not found. Returning empty history.")
return JSONResponse({ "messages": [] }) return JSONResponse({ "messages": [] })
logger.info(f"History for {agent_type} contains {len(agent.conversation.messages)} entries.") logger.info(f"History for {agent_type} contains {len(agent.conversation)} entries.")
return agent.conversation return agent.conversation
except Exception as e: except Exception as e:
logger.error(f"get_history error: {str(e)}") logger.error(f"get_history error: {str(e)}")
@ -771,7 +688,7 @@ class WebServer:
# Serialize the data to JSON and write to file # Serialize the data to JSON and write to file
with open(file_path, "w") as f: with open(file_path, "w") as f:
f.write(context.model_dump_json()) f.write(context.model_dump_json(by_alias=True))
return context_id return context_id
@ -797,9 +714,9 @@ class WebServer:
with open(file_path, "r") as f: with open(file_path, "r") as f:
content = f.read() content = f.read()
logger.info(f"Loading context from {file_path}, content length: {len(content)}") logger.info(f"Loading context from {file_path}, content length: {len(content)}")
import json
try: try:
# Try parsing as JSON first to ensure valid JSON # Try parsing as JSON first to ensure valid JSON
import json
json_data = json.loads(content) json_data = json.loads(content)
logger.info("JSON parsed successfully, attempting model validation") logger.info("JSON parsed successfully, attempting model validation")
@ -829,19 +746,19 @@ class WebServer:
""" """
if not self.file_watcher: if not self.file_watcher:
raise Exception("File watcher not initialized") raise Exception("File watcher not initialized")
if not context_id:
context_id = str(uuid4())
logger.info(f"Creating new context with ID: {context_id}") logger.info(f"Creating new context with ID: {context_id}")
context = Context(id=context_id, file_watcher=self.file_watcher) context = Context(id=context_id, file_watcher=self.file_watcher)
if os.path.exists(defines.resume_doc): if os.path.exists(defines.resume_doc):
context.user_resume = open(defines.resume_doc, "r").read() context.user_resume = open(defines.resume_doc, "r").read()
context.get_or_create_agent( context.get_or_create_agent(agent_type="chat")
agent_type="chat", # system_prompt=system_message)
system_prompt=system_message)
# context.add_agent(Resume(system_prompt = system_generate_resume)) # context.add_agent(Resume(system_prompt = system_generate_resume))
# context.add_agent(JobDescription(system_prompt = system_job_description)) # context.add_agent(JobDescription(system_prompt = system_job_description))
# context.add_agent(FactCheck(system_prompt = system_fact_check)) # context.add_agent(FactCheck(system_prompt = system_fact_check))
context.tools = default_tools(tools) context.tools = Tools.enabled_tools(Tools.tools)
context.rags = rags.copy() context.rags = rags.copy()
logger.info(f"{context.id} created and added to contexts.") logger.info(f"{context.id} created and added to contexts.")
@ -849,73 +766,6 @@ class WebServer:
self.save_context(context.id) self.save_context(context.id)
return context return context
def get_optimal_ctx_size(self, context, messages, ctx_buffer = 4096):
ctx = round(context + len(str(messages)) * 3 / 4)
return max(defines.max_context, min(2048, ctx + ctx_buffer))
# %%
async def handle_tool_calls(self, message):
"""
Process tool calls and yield status updates along the way.
The last yielded item will be a tuple containing (tool_result, tools_used).
"""
tools_used = []
all_responses = []
for i, tool_call in enumerate(message["tool_calls"]):
arguments = tool_call["function"]["arguments"]
tool = tool_call["function"]["name"]
# Yield status update before processing each tool
yield {"status": "processing", "message": f"Processing tool {i+1}/{len(message['tool_calls'])}: {tool}..."}
# Process the tool based on its type
match tool:
case "TickerValue":
ticker = arguments.get("ticker")
if not ticker:
ret = None
else:
ret = TickerValue(ticker)
tools_used.append({ "tool": f"{tool}({ticker})", "result": ret})
case "AnalyzeSite":
url = arguments.get("url")
question = arguments.get("question", "what is the summary of this content?")
# Additional status update for long-running operations
yield {"status": "processing", "message": f"Retrieving and summarizing content from {url}..."}
ret = await AnalyzeSite(llm=self.llm, model=self.model, url=url, question=question)
tools_used.append({ "tool": f"{tool}('{url}', '{question}')", "result": ret })
case "DateTime":
tz = arguments.get("timezone")
ret = DateTime(tz)
tools_used.append({ "tool": f"{tool}('{tz}')", "result": ret })
case "WeatherForecast":
city = arguments.get("city")
state = arguments.get("state")
yield {"status": "processing", "message": f"Fetching weather data for {city}, {state}..."}
ret = WeatherForecast(city, state)
tools_used.append({ "tool": f"{tool}('{city}', '{state}')", "result": ret })
case _:
ret = None
# Build response for this tool
tool_response = {
"role": "tool",
"content": str(ret),
"name": tool_call["function"]["name"]
}
all_responses.append(tool_response)
# Yield the final result as the last item
final_result = all_responses[0] if len(all_responses) == 1 else all_responses
yield (final_result, tools_used)
def upsert_context(self, context_id = None) -> Context: def upsert_context(self, context_id = None) -> Context:
""" """
Upsert a context based on the provided context_id. Upsert a context based on the provided context_id.
@ -934,83 +784,34 @@ class WebServer:
logger.info(f"Context {context_id} is not yet loaded.") 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)
def generate_rag_results(self, context, content):
if not self.file_watcher:
raise Exception("File watcher not initialized")
results_found = False async def generate_response(self, context : Context, agent : Agent, prompt : str, options: Tunables | None) -> AsyncGenerator[Message, None]:
for rag in context.rags:
if rag["enabled"] and rag["name"] == "JPK": # Only support JPK rag right now...
yield {"status": "processing", "message": f"Checking RAG context {rag['name']}..."}
chroma_results = self.file_watcher.find_similar(query=content, top_k=10)
if chroma_results:
results_found = True
chroma_embedding = np.array(chroma_results["query_embedding"]).flatten() # Ensure correct shape
logger.info(f"Chroma embedding shape: {chroma_embedding.shape}")
umap_2d = self.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])[0].tolist()
logger.info(f"UMAP 3D output: {umap_3d}, length: {len(umap_3d)}") # Debug output
yield {
**chroma_results,
"name": rag["name"],
"umap_embedding_2d": umap_2d,
"umap_embedding_3d": umap_3d
}
if not results_found:
yield {"status": "complete", "message": "No RAG context found"}
yield {
"rag": None,
"documents": [],
"embeddings": [],
"umap_embedding_2d": [],
"umap_embedding_3d": []
}
else:
yield {"status": "complete", "message": "RAG processing complete"}
async def generate_response(self, context : Context, agent : Agent, content : str) -> AsyncGenerator[Message, None]:
if not self.file_watcher: if not self.file_watcher:
raise Exception("File watcher not initialized") raise Exception("File watcher not initialized")
agent_type = agent.get_agent_type() agent_type = agent.get_agent_type()
logger.info(f"generate_response: {agent_type}") logger.info(f"generate_response: type - {agent_type}")
if agent_type == "chat": message = Message(prompt=prompt, options=agent.tunables)
message = Message(prompt=content) if options:
async for message in agent.prepare_message(message): message.tunables = options
# logger.info(f"{agent_type}.prepare_message: {value.status} - {value.response}")
if message.status == "error": async for message in agent.prepare_message(message):
yield message # logger.info(f"{agent_type}.prepare_message: {value.status} - {value.response}")
return if message.status == "error":
if message.status != "done": yield message
yield message return
async for message in agent.process_message(self.llm, self.model, message): if message.status != "done":
# logger.info(f"{agent_type}.process_message: {value.status} - {value.response}") yield message
if message.status == "error": async for message in agent.process_message(self.llm, self.model, message):
yield message if message.status == "error":
return yield message
if message.status != "done": return
yield message if message.status != "done":
# async for value in agent.generate_llm_response(message): yield message
# logger.info(f"{agent_type}.generate_llm_response: {value.status} - {value.response}") logger.info(f"{agent_type}.process_message: {message.status} {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}")
# if value.status != "done": message.status = "done"
# yield value yield message
# if value.status == "error": return
# message.status = "error"
# message.response = value.response
# yield message
# return
logger.info("TODO: There is more to do...")
yield message
return
return
if self.processing: if self.processing:
logger.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time") logger.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time")
@ -1168,7 +969,7 @@ Use to the above information to respond to this prompt:
stuffingMessage.response = "Job description stored to use in future queries." stuffingMessage.response = "Job description stored to use in future queries."
stuffingMessage.metadata["origin"] = "job_description" stuffingMessage.metadata["origin"] = "job_description"
stuffingMessage.metadata["display"] = "hide" stuffingMessage.metadata["display"] = "hide"
conversation.add_message(stuffingMessage) conversation.add(stuffingMessage)
message.add_action("generate_resume") message.add_action("generate_resume")
@ -1254,7 +1055,7 @@ Use the above <|resume|> and <|job_description|> to answer this query:
stuffingMessage.metadata["display"] = "hide" stuffingMessage.metadata["display"] = "hide"
stuffingMessage.actions = [ "fact_check" ] stuffingMessage.actions = [ "fact_check" ]
logger.info("TODO: Switch this to use actions to keep the UI from showingit") logger.info("TODO: Switch this to use actions to keep the UI from showingit")
conversation.add_message(stuffingMessage) conversation.add(stuffingMessage)
# For all future calls to job_description, use the system_job_description # For all future calls to job_description, use the system_job_description
logger.info("TODO: Create a system_resume_QA prompt to use for the resume agent") logger.info("TODO: Create a system_resume_QA prompt to use for the resume agent")
@ -1270,7 +1071,7 @@ Use the above <|resume|> and <|job_description|> to answer this query:
case _: case _:
raise Exception(f"Invalid chat agent_type: {agent_type}") raise Exception(f"Invalid chat agent_type: {agent_type}")
conversation.add_message(message) conversation.add(message)
# llm_history.append({"role": "user", "content": message.preamble + content}) # llm_history.append({"role": "user", "content": message.preamble + content})
# user_history.append({"role": "user", "content": content, "origin": message.metadata["origin"]}) # user_history.append({"role": "user", "content": content, "origin": message.metadata["origin"]})
# message.metadata["full_query"] = llm_history[-1]["content"] # message.metadata["full_query"] = llm_history[-1]["content"]
@ -1472,7 +1273,7 @@ def main():
module="umap.*" module="umap.*"
) )
llm = ollama.Client(host=args.ollama_server) llm = ollama.Client(host=args.ollama_server) # type: ignore
model = args.ollama_model model = args.ollama_model
web_server = WebServer(llm, model) web_server = WebServer(llm, model)

View File

@ -1,366 +0,0 @@
# %%
# Imports [standard]
# Standard library modules (no try-except needed)
import argparse
import asyncio
import anyio
import json
import logging
import os
import queue
import re
import time
from datetime import datetime
def try_import(module_name, pip_name=None):
try:
__import__(module_name)
except ImportError:
print(f"Module '{module_name}' not found. Install it using:")
print(f" pip install {pip_name or module_name}")
# Third-party modules with import checks
try_import('gradio')
try_import('ollama')
try_import('openai')
try_import('pytz')
try_import('requests')
try_import('yfinance', 'yfinance')
try_import('dotenv', 'python-dotenv')
try_import('geopy', 'geopy')
from dotenv import load_dotenv
from geopy.geocoders import Nominatim
import gradio as gr
import pytz
import requests
import yfinance as yf
# %%
def WeatherForecast(city, state, country="USA"):
"""
Get weather information from weather.gov based on city, state, and country.
Args:
city (str): City name
state (str): State name or abbreviation
country (str): Country name (defaults to "USA" as weather.gov is for US locations)
Returns:
dict: Weather forecast information
"""
# Step 1: Get coordinates for the location using geocoding
location = f"{city}, {state}, {country}"
coordinates = get_coordinates(location)
if not coordinates:
return {"error": f"Could not find coordinates for {location}"}
# Step 2: Get the forecast grid endpoint for the coordinates
grid_endpoint = get_grid_endpoint(coordinates)
if not grid_endpoint:
return {"error": f"Could not find weather grid for coordinates {coordinates}"}
# Step 3: Get the forecast data from the grid endpoint
forecast = get_forecast(grid_endpoint)
if not forecast['location']:
forecast['location'] = location
return forecast
def get_coordinates(location):
"""Convert a location string to latitude and longitude using Nominatim geocoder."""
try:
# Create a geocoder with a meaningful user agent
geolocator = Nominatim(user_agent="weather_app_example")
# Get the location
location_data = geolocator.geocode(location)
if location_data:
return {
"latitude": location_data.latitude,
"longitude": location_data.longitude
}
else:
print(f"Location not found: {location}")
return None
except Exception as e:
print(f"Error getting coordinates: {e}")
return None
def get_grid_endpoint(coordinates):
"""Get the grid endpoint from weather.gov based on coordinates."""
try:
lat = coordinates["latitude"]
lon = coordinates["longitude"]
# Define headers for the API request
headers = {
"User-Agent": "WeatherAppExample/1.0 (your_email@example.com)",
"Accept": "application/geo+json"
}
# Make the request to get the grid endpoint
url = f"https://api.weather.gov/points/{lat},{lon}"
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
return data["properties"]["forecast"]
else:
print(f"Error getting grid: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"Error in get_grid_endpoint: {e}")
return None
# Weather related function
def get_forecast(grid_endpoint):
"""Get the forecast data from the grid endpoint."""
try:
# Define headers for the API request
headers = {
"User-Agent": "WeatherAppExample/1.0 (your_email@example.com)",
"Accept": "application/geo+json"
}
# Make the request to get the forecast
response = requests.get(grid_endpoint, headers=headers)
if response.status_code == 200:
data = response.json()
# Extract the relevant forecast information
periods = data["properties"]["periods"]
# Process the forecast data into a simpler format
forecast = {
"location": data["properties"].get("relativeLocation", {}).get("properties", {}),
"updated": data["properties"].get("updated", ""),
"periods": []
}
for period in periods:
forecast["periods"].append({
"name": period.get("name", ""),
"temperature": period.get("temperature", ""),
"temperatureUnit": period.get("temperatureUnit", ""),
"windSpeed": period.get("windSpeed", ""),
"windDirection": period.get("windDirection", ""),
"shortForecast": period.get("shortForecast", ""),
"detailedForecast": period.get("detailedForecast", "")
})
return forecast
else:
print(f"Error getting forecast: {response.status_code} - {response.text}")
return {"error": f"API Error: {response.status_code}"}
except Exception as e:
print(f"Error in get_forecast: {e}")
return {"error": f"Exception: {str(e)}"}
# Example usage
def do_weather():
city = input("Enter city: ")
state = input("Enter state: ")
country = input("Enter country (default USA): ") or "USA"
print(f"Getting weather for {city}, {state}, {country}...")
weather_data = WeatherForecast(city, state, country)
if "error" in weather_data:
print(f"Error: {weather_data['error']}")
else:
print("\nWeather Forecast:")
print(f"Location: {weather_data.get('location', {}).get('city', city)}, {weather_data.get('location', {}).get('state', state)}")
print(f"Last Updated: {weather_data.get('updated', 'N/A')}")
print("\nForecast Periods:")
for period in weather_data.get("periods", []):
print(f"\n{period['name']}:")
print(f" Temperature: {period['temperature']}{period['temperatureUnit']}")
print(f" Wind: {period['windSpeed']} {period['windDirection']}")
print(f" Forecast: {period['shortForecast']}")
print(f" Details: {period['detailedForecast']}")
# %%
# Stock related function
def TickerValue(ticker_symbols):
"""
Look up the current price of a stock using its ticker symbol.
Args:
ticker_symbol (str): The stock ticker symbol (e.g., 'AAPL' for Apple)
Returns:
dict: Current stock information including price
"""
results = []
print(f"TickerValue('{ticker_symbols}')")
for ticker_symbol in ticker_symbols.split(','):
ticker_symbol = ticker_symbol.strip()
if ticker_symbol == "":
continue
# Create a Ticker object
try:
ticker = yf.Ticker(ticker_symbol)
print(ticker)
# Get the latest market data
ticker_data = ticker.history(period="1d")
if ticker_data.empty:
results.append({"error": f"No data found for ticker {ticker_symbol}"})
continue
# Get the latest closing price
latest_price = ticker_data['Close'].iloc[-1]
# Get some additional info
info = ticker.info
results.append({ 'symbol': ticker_symbol, 'price': latest_price })
except Exception as e:
results.append({"error": f"Error fetching data for {ticker_symbol}: {str(e)}"})
return results[0] if len(results) == 1 else results
#{
# "symbol": ticker_symbol,
# "price": latest_price,
# "currency": info.get("currency", "Unknown"),
# "company_name": info.get("shortName", "Unknown"),
# "previous_close": info.get("previousClose", "Unknown"),
# "market_cap": info.get("marketCap", "Unknown"),
#}
# %%
def DateTime(timezone="America/Los_Angeles"):
"""
Returns the current date and time in the specified timezone in ISO 8601 format.
Args:
timezone (str): Timezone name (e.g., "UTC", "America/New_York", "Europe/London")
Default is "America/Los_Angeles"
Returns:
str: Current date and time with timezone in the format YYYY-MM-DDTHH:MM:SS+HH:MM
"""
try:
if timezone == 'system' or timezone == '' or not timezone:
timezone = 'America/Los_Angeles'
# Get current UTC time (timezone-aware)
local_tz = pytz.timezone("America/Los_Angeles")
local_now = datetime.now(tz=local_tz)
# Convert to target timezone
target_tz = pytz.timezone(timezone)
target_time = local_now.astimezone(target_tz)
return target_time.isoformat()
except Exception as e:
return {'error': f"Invalid timezone {timezone}: {str(e)}"}
# %%
tools = [ {
"type": "function",
"function": {
"name": "TickerValue",
"description": "Get the current stock price of one or more ticker symbols. Returns an array of objects with 'symbol' and 'price' fields. Call this whenever you need to know the latest value of stock ticker symbols, for example when a user asks 'How much is Intel trading at?' or 'What are the prices of AAPL and MSFT?'",
"parameters": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "The company stock ticker symbol. For multiple tickers, provide a comma-separated list (e.g., 'AAPL,MSFT,GOOGL').",
},
},
"required": ["ticker"],
"additionalProperties": False
}
}
}, {
"type": "function",
"function": {
"name": "AnalyzeSite",
"description": "Downloads the requested site and asks a second LLM agent to answer the question based on the site content. For example if the user says 'What are the top headlines on cnn.com?' you would use AnalyzeSite to get the answer. Only use this if the user asks about a specific site or company.",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The website URL to download and process",
},
"question": {
"type": "string",
"description": "The question to ask the second LLM about the content",
},
},
"required": ["url", "question"],
"additionalProperties": False
},
"returns": {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "Identifier for the source LLM"
},
"content": {
"type": "string",
"description": "The complete response from the second LLM"
},
"metadata": {
"type": "object",
"description": "Additional information about the response"
}
}
}
}
}, {
"type": "function",
"function": {
"name": "DateTime",
"description": "Get the current date and time in a specified timezone. For example if a user asks 'What time is it in Poland?' you would pass the Warsaw timezone to DateTime.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "Timezone name (e.g., 'UTC', 'America/New_York', 'Europe/London', 'America/Los_Angeles'). Default is 'America/Los_Angeles'."
}
},
"required": []
}
}
}, {
"type": "function",
"function": {
"name": "WeatherForecast",
"description": "Get the full weather forecast as structured data for a given CITY and STATE location in the United States. For example, if the user asks 'What is the weather in Portland?' or 'What is the forecast for tomorrow?' use the provided data to answer the question.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City to find the weather forecast (e.g., 'Portland', 'Seattle').",
"minLength": 2
},
"state": {
"type": "string",
"description": "State to find the weather forecast (e.g., 'OR', 'WA').",
"minLength": 2
}
},
"required": [ "city", "state" ],
"additionalProperties": False
}
}
}]
__all__ = [ 'tools', 'DateTime', 'WeatherForecast', 'TickerValue' ]

View File

@ -1,36 +1,27 @@
from typing import Optional, Type from __future__ import annotations
from pydantic import BaseModel # type: ignore
import importlib
from . import defines from . import defines
from . rag import ChromaDBFileWatcher, start_file_watcher
from . message import Message
from . conversation import Conversation
from . context import Context from . context import Context
from . import agents from . conversation import Conversation
from . message import Message, Tunables
from . rag import ChromaDBFileWatcher, start_file_watcher
from . setup_logging import setup_logging from . setup_logging import setup_logging
from . agents import class_registry, AnyAgent, Agent, __all__ as agents_all
from .agents import Agent, __all__ as agents_all
__all__ = [ __all__ = [
'Agent', 'Agent',
'Tunables',
'Context', 'Context',
'Conversation', 'Conversation',
'Message', 'Message',
'ChromaDBFileWatcher', 'ChromaDBFileWatcher',
'start_file_watcher' 'start_file_watcher',
'logger', 'logger',
] + agents_all ]
# Resolve circular dependencies by rebuilding models __all__.extend(agents_all) # type: ignore
# Call model_rebuild() on Agent and Context
Agent.model_rebuild()
Context.model_rebuild()
import importlib
from pydantic import BaseModel
from typing import Type
# Assuming class_registry is available from agents/__init__.py
from .agents import class_registry, AnyAgent
logger = setup_logging(level=defines.logging_level) logger = setup_logging(level=defines.logging_level)
@ -64,4 +55,4 @@ def rebuild_models():
logger.error(f"Error processing {class_name} in {module_name}: {e}") logger.error(f"Error processing {class_name} in {module_name}: {e}")
# Call this after all modules are imported # Call this after all modules are imported
rebuild_models() rebuild_models()

View File

@ -1,19 +1,25 @@
from __future__ import annotations from __future__ import annotations
from typing import TypeAlias, Dict, Tuple, Optional
import importlib import importlib
import pathlib import pathlib
import inspect import inspect
import logging
from typing import TypeAlias, Dict, Tuple from . types import registry
from pydantic import BaseModel from .. setup_logging import setup_logging
from .. import defines
from . base import Agent from . base import Agent
logger = setup_logging(defines.logging_level)
__all__ = [ "AnyAgent", "Agent", "registry", "class_registry" ]
# Type alias for Agent or any subclass # Type alias for Agent or any subclass
AnyAgent: TypeAlias = Agent # BaseModel covers Agent and subclasses AnyAgent: TypeAlias = Agent # BaseModel covers Agent and subclasses
class_registry: Dict[str, Tuple[str, str]] = {} # Maps class_name to (module_name, class_name)
package_dir = pathlib.Path(__file__).parent package_dir = pathlib.Path(__file__).parent
package_name = __name__ package_name = __name__
__all__ = []
class_registry: Dict[str, Tuple[str, str]] = {} # Maps class_name to (module_name, class_name)
for path in package_dir.glob("*.py"): for path in package_dir.glob("*.py"):
if path.name in ("__init__.py", "base.py") or path.name.startswith("_"): if path.name in ("__init__.py", "base.py") or path.name.startswith("_"):
@ -21,7 +27,7 @@ for path in package_dir.glob("*.py"):
module_name = path.stem module_name = path.stem
full_module_name = f"{package_name}.{module_name}" full_module_name = f"{package_name}.{module_name}"
try: try:
module = importlib.import_module(full_module_name) module = importlib.import_module(full_module_name)
@ -35,9 +41,11 @@ for path in package_dir.glob("*.py"):
): ):
class_registry[name] = (full_module_name, name) class_registry[name] = (full_module_name, name)
globals()[name] = obj globals()[name] = obj
logging.info(f"Adding agent: {name} from {full_module_name}") logger.info(f"Adding agent: {name}")
__all__.append(name) __all__.append(name) # type: ignore
except ImportError as e: except ImportError as e:
logging.error(f"Failed to import module {full_module_name}: {e}") logger.error(f"Error importing {full_module_name}: {e}")
raise e
__all__.append("AnyAgent") except Exception as e:
logger.error(f"Error processing {full_module_name}: {e}")
raise e

View File

@ -1,9 +1,18 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel, model_validator, PrivateAttr, Field from pydantic import BaseModel, PrivateAttr, Field # type: ignore
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar, ForwardRef, Any from typing import (
from abc import ABC, abstractmethod Literal, get_args, List, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar, Any,
from typing_extensions import Annotated TypeAlias, Dict, Tuple
)
import json
import time
import inspect
from abc import ABC
from .. setup_logging import setup_logging from .. setup_logging import setup_logging
from .. import defines
from .. message import Message
from .. tools import ( TickerValue, WeatherForecast, AnalyzeSite, DateTime, llm_tools ) # type: ignore -- dynamically added to __all__
logger = setup_logging() logger = setup_logging()
@ -14,35 +23,78 @@ if TYPE_CHECKING:
from .types import registry from .types import registry
from .. conversation import Conversation from .. conversation import Conversation
from .. message import Message from .. message import Message, Tunables
class LLMMessage(BaseModel):
role : str = Field(default="")
content : str = Field(default="")
tool_calls : Optional[List[Dict]] = Field(default={}, exclude=True)
class Agent(BaseModel, ABC): class Agent(BaseModel, ABC):
""" """
Base class for all agent types. Base class for all agent types.
This class defines the common attributes and methods for all agent types. This class defines the common attributes and methods for all agent types.
""" """
# Agent management with pydantic # Agent management with pydantic
agent_type: Literal["base"] = "base" agent_type: Literal["base"] = "base"
_agent_type: ClassVar[str] = agent_type # Add this for registration _agent_type: ClassVar[str] = agent_type # Add this for registration
# Tunables (sets default for new Messages attached to this agent)
tunables: Tunables = Field(default_factory=Tunables)
# Agent properties # Agent properties
system_prompt: str # Mandatory system_prompt: str # Mandatory
conversation: Conversation = Conversation() conversation: Conversation = Conversation()
context_tokens: int = 0 context_tokens: int = 0
context: Optional[Context] = Field(default=None, exclude=True) # Avoid circular reference, require as param, and prevent serialization context: Optional[Context] = Field(default=None, exclude=True) # Avoid circular reference, require as param, and prevent serialization
_content_seed: str = PrivateAttr(default="") # context_size is shared across all subclasses
_context_size: ClassVar[int] = int(defines.max_context * 0.5)
@property
def context_size(self) -> int:
return Agent._context_size
@context_size.setter
def context_size(self, value: int):
Agent._context_size = value
def set_optimal_context_size(self, llm: Any, model: str, prompt: str, ctx_buffer=2048) -> int:
# # Get more accurate token count estimate using tiktoken or similar
# response = llm.generate(
# model=model,
# prompt=prompt,
# options={
# "num_ctx": self.context_size,
# "num_predict": 0,
# } # Don't generate any tokens, just tokenize
# )
# # The prompt_eval_count gives you the token count of your input
# tokens = response.get("prompt_eval_count", 0)
# Most models average 1.3-1.5 tokens per word
word_count = len(prompt.split())
tokens = int(word_count * 1.4)
# Add buffer for safety
total_ctx = tokens + ctx_buffer
if total_ctx > self.context_size:
logger.info(f"Increasing context size from {self.context_size} to {total_ctx}")
# Grow the context size if necessary
self.context_size = max(self.context_size, total_ctx)
# Use actual model maximum context size
return self.context_size
# Class and pydantic model management # Class and pydantic model management
def __init_subclass__(cls, **kwargs): def __init_subclass__(cls, **kwargs) -> None:
"""Auto-register subclasses""" """Auto-register subclasses"""
super().__init_subclass__(**kwargs) super().__init_subclass__(**kwargs)
# Register this class if it has an agent_type # Register this class if it has an agent_type
if hasattr(cls, 'agent_type') and cls.agent_type != Agent._agent_type: if hasattr(cls, 'agent_type') and cls.agent_type != Agent._agent_type:
registry.register(cls.agent_type, cls) registry.register(cls.agent_type, cls)
def model_dump(self, *args, **kwargs): def model_dump(self, *args, **kwargs) -> Any:
# Ensure context is always excluded, even with exclude_unset=True # Ensure context is always excluded, even with exclude_unset=True
kwargs.setdefault("exclude", set()) kwargs.setdefault("exclude", set())
if isinstance(kwargs["exclude"], set): if isinstance(kwargs["exclude"], set):
@ -62,56 +114,322 @@ class Agent(BaseModel, ABC):
# Agent methods # Agent methods
def get_agent_type(self): def get_agent_type(self):
return self._agent_type return self._agent_type
async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]: async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]:
""" """
Prepare message with context information in message.preamble Prepare message with context information in message.preamble
""" """
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
# Generate RAG content if enabled, based on the content # Generate RAG content if enabled, based on the content
rag_context = "" rag_context = ""
if not message.disable_rag: if message.tunables.enable_rag and message.prompt:
# Gather RAG results, yielding each result # Gather RAG results, yielding each result
# as it becomes available # as it becomes available
for value in self.context.generate_rag_results(message): for message in self.context.generate_rag_results(message):
logger.info(f"RAG: {value.status} - {value.response}") logger.info(f"RAG: {message.status} - {message.response}")
if value.status != "done": if message.status == "error":
yield value
if value.status == "error":
message.status = "error"
message.response = value.response
yield message yield message
return return
if message.status != "done":
yield message
if message.metadata["rag"]: if "rag" in message.metadata and message.metadata["rag"]:
for rag_collection in message.metadata["rag"]: for rag in message.metadata["rag"]:
for doc in rag_collection["documents"]: for doc in rag["documents"]:
rag_context += f"{doc}\n" rag_context += f"{doc}\n"
message.preamble = {}
if rag_context: if rag_context:
message["context"] = rag_context message.preamble["context"] = rag_context
if self.context.user_resume:
message["resume"] = self.content.user_resume
if message.preamble:
preamble_types = [f"<|{p}|>" for p in message.preamble.keys()]
preamble_types_AND = " and ".join(preamble_types)
preamble_types_OR = " or ".join(preamble_types)
message.preamble["rules"] = f"""\
- Answer the question based on the information provided in the {preamble_types_AND} sections by incorporate it seamlessly and refer to it using natural language instead of mentioning {preamble_or_types} or quoting it directly.
- If there is no information in these sections, answer based on your knowledge.
- Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}.
"""
message.preamble["question"] = "Use that information to respond to:"
else:
message.preamble["question"] = "Respond to:"
if message.tunables.enable_context and self.context.user_resume:
message.preamble["resume"] = self.context.user_resume
message.system_prompt = self.system_prompt message.system_prompt = self.system_prompt
message.status = "done" message.status = "done"
yield message yield message
return return
async def generate_llm_response(self, message: Message) -> AsyncGenerator[Message, None]: async def process_tool_calls(self, llm: Any, model: str, message: Message, tool_message: Any, messages: List[LLMMessage]) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
if not message.metadata["tools"]:
raise ValueError("tools field not initialized")
tool_metadata = message.metadata["tools"]
tool_metadata["tool_calls"] = []
message.status = "tooling"
for i, tool_call in enumerate(tool_message.tool_calls):
arguments = tool_call.function.arguments
tool = tool_call.function.name
# Yield status update before processing each tool
message.response = f"Processing tool {i+1}/{len(tool_message.tool_calls)}: {tool}..."
yield message
logger.info(f"LLM - {message.response}")
# Process the tool based on its type
match tool:
case "TickerValue":
ticker = arguments.get("ticker")
if not ticker:
ret = None
else:
ret = TickerValue(ticker)
case "AnalyzeSite":
url = arguments.get("url")
question = arguments.get("question", "what is the summary of this content?")
# Additional status update for long-running operations
message.response = f"Retrieving and summarizing content from {url}..."
yield message
ret = await AnalyzeSite(llm=llm, model=model, url=url, question=question)
case "DateTime":
tz = arguments.get("timezone")
ret = DateTime(tz)
case "WeatherForecast":
city = arguments.get("city")
state = arguments.get("state")
message.response = f"Fetching weather data for {city}, {state}..."
yield message
ret = WeatherForecast(city, state)
case _:
ret = None
# Build response for this tool
tool_response = {
"role": "tool",
"content": json.dumps(ret),
"name": tool_call.function.name
}
tool_metadata["tool_calls"].append(tool_response)
if len(tool_metadata["tool_calls"]) == 0:
message.status = "done"
yield message
return
message_dict = LLMMessage(
role=tool_message.get("role", "assistant"),
content=tool_message.get("content", ""),
tool_calls=[ {
"function": {
"name": tc["function"]["name"],
"arguments": tc["function"]["arguments"]
}
} for tc in tool_message.tool_calls
]
)
messages.append(message_dict)
messages.extend(tool_metadata["tool_calls"])
message.status = "thinking"
message.response = "Incorporating tool results into response..."
yield message
# Decrease creativity when processing tool call requests
message.response = ""
start_time = time.perf_counter()
for response in llm.chat(
model=model,
messages=messages,
stream=True,
options={
**message.metadata["options"],
# "temperature": 0.5,
}
):
# logger.info(f"LLM::Tools: {'done' if response.done else 'processing'} - {response.message}")
message.status = "streaming"
message.response += response.message.content
if not response.done:
yield message
if response.done:
message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count
message.metadata["prompt_eval_duration"] += response.prompt_eval_duration
self.context_tokens = response.prompt_eval_count + response.eval_count
message.status = "done"
yield message
end_time = time.perf_counter()
message.metadata["timers"]["llm_with_tools"] = f"{(end_time - start_time):.4f}"
return
async def generate_llm_response(self, llm: Any, model: str, message: Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
# Create a pruned down message list based purely on the prompt and responses,
# discarding the full preamble generated by prepare_message
messages: List[LLMMessage] = [ LLMMessage(role="system", content=message.system_prompt) ]
messages.extend([
item for m in self.conversation
for item in [
LLMMessage(role="user", content=m.prompt.strip()),
LLMMessage(role="assistant", content=m.response.strip())
]
])
# Only the actual user query is provided with the full context message
messages.append(LLMMessage(role="user", content=message.context_prompt.strip()))
#message.metadata["messages"] = messages
message.metadata["options"]={
"seed": 8911,
"num_ctx": self.context_size,
#"temperature": 0.9, # Higher temperature to encourage tool usage
}
message.metadata["timers"] = {}
use_tools = message.tunables.enable_tools and len(self.context.tools) > 0
message.metadata["tools"] = {
"available": llm_tools(self.context.tools),
"used": False
}
tool_metadata = message.metadata["tools"]
if use_tools:
message.status = "thinking"
message.response = f"Performing tool analysis step 1/2..."
yield message
logger.info("Checking for LLM tool usage")
start_time = time.perf_counter()
# Tools are enabled and available, so query the LLM with a short token target to see if it will
# use the tools
tool_metadata["messages"] = [{ "role": "system", "content": self.system_prompt}, {"role": "user", "content": message.prompt}]
response = llm.chat(
model=model,
messages=tool_metadata["messages"],
tools=tool_metadata["available"],
options={
**message.metadata["options"],
#"num_predict": 1024, # "Low" token limit to cut off after tool call
},
stream=False # No need to stream the probe
)
end_time = time.perf_counter()
message.metadata["timers"]["tool_check"] = f"{(end_time - start_time):.4f}"
if not response.message.tool_calls:
logger.info("LLM indicates tools will not be used")
# The LLM will not use tools, so disable use_tools so we can stream the full response
use_tools = False
else:
tool_metadata["attempted"] = response.message.tool_calls
if use_tools:
logger.info("LLM indicates tools will be used")
# Tools are enabled and available and the LLM indicated it will use them
message.response = f"Performing tool analysis step 2/2 (tool use suspected)..."
yield message
logger.info(f"Performing LLM call with tools")
start_time = time.perf_counter()
response = llm.chat(
model=model,
messages=tool_metadata["messages"], # messages,
tools=tool_metadata["available"],
options={
**message.metadata["options"],
},
stream=False
)
end_time = time.perf_counter()
message.metadata["timers"]["non_streaming"] = f"{(end_time - start_time):.4f}"
if not response:
message.status = "error"
message.response = "No response from LLM."
yield message
return
if response.message.tool_calls:
tool_metadata["used"] = response.message.tool_calls
# Process all yielded items from the handler
start_time = time.perf_counter()
async for message in self.process_tool_calls(llm=llm, model=model, message=message, tool_message=response.message, messages=messages):
if message.status == "error":
yield message
return
yield message
end_time = time.perf_counter()
message.metadata["timers"]["process_tool_calls"] = f"{(end_time - start_time):.4f}"
message.status = "done"
return
logger.info("LLM indicated tools will be used, and then they weren't")
message.response = response.message.content
message.status = "done"
yield message
return
# not use_tools
message.status = "thinking"
message.response = f"Generating response..."
yield message
# Reset the response for streaming
message.response = ""
start_time = time.perf_counter()
for response in llm.chat(
model=model,
messages=messages,
options={
**message.metadata["options"],
},
stream=True,
):
if not response:
message.status = "error"
message.response = "No response from LLM."
yield message
return
message.status = "streaming"
message.response += response.message.content
if not response.done:
yield message
if response.done:
message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count
message.metadata["prompt_eval_duration"] += response.prompt_eval_duration
self.context_tokens = response.prompt_eval_count + response.eval_count
message.status = "done"
yield message
end_time = time.perf_counter()
message.metadata["timers"]["streamed"] = f"{(end_time - start_time):.4f}"
return
async def process_message(self, llm: Any, model: str, message:Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
if self.context.processing: if self.context.processing:
logger.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time") logger.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time")
message.status = "error" message.status = "error"
@ -121,138 +439,37 @@ class Agent(BaseModel, ABC):
self.context.processing = True self.context.processing = True
messages = [] message.metadata["system_prompt"] = f"<|system|>\n{self.system_prompt.strip()}\n"
message.context_prompt = ""
for value in self.llm.chat( for p in message.preamble.keys():
model=self.model, message.context_prompt += f"\n<|{p}|>\n{message.preamble[p].strip()}\n"
messages=messages, message.context_prompt += f"{message.prompt}"
#tools=llm_tools(context.tools) if message.enable_tools else None,
options={ "num_ctx": message.ctx_size }
):
logger.info(f"LLM: {value.status} - {value.response}")
if value.status != "done":
message.status = value.status
message.response = value.response
yield message
if value.status == "error":
return
response = value
message.metadata["eval_count"] += response["eval_count"]
message.metadata["eval_duration"] += response["eval_duration"]
message.metadata["prompt_eval_count"] += response["prompt_eval_count"]
message.metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
agent.context_tokens = response["prompt_eval_count"] + response["eval_count"]
tools_used = []
yield {"status": "processing", "message": "Initial response received..."}
if "tool_calls" in response.get("message", {}):
yield {"status": "processing", "message": "Processing tool calls..."}
tool_message = response["message"]
tool_result = None
# Process all yielded items from the handler
async for item in self.handle_tool_calls(tool_message):
if isinstance(item, tuple) and len(item) == 2:
# This is the final result tuple (tool_result, tools_used)
tool_result, tools_used = item
else:
# This is a status update, forward it
yield item
message_dict = {
"role": tool_message.get("role", "assistant"),
"content": tool_message.get("content", "")
}
if "tool_calls" in tool_message:
message_dict["tool_calls"] = [
{"function": {"name": tc["function"]["name"], "arguments": tc["function"]["arguments"]}}
for tc in tool_message["tool_calls"]
]
pre_add_index = len(messages)
messages.append(message_dict)
if isinstance(tool_result, list):
messages.extend(tool_result)
else:
if tool_result:
messages.append(tool_result)
message.metadata["tools"] = tools_used
# Estimate token length of new messages
ctx_size = self.get_optimal_ctx_size(agent.context_tokens, messages=messages[pre_add_index:])
yield {"status": "processing", "message": "Generating final response...", "num_ctx": ctx_size }
# Decrease creativity when processing tool call requests
response = self.llm.chat(model=self.model, messages=messages, stream=False, options={ "num_ctx": ctx_size }) #, "temperature": 0.5 })
message.metadata["eval_count"] += response["eval_count"]
message.metadata["eval_duration"] += response["eval_duration"]
message.metadata["prompt_eval_count"] += response["prompt_eval_count"]
message.metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
agent.context_tokens = response["prompt_eval_count"] + response["eval_count"]
reply = response["message"]["content"]
message.response = reply
message.metadata["origin"] = agent.agent_type
# final_message = {"role": "assistant", "content": reply }
# # history is provided to the LLM and should not have additional metadata
# llm_history.append(final_message)
# user_history is provided to the REST API and does not include CONTEXT
# It does include metadata
# final_message["metadata"] = message.metadata
# user_history.append({**final_message, "origin": message.metadata["origin"]})
# Return the REST API with metadata
yield {
"status": "done",
"message": {
**message.model_dump(mode='json'),
}
}
self.context.processing = False
return
async def process_message(self, llm: Any, model: str, message:Message) -> AsyncGenerator[Message, None]:
message.full_content = ""
for i, p in enumerate(message.preamble.keys()):
message.full_content += '' if i == 0 else '\n\n' + f"<|{p}|>{message.preamble[p].strip()}\n"
# Estimate token length of new messages # Estimate token length of new messages
message.ctx_size = self.context.get_optimal_ctx_size(self.context_tokens, messages=message.full_content) message.response = f"Optimizing context..."
message.status = "thinking"
message.response = f"Processing {'RAG augmented ' if message.metadata['rag'] else ''}query..."
message.status = "thinking"
yield message yield message
for value in self.generate_llm_response(message): message.metadata["context_size"] = self.set_optimal_context_size(llm, model, prompt=message.context_prompt)
logger.info(f"LLM: {value.status} - {value.response}")
if value.status != "done": message.response = f"Processing {'RAG augmented ' if message.metadata['rag'] else ''}query..."
yield value message.status = "thinking"
if value.status == "error": yield message
return
def get_and_reset_content_seed(self): async for message in self.generate_llm_response(llm, model, message):
tmp = self._content_seed # logger.info(f"LLM: {message.status} - {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}")
self._content_seed = "" if message.status == "error":
return tmp yield message
self.context.processing = False
def set_content_seed(self, content: str) -> None: return
"""Set the content seed for the agent.""" yield message
self._content_seed = content
# Done processing, add message to conversation
def get_content_seed(self) -> str: message.status = "done"
"""Get the content seed for the agent.""" self.conversation.add(message)
return self._content_seed self.context.processing = False
return
# Register the base agent # Register the base agent
registry.register(Agent._agent_type, Agent) registry.register(Agent._agent_type, Agent)

View File

@ -1,244 +1,59 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel, model_validator, PrivateAttr from typing import Literal, AsyncGenerator, ClassVar, Optional, Any
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar, Any from datetime import datetime
from typing_extensions import Annotated import inspect
from abc import ABC, abstractmethod
from typing_extensions import Annotated
import logging
from .base import Agent, registry
from .. conversation import Conversation
from .. message import Message
from .. import defines
class Chat(Agent, ABC): from . base import Agent, registry
from .. message import Message
from .. setup_logging import setup_logging
logger = setup_logging()
system_message = f"""
Launched on {datetime.now().isoformat()}.
When answering queries, follow these steps:
- First analyze the query to determine if real-time information from the tools might be helpful
- Even when <|context|> or <|resume|> is provided, consider whether the tools would provide more current or comprehensive information
- Use the provided tools whenever they would enhance your response, regardless of whether context is also available
- 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: ❄️
- When any combination of <|context|>, <|resume|> and tool outputs are relevant, synthesize information from all sources to provide the most complete answer
- Always prioritize the most up-to-date and relevant information, whether it comes from <|context|>, <|resume|> or tools
- If <|context|> and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
- If there is information in the <|context|> or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it as 'the latest information' or 'recent data' instead of mentioning '<|context|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|> or <|resume|>.
Always use tools, <|resume|>, and <|context|> when possible. Be concise, and never make up information. If you do not know the answer, say so.
"""
class Chat(Agent):
""" """
Base class for all agent types. Chat Agent
This class defines the common attributes and methods for all agent types.
""" """
agent_type: Literal["chat"] = "chat" agent_type: Literal["chat"] = "chat" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration _agent_type: ClassVar[str] = agent_type # Add this for registration
system_prompt: str = system_message
async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]: async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]:
""" logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
Prepare message with context information in message.preamble
"""
if not self.context: if not self.context:
raise ValueError("Context is not set for this agent.") raise ValueError("Context is not set for this agent.")
# Generate RAG content if enabled, based on the content async for message in super().prepare_message(message):
rag_context = "" if message.status != "done":
if not message.disable_rag: yield message
# Gather RAG results, yielding each result
# as it becomes available
for message in self.context.generate_rag_results(message):
logging.info(f"RAG: {message.status} - {message.response}")
if message.status == "error":
yield message
return
if message.status != "done":
yield message
if "rag" in message.metadata and message.metadata["rag"]:
for rag in message.metadata["rag"]:
for doc in rag["documents"]:
rag_context += f"{doc}\n"
message.preamble = {}
if rag_context:
message.preamble["context"] = rag_context
if self.context.user_resume:
message.preamble["resume"] = self.context.user_resume
if message.preamble: if message.preamble:
preamble_types = [f"<|{p}|>" for p in message.preamble.keys()] preamble_types = [f"<|{p}|>" for p in message.preamble.keys()]
preamble_types_AND = " and ".join(preamble_types) preamble_types_AND = " and ".join(preamble_types)
preamble_types_OR = " or ".join(preamble_types) preamble_types_OR = " or ".join(preamble_types)
message.preamble["rules"] = f"""\ message.preamble["rules"] = f"""\
- Answer the question based on the information provided in the {preamble_types_AND} sections by incorporate it seamlessly and refer to it using natural language instead of mentioning {preamble_types_OR} or quoting it directly. - Answer the question based on the information provided in the {preamble_types_AND} sections by incorporate it seamlessly and refer to it using natural language instead of mentioning {preamble_types_OR} or quoting it directly.
- If there is no information in these sections, answer based on your knowledge. - If there is no information in these sections, answer based on your knowledge, or use any available tools.
- Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}. - Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}.
""" """
message.preamble["question"] = "Use that information to respond to:" message.preamble["question"] = "Respond to:"
else:
message.preamble["question"] = "Respond to:"
message.system_prompt = self.system_prompt
message.status = "done"
yield message
return
async def generate_llm_response(self, llm: Any, model: str, message: Message) -> AsyncGenerator[Message, None]:
if not self.context:
raise ValueError("Context is not set for this agent.")
if self.context.processing:
logging.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time")
message.status = "error"
message.response = "Busy processing another request."
yield message
return
self.context.processing = True
self.conversation.add_message(message)
messages = [
item for m in self.conversation.messages
for item in [
{"role": "user", "content": m.prompt},
{"role": "assistant", "content": m.response}
]
]
message.status = "thinking"
for response in llm.chat(
model=model,
messages=messages,
#tools=llm_tools(context.tools) if message.enable_tools else None,
options={ "num_ctx": message.metadata["ctx_size"] if message.metadata["ctx_size"] else defines.max_context },
stream=True,
):
message.response += response.message.content
yield message
if response.done:
message.metadata["eval_count"] += response.eval_count
message.metadata["eval_duration"] += response.eval_duration
message.metadata["prompt_eval_count"] += response.prompt_eval_count
message.metadata["prompt_eval_duration"] += response.prompt_eval_duration
self.context_tokens = response.prompt_eval_count + response.eval_count
message.status = "done"
if not response:
message.status = "error"
message.response = "No response from LLM."
yield message
self.context.processing = False
return
yield message
self.context.processing = False
return
tools_used = []
if "tool_calls" in response.get("message", {}):
message.status = "thinking"
message.response = "Processing tool calls..."
tool_message = response["message"]
tool_result = None
# Process all yielded items from the handler
async for value in self.handle_tool_calls(tool_message):
if isinstance(value, tuple) and len(value) == 2:
# This is the final result tuple (tool_result, tools_used)
tool_result, tools_used = value
else:
# This is a status update, forward it
yield value
message_dict = {
"role": tool_message.get("role", "assistant"),
"content": tool_message.get("content", "")
}
if "tool_calls" in tool_message:
message_dict["tool_calls"] = [
{"function": {"name": tc["function"]["name"], "arguments": tc["function"]["arguments"]}}
for tc in tool_message["tool_calls"]
]
pre_add_index = len(messages)
messages.append(message_dict)
if isinstance(tool_result, list):
messages.extend(tool_result)
else:
if tool_result:
messages.append(tool_result)
message.metadata["tools"] = tools_used
# Estimate token length of new messages
ctx_size = self.get_optimal_ctx_size(agent.context_tokens, messages=messages[pre_add_index:])
yield {"status": "processing", "message": "Generating final response...", "num_ctx": ctx_size }
# Decrease creativity when processing tool call requests
response = self.llm.chat(model=self.model, messages=messages, stream=False, options={ "num_ctx": ctx_size }) #, "temperature": 0.5 })
message.metadata["eval_count"] += response["eval_count"]
message.metadata["eval_duration"] += response["eval_duration"]
message.metadata["prompt_eval_count"] += response["prompt_eval_count"]
message.metadata["prompt_eval_duration"] += response["prompt_eval_duration"]
agent.context_tokens = response["prompt_eval_count"] + response["eval_count"]
reply = response["message"]["content"]
message.response = reply
message.metadata["origin"] = agent.agent_type
# final_message = {"role": "assistant", "content": reply }
# # history is provided to the LLM and should not have additional metadata
# llm_history.append(final_message)
# user_history is provided to the REST API and does not include CONTEXT
# It does include metadata
# final_message["metadata"] = message.metadata
# user_history.append({**final_message, "origin": message.metadata["origin"]})
# Return the REST API with metadata
yield {
"status": "done",
"message": {
**message.model_dump(mode='json'),
}
}
self.context.processing = False
return
async def process_message(self, llm: Any, model: str, message:Message) -> AsyncGenerator[Message, None]:
if not self.context:
raise ValueError("Context is not set for this agent.")
message.full_content = f"<|system|>{self.system_prompt.strip()}\n"
for i, p in enumerate(message.preamble.keys()):
message.full_content += f"\n<|{p}|>\n{message.preamble[p].strip()}\n"
message.full_content += f"{message.prompt}"
# Estimate token length of new messages
message.metadata["ctx_size"] = self.context.get_optimal_ctx_size(self.context_tokens, messages=message.full_content)
message.response = f"Processing {'RAG augmented ' if message.metadata['rag'] else ''}query..."
message.status = "searching"
yield message
async for message in self.generate_llm_response(llm, model, message):
logging.info(f"LLM: {message.status} - {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}")
if message.status == "error":
return
if message.status != "done":
yield message
yield message
return
def get_and_reset_content_seed(self):
tmp = self._content_seed
self._content_seed = ""
return tmp
def set_content_seed(self, content: str) -> None:
"""Set the content seed for the agent."""
self._content_seed = content
def get_content_seed(self) -> str:
"""Get the content seed for the agent."""
return self._content_seed
@classmethod
def valid_agent_types(cls) -> set[str]:
"""Return the set of valid agent_type values."""
return set(get_args(cls.__annotations__["agent_type"]))
# Register the base agent # Register the base agent
registry.register(Chat._agent_type, Chat) registry.register(Chat._agent_type, Chat)

View File

@ -1,18 +1,32 @@
from pydantic import BaseModel, Field, model_validator, PrivateAttr from __future__ import annotations
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar from pydantic import model_validator # type: ignore
from typing_extensions import Annotated from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
from abc import ABC, abstractmethod from datetime import datetime
from typing_extensions import Annotated import inspect
import logging
from .base import Agent, registry from . base import Agent, registry
from .. conversation import Conversation from .. conversation import Conversation
from .. message import Message from .. message import Message
from .. setup_logging import setup_logging
logger = setup_logging()
system_fact_check = f"""
Launched on {datetime.now().isoformat()}.
You are a professional resume fact checker. Your task is answer any questions about items identified in the <|discrepancies|>.
The <|discrepancies|> indicate inaccuracies or unsupported claims in the <|generated-resume|> based on content from the <|resume|> and <|context|>.
When answering queries, follow these steps:
- If there is information in the <|context|> or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|context|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|generated-resume|>, or <|resume|> tags.
""".strip()
class FactCheck(Agent): class FactCheck(Agent):
agent_type: Literal["fact_check"] = "fact_check" agent_type: Literal["fact_check"] = "fact_check" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration _agent_type: ClassVar[str] = agent_type # Add this for registration
facts: str = "" system_prompt:str = system_fact_check
facts: str
@model_validator(mode="after") @model_validator(mode="after")
def validate_facts(self): def validate_facts(self):
@ -20,5 +34,36 @@ class FactCheck(Agent):
raise ValueError("Facts cannot be empty") raise ValueError("Facts cannot be empty")
return self return self
async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
resume_agent = self.context.get_agent("resume")
if not resume_agent:
raise ValueError("resume agent does not exist")
message.enable_tools = False
async for message in super().prepare_message(message):
if message.status != "done":
yield message
message.preamble["generated-resume"] = resume_agent.resume
message.preamble["discrepancies"] = self.facts
preamble_types = [f"<|{p}|>" for p in message.preamble.keys()]
preamble_types_AND = " and ".join(preamble_types)
preamble_types_OR = " or ".join(preamble_types)
message.preamble["rules"] = f"""\
- Answer the question based on the information provided in the {preamble_types_AND} sections by incorporate it seamlessly and refer to it using natural language instead of mentioning {preamble_types_OR} or quoting it directly.
- If there is no information in these sections, answer based on your knowledge, or use any available tools.
- Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}.
"""
message.preamble["question"] = "Respond to:"
yield message
return
# Register the base agent # Register the base agent
registry.register(FactCheck._agent_type, FactCheck) registry.register(FactCheck._agent_type, FactCheck)

View File

@ -1,18 +1,63 @@
from pydantic import BaseModel, Field, model_validator, PrivateAttr from __future__ import annotations
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar from pydantic import model_validator # type: ignore
from typing_extensions import Annotated from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
from abc import ABC, abstractmethod from datetime import datetime
from typing_extensions import Annotated import inspect
import logging
from .base import Agent, registry from . base import Agent, registry
from .. conversation import Conversation from .. conversation import Conversation
from .. message import Message from .. message import Message
from .. setup_logging import setup_logging
logger = setup_logging()
system_generate_resume = f"""
Launched on {datetime.now().isoformat()}.
You are a professional resume writer. Your task is to write a concise, polished, and tailored resume for a specific job based only on the individual's <|context|>.
When answering queries, follow these steps:
- You must not invent or assume any inforation not explicitly present in the <|context|>.
- Analyze the <|job_description|> to identify skills required for the job.
- Use the <|job_description|> provided to guide the focus, tone, and relevant skills or experience to highlight from the <|context|>.
- Identify and emphasize the experiences, achievements, and responsibilities from the <|context|> that best align with the <|job_description|>.
- Only provide information from <|context|> items if it is relevant to the <|job_description|>.
- Do not use the <|job_description|> skills unless listed in <|context|>.
- Do not include any information unless it is provided in <|context|>.
- Use the <|context|> to create a polished, professional resume.
- Do not list any locations or mailing addresses in the resume.
- If there is information in the <|context|>, <|job_description|>, <|context|>, or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|job_description|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, or <|context|> tags.
- Ensure the langauge is clear, concise, and aligned with industry standards for professional resumes.
Structure the resume professionally with the following sections where applicable:
* Name: Use full name
* Professional Summary: A 2-4 sentence overview tailored to the job.
* Skills: A bullet list of key skills derived from the work history and relevant to the job.
* Professional Experience: A detailed list of roles, achievements, and responsibilities from <|context|> that relate to the <|job_description|>.
* Education: Include only if available in the work history.
* Notes: Indicate the initial draft of the resume was generated using the Backstory application.
""".strip()
system_job_description = f"""
Launched on {datetime.now().isoformat()}.
You are a hiring and job placing specialist. Your task is to answers about a job description.
When answering queries, follow these steps:
- Analyze the <|job_description|> to provide insights for the asked question.
- If any financial information is requested, be sure to account for inflation.
- If there is information in the <|context|>, <|job_description|>, <|context|>, or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|job_description|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, <|resume|>, or <|context|> tags.
""".strip()
class JobDescription(Agent): class JobDescription(Agent):
agent_type: Literal["job_description"] = "job_description" agent_type: Literal["job_description"] = "job_description" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration _agent_type: ClassVar[str] = agent_type # Add this for registration
job_description: str = "" system_prompt: str = system_generate_resume
job_description: str
@model_validator(mode="after") @model_validator(mode="after")
def validate_job_description(self): def validate_job_description(self):
@ -20,5 +65,63 @@ class JobDescription(Agent):
raise ValueError("Job description cannot be empty") raise ValueError("Job description cannot be empty")
return self return self
async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
async for message in super().prepare_message(message):
if message.status != "done":
yield message
# Always add the job description, user resume, and question
message.preamble["job_description"] = self.job_description
message.preamble["resume"] = self.context.user_resume
preamble_types = [f"<|{p}|>" for p in message.preamble.keys()]
preamble_types_AND = " and ".join(preamble_types)
preamble_types_OR = " or ".join(preamble_types)
message.preamble["rules"] = f"""\
- Answer the question based on the information provided in the {preamble_types_AND} sections by incorporating it seamlessly and refer to it using natural language instead of mentioning {preamble_types_OR} or quoting it directly.
- If there is no information in these sections, answer based on your knowledge, or use any available tools.
- Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}.
"""
resume_agent = self.context.get_agent(agent_type="resume")
if resume_agent:
message.preamble["question"] = "Respond to:"
else:
message.preamble["question"] = "Generate a resume given the <|resume|> and <|job_description|>."
yield message
return
async def process_message(self, llm: Any, model: str, message:Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
async for message in super().process_message(llm, model, message):
if message.status != "done":
yield message
resume_agent = self.context.get_agent(agent_type="resume")
if not resume_agent:
# Switch agent from "Create Resume from Job Desription" mode
# to "Answer Questions about Job Description"
self.system_prompt = system_job_description
# Instantiate the "resume" agent, and seed (or reset) its conversation
# with this message.
resume_agent = self.context.get_or_create_agent(agent_type="resume", resume=message.response)
first_resume_message = message.copy()
first_resume_message.prompt = "Generate a resume for the job description."
resume_agent.conversation.add(first_resume_message)
message.response = "Resume generated."
# Return the final message
yield message
return
# Register the base agent # Register the base agent
registry.register(JobDescription._agent_type, JobDescription) registry.register(JobDescription._agent_type, JobDescription)

View File

@ -1,18 +1,47 @@
from pydantic import BaseModel, Field, model_validator, PrivateAttr from __future__ import annotations
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar from pydantic import model_validator # type: ignore
from typing_extensions import Annotated from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
from abc import ABC, abstractmethod from datetime import datetime
from typing_extensions import Annotated import inspect
import logging
from .base import Agent, registry from . base import Agent, registry
from .. conversation import Conversation
from .. message import Message from .. message import Message
from .. setup_logging import setup_logging
logger = setup_logging()
system_fact_check = f"""
Launched on {datetime.now().isoformat()}.
You are a professional resume fact checker. Your task is to identify any inaccuracies in the <|generated-resume|> based on the individual's <|context|> and <|resume|>.
If there are inaccuracies, list them in a bullet point format.
When answering queries, follow these steps:
- Analyze the <|generated-resume|> to identify any discrepancies or inaccuracies which are not supported by the <|context|> and <|resume|>.
- If there is information in the <|context|> or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|context|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|generated-resume|>, or <|resume|> tags.
Do not generate a revised resume.
""".strip()
system_resume = f"""
Launched on {datetime.now().isoformat()}.
You are a hiring and job placing specialist. Your task is to answers about a resume and work history as it relates to a potential job.
When answering queries, follow these steps:
- Analyze the <|job_description|> and <|generated-resume|> to provide insights for the asked question.
- If any financial information is requested, be sure to account for inflation.
- If there is information in the <|context|>, <|job_description|>, <|generated-resume|>, or <|resume|> sections to enhance the answer, incorporate it seamlessly and refer to it using natural language instead of mentioning '<|job_description|>' (etc.) or quoting it directly.
- Avoid phrases like 'According to the <|context|>' or similar references to the <|context|>, <|job_description|>, <|resume|>, or <|context|> tags.
""".strip()
class Resume(Agent): class Resume(Agent):
agent_type: Literal["resume"] = "resume" agent_type: Literal["resume"] = "resume" # type: ignore
_agent_type: ClassVar[str] = agent_type # Add this for registration _agent_type: ClassVar[str] = agent_type # Add this for registration
resume: str = "" system_prompt:str = system_fact_check
resume: str
@model_validator(mode="after") @model_validator(mode="after")
def validate_resume(self): def validate_resume(self):
@ -20,13 +49,65 @@ class Resume(Agent):
raise ValueError("Resume content cannot be empty") raise ValueError("Resume content cannot be empty")
return self return self
def get_resume(self) -> str: async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]:
"""Get the resume content.""" logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
return self.resume if not self.context:
raise ValueError("Context is not set for this agent.")
async for message in super().prepare_message(message):
if message.status != "done":
yield message
def set_resume(self, resume: str) -> None: message.preamble["generated-resume"] = self.resume
"""Set the resume content.""" job_description_agent = self.context.get_agent("job_description")
self.resume = resume if not job_description_agent:
raise ValueError("job_description agent does not exist")
message.preamble["job_description"] = job_description_agent.job_description
preamble_types = [f"<|{p}|>" for p in message.preamble.keys()]
preamble_types_AND = " and ".join(preamble_types)
preamble_types_OR = " or ".join(preamble_types)
message.preamble["rules"] = f"""\
- Answer the question based on the information provided in the {preamble_types_AND} sections by incorporate it seamlessly and refer to it using natural language instead of mentioning {preamble_types_OR} or quoting it directly.
- If there is no information in these sections, answer based on your knowledge, or use any available tools.
- Avoid phrases like 'According to the {preamble_types[0]}' or similar references to the {preamble_types_OR}.
"""
fact_check_agent = self.context.get_agent(agent_type="fact_check")
if fact_check_agent:
message.preamble["question"] = "Respond to:"
else:
message.preamble["question"] = f"Fact check the <|generated-resume|> based on the <|resume|>{' and <|context|>' if 'context' in message.preamble else ''}."
yield message
return
async def process_message(self, llm: Any, model: str, message:Message) -> AsyncGenerator[Message, None]:
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
if not self.context:
raise ValueError("Context is not set for this agent.")
async for message in super().process_message(llm, model, message):
if message.status != "done":
yield message
fact_check_agent = self.context.get_agent(agent_type="fact_check")
if not fact_check_agent:
# Switch agent from "Fact Check Generated Resume" mode
# to "Answer Questions about Generated Resume"
self.system_prompt = system_resume
# Instantiate the "resume" agent, and seed (or reset) its conversation
# with this message.
fact_check_agent = self.context.get_or_create_agent(agent_type="fact_check", facts=message.response)
first_fact_check_message = message.copy()
first_fact_check_message.prompt = "Fact check the generated resume."
fact_check_agent.conversation.add(first_fact_check_message)
message.response = "Resume fact checked."
# Return the final message
yield message
return
# Register the base agent # Register the base agent
registry.register(Resume._agent_type, Resume) registry.register(Resume._agent_type, Resume)

View File

@ -1,12 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import List, Dict, Any, Union, ForwardRef, TypeVar, Optional, TYPE_CHECKING, Type, ClassVar, Literal from typing import List, Dict, Optional, Type
from typing_extensions import Annotated
from pydantic import Field, BaseModel
from abc import ABC, abstractmethod
# Forward references
AgentRef = ForwardRef('Agent')
ContextRef = ForwardRef('Context')
# We'll use a registry pattern rather than hardcoded strings # We'll use a registry pattern rather than hardcoded strings
class AgentRegistry: class AgentRegistry:

View File

@ -1,4 +1,4 @@
import tiktoken import tiktoken # type: ignore
from . import defines from . import defines
from typing import List, Dict, Any, Union from typing import List, Dict, Any, Union

View File

@ -1,18 +1,17 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel, Field, model_validator, ValidationError from pydantic import BaseModel, Field, model_validator# type: ignore
from uuid import uuid4 from uuid import uuid4
from typing import List, Dict, Any, Optional, Generator, TYPE_CHECKING from typing import List, Optional, Generator
from typing_extensions import Annotated, Union from typing_extensions import Annotated, Union
import numpy as np import numpy as np # type: ignore
import logging import logging
from uuid import uuid4 from uuid import uuid4
import re
from .message import Message from . message import Message, Tunables
from .rag import ChromaDBFileWatcher from . rag import ChromaDBFileWatcher
from . import defines from . import defines
from . import tools as Tools
from .agents import AnyAgent from . agents import AnyAgent
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,12 +29,11 @@ class Context(BaseModel):
user_resume: Optional[str] = None user_resume: Optional[str] = None
user_job_description: Optional[str] = None user_job_description: Optional[str] = None
user_facts: Optional[str] = None user_facts: Optional[str] = None
tools: List[dict] = [] tools: List[dict] = Tools.enabled_tools(Tools.tools)
rags: List[dict] = [] rags: List[dict] = []
message_history_length: int = 5 message_history_length: int = 5
context_tokens: int = 0
# Class managed fields # Class managed fields
agents: List[Annotated[Union[*Agent.__subclasses__()], Field(discriminator="agent_type")]] = Field( agents: List[Annotated[Union[*Agent.__subclasses__()], Field(discriminator="agent_type")]] = Field( # type: ignore
default_factory=list default_factory=list
) )
@ -58,10 +56,6 @@ class Context(BaseModel):
agent.set_context(self) agent.set_context(self)
return self return self
def get_optimal_ctx_size(self, context, messages, ctx_buffer = 4096):
ctx = round(context + len(str(messages)) * 3 / 4)
return max(defines.max_context, min(2048, ctx + ctx_buffer))
def generate_rag_results(self, message: Message) -> Generator[Message, None, None]: def generate_rag_results(self, message: Message) -> Generator[Message, None, None]:
""" """
Generate RAG results for the given query. Generate RAG results for the given query.
@ -109,6 +103,7 @@ class Context(BaseModel):
"umap_embedding_2d": umap_2d, "umap_embedding_2d": umap_2d,
"umap_embedding_3d": umap_3d "umap_embedding_3d": umap_3d
}) })
message.response = f"Results from {rag['name']} RAG: {len(chroma_results['documents'])} results."
yield message yield message
if entries == 0: if entries == 0:

View File

@ -1,22 +1,41 @@
from pydantic import BaseModel from pydantic import BaseModel, Field, PrivateAttr # type: ignore
from typing import List from typing import List
from .message import Message from .message import Message
class Conversation(BaseModel): class Conversation(BaseModel):
messages: List[Message] = [] Conversation_messages: List[Message] = Field(default=[], alias="messages")
def add_message(self, message: Message | List[Message]) -> None: def __len__(self):
return len(self.Conversation_messages)
def __iter__(self):
return iter(self.Conversation_messages)
def reset(self):
self.Conversation_messages = []
@property
def messages(self):
"""Return a copy of messages to prevent modification of the internal list."""
raise AttributeError("Cannot directly get messages. Use Conversation.add() or .reset()")
@messages.setter
def messages(self, value):
"""Control how messages can be set, or prevent setting altogether."""
raise AttributeError("Cannot directly set messages. Use Conversation.add() or .reset()")
def add(self, message: Message | List[Message]) -> None:
"""Add a Message(s) to the conversation.""" """Add a Message(s) to the conversation."""
if isinstance(message, Message): if isinstance(message, Message):
self.messages.append(message) self.Conversation_messages.append(message)
else: else:
self.messages.extend(message) self.Conversation_messages.extend(message)
def get_summary(self) -> str: def get_summary(self) -> str:
"""Return a summary of the conversation.""" """Return a summary of the conversation."""
if not self.messages: if not self.Conversation_messages:
return "Conversation is empty." return "Conversation is empty."
summary = f"Conversation:\n" summary = f"Conversation:\n"
for i, message in enumerate(self.messages, 1): for i, message in enumerate(self.Conversation_messages, 1):
summary += f"\nMessage {i}:\n{message.get_summary()}\n" summary += f"\nMessage {i}:\n{message.get_summary()}\n"
return summary return summary

View File

@ -1,30 +1,35 @@
from pydantic import BaseModel, model_validator from pydantic import BaseModel, Field # type: ignore
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
from datetime import datetime, timezone from datetime import datetime, timezone
class Tunables(BaseModel):
enable_rag : bool = Field(default=True) # Enable RAG collection chromadb matching
enable_tools : bool = Field(default=True) # Enable LLM to use tools
enable_context : bool = Field(default=True) # Add <|context|> field to message
class Message(BaseModel): class Message(BaseModel):
# Required # Required
prompt: str # Query to be answered prompt: str # Query to be answered
# Tunables # Tunables
disable_rag: bool = False tunables: Tunables = Field(default_factory=Tunables)
disable_tools: bool = False
# Generated while processing message # Generated while processing message
status: str = "" # Status of the message status: str = "" # Status of the message
preamble: dict[str,str] = {} # Preamble to be prepended to the prompt preamble: dict[str,str] = {} # Preamble to be prepended to the prompt
system_prompt: str = "" # System prompt provided to the LLM system_prompt: str = "" # System prompt provided to the LLM
full_content: str = "" # Full content of the message (preamble + prompt) context_prompt: str = "" # Full content of the message (preamble + prompt)
response: str = "" # LLM response to the preamble + query response: str = "" # LLM response to the preamble + query
metadata: dict[str, Any] = { metadata: Dict[str, Any] = Field(default_factory=lambda: {
"rag": List[dict[str, Any]], "rag": [],
"tools": [],
"eval_count": 0, "eval_count": 0,
"eval_duration": 0, "eval_duration": 0,
"prompt_eval_count": 0, "prompt_eval_count": 0,
"prompt_eval_duration": 0, "prompt_eval_duration": 0,
"ctx_size": 0, "context_size": 0,
} })
network_packets: int = 0 # Total number of streaming packets
network_bytes: int = 0 # Total bytes sent while streaming packets
actions: List[str] = [] # Other session modifying actions performed while processing the message actions: List[str] = [] # Other session modifying actions performed while processing the message
timestamp: datetime = datetime.now(timezone.utc) timestamp: datetime = datetime.now(timezone.utc)

View File

@ -1,4 +1,5 @@
from pydantic import BaseModel, Field, model_validator, PrivateAttr from pydantic import BaseModel # type: ignore
from typing import List, Optional, Dict, Any
import os import os
import glob import glob
from pathlib import Path from pathlib import Path
@ -12,19 +13,18 @@ import time
import hashlib import hashlib
import asyncio import asyncio
import json import json
import pickle import numpy as np # type: ignore
import numpy as np
import re
import chromadb import chromadb
import ollama import ollama
from langchain.text_splitter import CharacterTextSplitter from langchain.text_splitter import CharacterTextSplitter # type: ignore
from sentence_transformers import SentenceTransformer from sentence_transformers import SentenceTransformer # type: ignore
from langchain.schema import Document from langchain.schema import Document # type: ignore
from watchdog.observers import Observer from watchdog.observers import Observer # type: ignore
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler # type: ignore
import umap import umap # type: ignore
from markitdown import MarkItDown from markitdown import MarkItDown # type: ignore
from chromadb.api.models.Collection import Collection # type: ignore
# Import your existing modules # Import your existing modules
if __name__ == "__main__": if __name__ == "__main__":
@ -42,6 +42,12 @@ __all__ = [
DEFAULT_CHUNK_SIZE=750 DEFAULT_CHUNK_SIZE=750
DEFAULT_CHUNK_OVERLAP=100 DEFAULT_CHUNK_OVERLAP=100
class ChromaDBGetResponse(BaseModel):
ids: List[str]
embeddings: Optional[List[List[float]]] = None
documents: Optional[List[str]] = None
metadatas: Optional[List[Dict[str, Any]]] = None
class ChromaDBFileWatcher(FileSystemEventHandler): class ChromaDBFileWatcher(FileSystemEventHandler):
def __init__(self, llm, watch_directory, loop, persist_directory=None, collection_name="documents", def __init__(self, llm, watch_directory, loop, persist_directory=None, collection_name="documents",
chunk_size=DEFAULT_CHUNK_SIZE, chunk_overlap=DEFAULT_CHUNK_OVERLAP, recreate=False): chunk_size=DEFAULT_CHUNK_SIZE, chunk_overlap=DEFAULT_CHUNK_OVERLAP, recreate=False):
@ -52,12 +58,11 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
self.chunk_size = chunk_size self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap self.chunk_overlap = chunk_overlap
self.loop = loop self.loop = loop
self._umap_collection = None self._umap_collection : ChromaDBGetResponse | None = None
self._umap_embedding_2d = [] self._umap_embedding_2d : np.ndarray = []
self._umap_embedding_3d = [] self._umap_embedding_3d : np.ndarray = []
self._umap_model_2d = None self._umap_model_2d : umap.UMAP = None
self._umap_model_3d = None self._umap_model_3d : umap.UMAP = None
self._collection = None
self.md = MarkItDown(enable_plugins=False) # Set to True to enable plugins self.md = MarkItDown(enable_plugins=False) # Set to True to enable plugins
#self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2') #self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
@ -69,7 +74,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
self.is_new_collection = False self.is_new_collection = False
# Initialize ChromaDB collection # Initialize ChromaDB collection
self._collection = self._get_vector_collection(recreate=recreate) self._collection : Collection = self._get_vector_collection(recreate=recreate)
self._update_umaps() self._update_umaps()
# Setup text splitter # Setup text splitter
@ -90,15 +95,15 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
return self._collection return self._collection
@property @property
def umap_collection(self): def umap_collection(self) -> ChromaDBGetResponse | None:
return self._umap_collection return self._umap_collection
@property @property
def umap_embedding_2d(self): def umap_embedding_2d(self) -> np.ndarray:
return self._umap_embedding_2d return self._umap_embedding_2d
@property @property
def umap_embedding_3d(self): def umap_embedding_3d(self) -> np.ndarray:
return self._umap_embedding_3d return self._umap_embedding_3d
@property @property
@ -286,12 +291,12 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
self._umap_embedding_3d = self._umap_model_3d.fit_transform(vectors) self._umap_embedding_3d = self._umap_model_3d.fit_transform(vectors)
logging.info(f"3D UMAP model n_components: {self._umap_model_3d.n_components}") # Should be 3 logging.info(f"3D UMAP model n_components: {self._umap_model_3d.n_components}") # Should be 3
def _get_vector_collection(self, recreate=False): def _get_vector_collection(self, recreate=False) -> Collection:
"""Get or create a ChromaDB collection.""" """Get or create a ChromaDB collection."""
# Initialize ChromaDB client # Initialize ChromaDB client
chroma_client = chromadb.PersistentClient( chroma_client = chromadb.PersistentClient( # type: ignore
path=self.persist_directory, path=self.persist_directory,
settings=chromadb.Settings(anonymized_telemetry=False) settings=chromadb.Settings(anonymized_telemetry=False) # type: ignore
) )
# Check if the collection exists # Check if the collection exists
@ -577,7 +582,7 @@ if __name__ == "__main__":
import defines import defines
# Initialize Ollama client # Initialize Ollama client
llm = ollama.Client(host=defines.ollama_api_url) llm = ollama.Client(host=defines.ollama_api_url) # type: ignore
# Start the file watcher (with initialization) # Start the file watcher (with initialization)
observer, file_watcher = start_file_watcher( observer, file_watcher = start_file_watcher(

View File

@ -8,9 +8,10 @@ def setup_logging(level=defines.logging_level) -> logging.Logger:
os.environ["TORCH_CPP_LOG_LEVEL"] = "ERROR" os.environ["TORCH_CPP_LOG_LEVEL"] = "ERROR"
warnings.filterwarnings("ignore", message="Overriding a previously registered kernel") warnings.filterwarnings("ignore", message="Overriding a previously registered kernel")
warnings.filterwarnings("ignore", message="Warning only once for all operators") warnings.filterwarnings("ignore", message="Warning only once for all operators")
warnings.filterwarnings("ignore", message="Couldn't find ffmpeg or avconv") warnings.filterwarnings("ignore", message=".*Couldn't find ffmpeg or avconv.*")
warnings.filterwarnings("ignore", message="'force_all_finite' was renamed to") warnings.filterwarnings("ignore", message="'force_all_finite' was renamed to")
warnings.filterwarnings("ignore", message="n_jobs value 1 overridden") warnings.filterwarnings("ignore", message="n_jobs value 1 overridden")
warnings.filterwarnings("ignore", message=".*websocket.*is deprecated")
numeric_level = getattr(logging, level.upper(), None) numeric_level = getattr(logging, level.upper(), None)
if not isinstance(numeric_level, int): if not isinstance(numeric_level, int):