Compare commits
No commits in common. "a29f51ac9bca4fa6322471d85e7131f305df0de5" and "b8fbe145c91534e03123803d3ddc6fcc56b62f8e" have entirely different histories.
a29f51ac9b
...
b8fbe145c9
@ -1,4 +1,3 @@
|
||||
*
|
||||
!src
|
||||
!frontend
|
||||
**/node_modules
|
||||
|
13
Dockerfile
13
Dockerfile
@ -292,18 +292,14 @@ RUN { \
|
||||
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 ' fi' ; \
|
||||
echo ' declare once=0' ; \
|
||||
echo ' while true; do'; \
|
||||
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
|
||||
echo ' echo "Launching Backstory server..."'; \
|
||||
echo ' python src/server.py "${@}" || echo "Backstory server died."'; \
|
||||
echo ' echo "Sleeping for 3 seconds."'; \
|
||||
echo ' else'; \
|
||||
echo ' if [[ ${once} -eq 0 ]]; then' ; \
|
||||
echo ' echo "/opt/backstory/block-server exists. Sleeping for 3 seconds."'; \
|
||||
echo ' once=1' ; \
|
||||
echo ' fi' ; \
|
||||
echo ' echo "block-server file exists. Not launching."'; \
|
||||
echo ' fi' ; \
|
||||
echo ' echo "Sleeping for 3 seconds."'; \
|
||||
echo ' sleep 3'; \
|
||||
echo ' done' ; \
|
||||
echo 'fi'; \
|
||||
@ -317,11 +313,6 @@ ENV SYCL_CACHE_PERSISTENT=1
|
||||
ENV PATH=/opt/backstory:$PATH
|
||||
|
||||
COPY /src/ /opt/backstory/src/
|
||||
COPY /frontend/ /opt/backstory/frontend/
|
||||
|
||||
WORKDIR /opt/backstory/frontend
|
||||
RUN npm install --force
|
||||
WORKDIR /opt/backstory
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
|
||||
|
63
docs/jobs/plant-conrervation-specialist.md
Normal file
63
docs/jobs/plant-conrervation-specialist.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Plant Conservation Specialist
|
||||
|
||||
**Organization:** Oregon Botanical Gardens
|
||||
**Location:** Portland, Oregon
|
||||
**Duration:** April 2017 - May 2020
|
||||
|
||||
## Position Overview
|
||||
As Plant Conservation Specialist at the Oregon Botanical Gardens, I managed the institution's ex-situ conservation program for rare and endangered plant species native to the Pacific Northwest. This position bridged scientific research, hands-on horticulture, and public education.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
### Ex-situ Conservation Program
|
||||
- Coordinated conservation collections for 45 rare and endangered plant species
|
||||
- Developed and maintained comprehensive database of accession records, phenology data, and propagation histories
|
||||
- Established genetic management protocols to ensure maximum diversity in conservation collections
|
||||
- Collaborated with Center for Plant Conservation on national rare plant conservation initiatives
|
||||
|
||||
### Propagation & Cultivation
|
||||
- Designed specialized growing environments for challenging species with specific habitat requirements
|
||||
- Experimented with various propagation techniques including tissue culture, specialized seed treatments, and vegetative methods
|
||||
- Maintained detailed documentation of successful and unsuccessful propagation attempts
|
||||
- Achieved first-ever successful cultivation of three critically endangered Oregon wildflowers
|
||||
|
||||
### Reintroduction Planning
|
||||
- Collaborated with federal and state agencies on plant reintroduction strategies
|
||||
- Conducted site assessments to evaluate habitat suitability for reintroductions
|
||||
- Developed monitoring protocols to track survival and reproduction of reintroduced populations
|
||||
- Prepared detailed reintroduction plans for 8 endangered species
|
||||
|
||||
### Research Projects
|
||||
- Designed and implemented germination studies for 15 rare species with unknown propagation requirements
|
||||
- Conducted pollination biology investigations for several endangered plant species
|
||||
- Collaborated with university researchers on seed viability and longevity studies
|
||||
- Maintained comprehensive records of phenological patterns across multiple growing seasons
|
||||
|
||||
### Education & Outreach
|
||||
- Developed educational materials explaining the importance of plant conservation
|
||||
- Led specialized tours focusing on rare plant conservation for visitors and donors
|
||||
- Trained volunteers in proper care of sensitive plant collections
|
||||
- Created interpretive signage for conservation garden displays
|
||||
|
||||
## Notable Projects
|
||||
|
||||
1. **Willamette Valley Prairie Species Recovery**
|
||||
- Established seed bank of 25 declining prairie species
|
||||
- Developed germination protocols that improved propagation success from 30% to 75%
|
||||
- Produced over 5,000 plants for restoration projects throughout the region
|
||||
|
||||
2. **Alpine Rare Plant Conservation Initiative**
|
||||
- Created specialized growing facilities mimicking alpine conditions
|
||||
- Successfully propagated 8 high-elevation rare species never before cultivated
|
||||
- Documented critical temperature and moisture requirements for germination
|
||||
|
||||
3. **Serpentine Soils Conservation Collection**
|
||||
- Developed custom soil mixes replicating challenging serpentine conditions
|
||||
- Maintained living collection of 12 rare serpentine endemic species
|
||||
- Created public display educating visitors about specialized plant adaptations
|
||||
|
||||
## Achievements
|
||||
- Received "Conservation Innovation Award" from the American Public Gardens Association (2019)
|
||||
- Developed propagation protocol for Kincaid's lupine that doubled germination success rates
|
||||
- Established Oregon Botanical Gardens' first dedicated conservation nursery facility
|
||||
- Created seed banking protocols adopted by three other botanical institutions
|
75
docs/jobs/research-assistant.md
Normal file
75
docs/jobs/research-assistant.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Research Assistant
|
||||
|
||||
**Organization:** Institute for Applied Ecology
|
||||
**Location:** Corvallis, Oregon
|
||||
**Duration:** January 2015 - March 2017
|
||||
|
||||
## Position Overview
|
||||
As Research Assistant at the Institute for Applied Ecology, I supported multiple research projects focused on native plant ecology and restoration techniques. This position provided foundational experience in applying scientific methods to practical conservation challenges.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
### Field Surveys
|
||||
- Conducted comprehensive botanical surveys in diverse ecosystems throughout western Oregon
|
||||
- Documented population sizes, health metrics, and habitat conditions for threatened plant species
|
||||
- Established long-term monitoring plots using standardized protocols
|
||||
- Collected voucher specimens for herbarium collections following strict ethical guidelines
|
||||
- Mapped plant populations using GPS and GIS technologies
|
||||
|
||||
### Greenhouse Operations
|
||||
- Assisted with propagation of native plants for restoration experiments and projects
|
||||
- Maintained detailed records of seed treatments, germination rates, and growth parameters
|
||||
- Implemented and monitored experimental growing conditions for research projects
|
||||
- Managed irrigation systems and pest control for approximately 10,000 plants
|
||||
- Prepared plant materials for outplanting at restoration sites
|
||||
|
||||
### Data Collection & Analysis
|
||||
- Collected vegetation data using quadrat, transect, and plot-based sampling methods
|
||||
- Processed and organized large datasets for long-term monitoring studies
|
||||
- Performed statistical analyses using R to assess restoration treatment effectiveness
|
||||
- Created data visualization graphics for reports and publications
|
||||
- Maintained research databases ensuring data quality and accessibility
|
||||
|
||||
### Research Projects
|
||||
- **Prairie Restoration Techniques:**
|
||||
- Compared effectiveness of different site preparation methods on native plant establishment
|
||||
- Monitored post-treatment recovery of native species diversity
|
||||
- Documented invasive species response to various control techniques
|
||||
|
||||
- **Rare Plant Demography:**
|
||||
- Tracked population dynamics of three endangered Willamette Valley plant species
|
||||
- Monitored individual plant survival, growth, and reproductive output
|
||||
- Assessed impacts of management interventions on population trends
|
||||
|
||||
- **Seed Viability Studies:**
|
||||
- Tested germination requirements for 30+ native species
|
||||
- Evaluated effects of smoke, scarification, and stratification on dormancy
|
||||
- Documented optimal storage conditions for maintaining seed viability
|
||||
|
||||
### Publication Support
|
||||
- Co-authored three peer-reviewed publications on prairie restoration techniques
|
||||
- Prepared figures, tables, and data appendices for manuscripts
|
||||
- Conducted literature reviews on specialized ecological topics
|
||||
- Assisted with manuscript revisions based on peer review feedback
|
||||
|
||||
## Key Projects
|
||||
|
||||
1. **Willamette Valley Wet Prairie Restoration**
|
||||
- Implemented experimental plots testing 4 restoration techniques
|
||||
- Collected 3 years of post-treatment vegetation data
|
||||
- Documented successful establishment of 15 target native species
|
||||
|
||||
2. **Endangered Butterfly Habitat Enhancement**
|
||||
- Propagated host and nectar plants for Fender's blue butterfly habitat
|
||||
- Monitored plant-insect interactions in restoration sites
|
||||
- Assessed habitat quality improvements following restoration treatments
|
||||
|
||||
3. **Native Seed Production Research**
|
||||
- Tested cultivation methods for improving seed yields of 10 native species
|
||||
- Documented pollination requirements for optimal seed production
|
||||
- Developed harvest timing recommendations based on seed maturation patterns
|
||||
|
||||
## Publications
|
||||
- Johnson, T., **Morgan, E.**, et al. (2016). "Comparative effectiveness of site preparation techniques for prairie restoration." *Restoration Ecology*, 24(4), 472-481.
|
||||
- Williams, R., **Morgan, E.**, & Smith, B. (2016). "Germination requirements of Willamette Valley wet prairie species." *Native Plants Journal*, 17(2), 99-112.
|
||||
- **Morgan, E.**, Johnson, T., & Davis, A. (2017). "Long-term vegetation response to restoration treatments in degraded oak savanna." *Northwest Science*, 91(1), 27-39.
|
55
docs/jobs/restoration-botanist.md
Normal file
55
docs/jobs/restoration-botanist.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Senior Restoration Botanist
|
||||
|
||||
**Organization:** Pacific Northwest Conservation Alliance
|
||||
**Location:** Portland, Oregon
|
||||
**Duration:** June 2020 - Present
|
||||
|
||||
## Position Overview
|
||||
As Senior Restoration Botanist at the Pacific Northwest Conservation Alliance, I lead complex restoration projects aimed at preserving endangered plant communities throughout the Cascade Range. This role combines technical botanical expertise with project management and leadership responsibilities.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
### Project Leadership
|
||||
- Design and implement comprehensive restoration plans for degraded ecosystems with emphasis on rare plant conservation
|
||||
- Lead field operations across multiple concurrent restoration sites covering over 2,000 acres
|
||||
- Establish measurable success criteria and monitoring protocols for all restoration projects
|
||||
- Conduct regular site assessments to track progress and adapt management strategies
|
||||
|
||||
### Native Plant Propagation
|
||||
- Oversee native plant nursery operations producing 75,000+ plants annually
|
||||
- Develop specialized propagation protocols for difficult-to-grow rare species
|
||||
- Maintain detailed records of germination rates, growth metrics, and treatment effects
|
||||
- Coordinate seed collection expeditions throughout diverse ecosystems of the Pacific Northwest
|
||||
|
||||
### Team Management
|
||||
- Supervise a core team of 5 field botanists and up to 12 seasonal restoration technicians
|
||||
- Conduct staff training on plant identification, restoration techniques, and field safety
|
||||
- Facilitate weekly team meetings and monthly progress reviews
|
||||
- Mentor junior staff and provide professional development opportunities
|
||||
|
||||
### Funding & Partnerships
|
||||
- Secured $750,000 in grant funding for riparian habitat restoration projects
|
||||
- Authored major sections of successful proposals to state and federal agencies
|
||||
- Manage project budgets ranging from $50,000 to $250,000
|
||||
- Cultivate partnerships with government agencies, tribes, and conservation NGOs
|
||||
|
||||
### Notable Projects
|
||||
1. **Willamette Valley Prairie Restoration Initiative**
|
||||
- Restored 350 acres of native prairie habitat
|
||||
- Reintroduced 12 threatened plant species with 85% establishment success
|
||||
- Developed innovative seeding techniques that increased native diversity by 40%
|
||||
|
||||
2. **Mount Hood Meadow Rehabilitation**
|
||||
- Led post-wildfire recovery efforts in alpine meadow ecosystems
|
||||
- Implemented erosion control measures using native plant materials
|
||||
- Achieved 90% reduction in invasive species cover within treatment areas
|
||||
|
||||
3. **Columbia River Gorge Rare Plant Recovery**
|
||||
- Established new populations of 5 federally listed plant species
|
||||
- Developed habitat suitability models to identify optimal reintroduction sites
|
||||
- Created monitoring protocols adopted by multiple conservation organizations
|
||||
|
||||
## Achievements
|
||||
- Received Excellence in Ecological Restoration Award from the Society for Ecological Restoration, Northwest Chapter (2023)
|
||||
- Featured in Oregon Public Broadcasting documentary on native plant conservation (2022)
|
||||
- Published 2 peer-reviewed articles on restoration techniques developed during project work
|
@ -1,272 +1,78 @@
|
||||
# JAMES KETRENOS
|
||||
software architect, designer, developer, and team lead
|
||||
Beaverton, OR 97003
|
||||
Resume - Eliza Morgan
|
||||
|
||||
james@ketrenos.com
|
||||
(503) 501 8281
|
||||
Professional Profile
|
||||
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.
|
||||
Contact Information
|
||||
|
||||
Seeking an opportunity to contribute to the advancement of energy efficient AI solutions,
|
||||
James is a driven problem solver, solution creator, technical leader, and skilled software
|
||||
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 Intel’s 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.
|
||||
Email: eliza.morgan@botanist.com
|
||||
Phone: (555) 782-3941
|
||||
Address: 427 Maple Street, Portland, OR 97205
|
||||
|
||||
Education
|
||||
* Attended University of California San Diego, Oregon State University, and Portland State University
|
||||
* During senior year in college, James began working full time at Intel and stopped pursuing a degree.
|
||||
University of Washington, Seattle
|
||||
Master of Science in Botany - 2015
|
||||
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
|
||||
|
||||
Personal Projects
|
||||
Lead restoration projects for endangered plant communities in the Cascade Range
|
||||
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
|
||||
|
||||
Eikona Android App | 2016 - 2024
|
||||
* Developed Android port of Eikona application utilizing Java, Expo, and React technologies.
|
||||
* Maintains new releases targeting the latest Android APIs.
|
||||
Plant Conservation Specialist
|
||||
Oregon Botanical Gardens — Portland, OR
|
||||
April 2017 - May 2020
|
||||
|
||||
System Administrator | 1995 - present
|
||||
* Maintains a small cluster of servers providing email, photo management, game servers, and generative AI deployments.
|
||||
Coordinated ex-situ conservation program for 45 rare and endangered plant species
|
||||
Maintained detailed documentation of propagation techniques and germination rates
|
||||
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
|
||||
|
||||
|
162
frontend/package-lock.json
generated
162
frontend/package-lock.json
generated
@ -38,7 +38,6 @@
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@types/plotly.js": "^2.35.5"
|
||||
}
|
||||
},
|
||||
@ -1975,35 +1974,12 @@
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"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": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
@ -2015,7 +1991,8 @@
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
@ -4429,25 +4406,29 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@turf/area": {
|
||||
"version": "7.2.0",
|
||||
@ -6818,20 +6799,6 @@
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@ -7279,25 +7246,6 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz",
|
||||
@ -7308,7 +7256,8 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
@ -8189,7 +8138,8 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@ -9936,15 +9886,6 @@
|
||||
"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": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
@ -11966,18 +11907,6 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
@ -12175,15 +12104,6 @@
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"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": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@ -13607,7 +13527,8 @@
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
@ -18782,18 +18703,6 @@
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz",
|
||||
@ -20247,7 +20156,8 @@
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@ -20290,7 +20200,8 @@
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
@ -20302,7 +20213,8 @@
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
@ -20825,7 +20737,8 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"devOptional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "8.1.1",
|
||||
@ -21168,20 +21081,6 @@
|
||||
"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": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
@ -21368,12 +21267,6 @@
|
||||
"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": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@ -21825,7 +21718,8 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -56,7 +56,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@types/plotly.js": "^2.35.5"
|
||||
"@types/plotly.js": "^2.35.5",
|
||||
"@craco/craco": "^0.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -23,10 +23,10 @@ The backstory about Backstory...
|
||||
|
||||
## Some questions I've been asked
|
||||
|
||||
Q. <ChatQuery prompt="Why aren't you providing this as a Platform As a Service (PaaS) application?" tunables={{ "enable_tools": false }} />
|
||||
Q. <ChatQuery text="Why aren't you providing this as a Platform As a Service (PaaS) application?"/>
|
||||
|
||||
A. I could; but I don't want to store your data. I also don't want to have to be on the hook for support of this service. I like it, it's fun, but it's not what I want as my day-gig, you know? If it was, I wouldn't be looking for a job...
|
||||
|
||||
Q. <ChatQuery prompt="Why can't I just ask Backstory these questions?" tunables={{ "enable_tools": false }} />
|
||||
Q. <ChatQuery text="Why can't I just ask Backstory these questions?"/>
|
||||
|
||||
A. Try it. See what you find out :)
|
@ -19,12 +19,11 @@ import { SxProps } from '@mui/material';
|
||||
|
||||
|
||||
import { ResumeBuilder } from './ResumeBuilder';
|
||||
import { Message, MessageList } from './Message';
|
||||
import { Message, ChatQuery, MessageList } from './Message';
|
||||
import { Snack, SeverityType } from './Snack';
|
||||
import { VectorVisualizer } from './VectorVisualizer';
|
||||
import { Controls } from './Controls';
|
||||
import { Conversation, ConversationHandle } from './Conversation';
|
||||
import { ChatQuery, QueryOptions } from './ChatQuery';
|
||||
import { Scrollable } from './AutoScroll';
|
||||
import { BackstoryTab } from './BackstoryTab';
|
||||
|
||||
@ -113,9 +112,9 @@ const App = () => {
|
||||
fetchAbout();
|
||||
}, [about, setAbout])
|
||||
|
||||
const handleSubmitChatQuery = (prompt: string, tunables?: QueryOptions) => {
|
||||
console.log(`handleSubmitChatQuery: ${prompt} ${tunables || {}} -- `, chatRef.current ? ' sending' : 'no handler');
|
||||
chatRef.current?.submitQuery(prompt, tunables);
|
||||
const handleSubmitChatQuery = (query: string) => {
|
||||
console.log(`handleSubmitChatQuery: ${query} -- `, chatRef.current ? ' sending' : 'no handler');
|
||||
chatRef.current?.submitQuery(query);
|
||||
setActiveTab(0);
|
||||
};
|
||||
|
||||
@ -138,10 +137,10 @@ const App = () => {
|
||||
|
||||
const backstoryQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
<ChatQuery prompt="What is James Ketrenos' work history?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery prompt="What programming languages has James used?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery prompt="What are James' professional strengths?" tunables={{ enable_tools: false }} submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery prompt="What are today's headlines on CNBC.com?" tunables={{ enable_tools: true, enable_rag: false, enable_context: false }} submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What is James Ketrenos' work history?" submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What programming languages has James used?" submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What are James' professional strengths?" submitQuery={handleSubmitChatQuery} />
|
||||
<ChatQuery text="What are today's headlines on CNBC.com?" submitQuery={handleSubmitChatQuery} />
|
||||
</Box>,
|
||||
<Box sx={{ p: 1 }}>
|
||||
<MuiMarkdown>
|
||||
@ -256,20 +255,11 @@ const App = () => {
|
||||
icon: <SettingsIcon />
|
||||
},
|
||||
children: (
|
||||
<Scrollable
|
||||
autoscroll={false}
|
||||
sx={{
|
||||
maxWidth: "1024px",
|
||||
height: "calc(100vh - 72px)",
|
||||
flexDirection: "column",
|
||||
margin: "0 auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Box className="ChatBox">
|
||||
{sessionId !== undefined &&
|
||||
<Controls {...{ sessionId, setSnack, connectionBase }} />
|
||||
}
|
||||
</Scrollable>
|
||||
</Box >
|
||||
)
|
||||
}];
|
||||
}, [about, connectionBase, sessionId, setSnack, isMobile]);
|
||||
|
@ -6,10 +6,8 @@ import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
|
||||
|
||||
import { MessageRoles } from './Message';
|
||||
import { ErrorOutline, InfoOutline, Memory, Psychology, /* Stream, */ } from '@mui/icons-material';
|
||||
|
||||
interface ChatBubbleProps {
|
||||
role: MessageRoles,
|
||||
@ -22,7 +20,7 @@ interface ChatBubbleProps {
|
||||
}
|
||||
|
||||
function ChatBubble(props: ChatBubbleProps) {
|
||||
const { role, isFullWidth, children, sx, className, title }: ChatBubbleProps = props;
|
||||
const { role, isFullWidth, children, sx, className, title } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const defaultRadius = '16px';
|
||||
@ -118,28 +116,6 @@ function ChatBubble(props: ChatBubbleProps) {
|
||||
lineHeight: '1.3', // More compact line height
|
||||
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) {
|
||||
@ -163,11 +139,8 @@ function ChatBubble(props: ChatBubbleProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={className} sx={{ ...(styles[role] !== undefined ? styles[role] : styles["status"]), gap: 1, display: "flex", ...sx, flexDirection: "row" }}>
|
||||
{icons[role] !== undefined && icons[role]}
|
||||
<Box sx={{ p: 0, m: 0, gap: 0, display: "flex", flexGrow: 1, flexDirection: "column" }}>
|
||||
{children}
|
||||
</Box>
|
||||
<Box className={className} sx={{ ...styles[role], ...sx }}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
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,
|
||||
};
|
||||
|
@ -223,7 +223,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
}, [systemInfo, setSystemInfo, connectionBase, setSnack, sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
setEditSystemPrompt(systemPrompt.trim());
|
||||
setEditSystemPrompt(systemPrompt);
|
||||
}, [systemPrompt, setEditSystemPrompt]);
|
||||
|
||||
const toggleRag = async (tool: Tool) => {
|
||||
@ -314,36 +314,39 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
|
||||
const handleKeyPress = (event: any) => {
|
||||
if (event.key === 'Enter' && event.ctrlKey) {
|
||||
setSystemPrompt(editSystemPrompt);
|
||||
switch (event.target.id) {
|
||||
case 'SystemPromptInput':
|
||||
setSystemPrompt(editSystemPrompt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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:
|
||||
|
||||
</Typography>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">System Prompt</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionActions style={{ display: "flex", flexDirection: "column" }}>
|
||||
<AccordionActions style={{ flexDirection: "column" }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
slotProps={{
|
||||
htmlInput: { style: { fontSize: "0.85rem", lineHeight: "1.25rem" } }
|
||||
}}
|
||||
type="text"
|
||||
value={editSystemPrompt}
|
||||
onChange={(e) => setEditSystemPrompt(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Enter the new system prompt.."
|
||||
id="SystemPromptInput"
|
||||
/>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
||||
<Button variant="contained" disabled={editSystemPrompt.trim() === systemPrompt.trim()} onClick={() => { setSystemPrompt(editSystemPrompt.trim()); }}>Set</Button>
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
|
||||
<Button variant="contained" disabled={editSystemPrompt === systemPrompt} onClick={() => { setSystemPrompt(editSystemPrompt); }}>Set</Button>
|
||||
<Button variant="outlined" onClick={() => { reset(["system_prompt"], "System prompt reset."); }} color="error">Reset</Button>
|
||||
</Box>
|
||||
</div>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
|
||||
@ -414,8 +417,7 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
)
|
||||
}</FormGroup>
|
||||
</AccordionActions>
|
||||
</Accordion> */}
|
||||
|
||||
</Accordion>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography component="span">System Information</Typography>
|
||||
@ -427,9 +429,8 @@ const Controls = ({ sessionId, setSnack, connectionBase }: ControlsParams) => {
|
||||
<SystemInfoComponent systemInfo={systemInfo} />
|
||||
</AccordionActions>
|
||||
</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>);
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { SetSnackType } from './Snack';
|
||||
import { ContextStatus } from './ContextStatus';
|
||||
import { useAutoScrollToBottom } from './AutoScroll';
|
||||
import { DeleteConfirmation } from './DeleteConfirmation';
|
||||
import { QueryOptions } from './ChatQuery';
|
||||
|
||||
import './Conversation.css';
|
||||
|
||||
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';
|
||||
|
||||
interface ConversationHandle {
|
||||
submitQuery: (prompt: string, options?: QueryOptions) => void;
|
||||
submitQuery: (query: string) => void;
|
||||
}
|
||||
|
||||
interface BackstoryMessage {
|
||||
@ -54,7 +54,6 @@ interface ConversationProps {
|
||||
setSnack: SetSnackType, // Callback to display snack popups
|
||||
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
|
||||
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
|
||||
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
|
||||
@ -68,7 +67,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
className,
|
||||
type,
|
||||
prompt,
|
||||
emptyPrompt,
|
||||
actionLabel,
|
||||
resetAction,
|
||||
multiline,
|
||||
@ -203,13 +201,17 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
// isProcessing?: boolean,
|
||||
// metadata?: MessageMetaData
|
||||
// };
|
||||
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => [{
|
||||
setConversation(backstoryMessages.flatMap((message: BackstoryMessage) => [{
|
||||
role: 'user',
|
||||
content: backstoryMessage.prompt || "",
|
||||
content: message.prompt || "",
|
||||
}, {
|
||||
...backstoryMessage,
|
||||
role: 'assistant',
|
||||
content: backstoryMessage.response || "",
|
||||
prompt: message.prompt || "",
|
||||
preamble: message.preamble || {},
|
||||
full_content: message.full_content || "",
|
||||
content: message.response || "",
|
||||
metadata: message.metadata,
|
||||
actions: message.actions,
|
||||
}] as MessageList));
|
||||
setNoInteractions(false);
|
||||
}
|
||||
@ -258,8 +260,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submitQuery: (query: string, tunables?: QueryOptions) => {
|
||||
sendQuery(query, tunables);
|
||||
submitQuery: (query: string) => {
|
||||
sendQuery(query);
|
||||
}
|
||||
}));
|
||||
|
||||
@ -305,34 +307,38 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
}
|
||||
};
|
||||
|
||||
const sendQuery = async (request: string, options?: QueryOptions) => {
|
||||
request = request.trim();
|
||||
const sendQuery = async (query: string) => {
|
||||
query = query.trim();
|
||||
|
||||
// If the query was empty, a default query was provided,
|
||||
// and there is no prompt for the user, send the default query.
|
||||
if (!request && defaultQuery && !prompt) {
|
||||
request = defaultQuery.trim();
|
||||
if (!query && defaultQuery && !prompt) {
|
||||
query = defaultQuery.trim();
|
||||
}
|
||||
|
||||
// Do not send an empty query.
|
||||
if (!request) {
|
||||
// If the query is empty, and a prompt was provided, do not
|
||||
// send an empty query.
|
||||
if (!query && prompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNoInteractions(false);
|
||||
|
||||
setConversation([
|
||||
...conversationRef.current,
|
||||
{
|
||||
role: 'user',
|
||||
origin: type,
|
||||
content: request,
|
||||
disableCopy: true
|
||||
}
|
||||
]);
|
||||
if (query) {
|
||||
setConversation([
|
||||
...conversationRef.current,
|
||||
{
|
||||
role: 'user',
|
||||
origin: type,
|
||||
content: query,
|
||||
disableCopy: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
console.log(conversation);
|
||||
|
||||
// Clear input
|
||||
setQuery('');
|
||||
@ -351,25 +357,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
// Make the fetch request with proper headers
|
||||
let query;
|
||||
if (options) {
|
||||
query = {
|
||||
options: options,
|
||||
prompt: request.trim()
|
||||
}
|
||||
} else {
|
||||
query = {
|
||||
prompt: request.trim()
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(connectionBase + `/api/chat/${sessionId}/${type}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query)
|
||||
body: JSON.stringify({ role: 'user', content: query.trim() }),
|
||||
});
|
||||
|
||||
// We'll guess that the response will be around 500 tokens...
|
||||
@ -393,23 +387,39 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
let buffer = '';
|
||||
|
||||
const process_line = async (line: string) => {
|
||||
let update = JSON.parse(line);
|
||||
const update = JSON.parse(line);
|
||||
|
||||
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':
|
||||
console.log('Done processing:', update);
|
||||
// Replace processing message with final result
|
||||
if (onResponse) {
|
||||
update = onResponse(update);
|
||||
update.message = onResponse(update);
|
||||
}
|
||||
setProcessingMessage(undefined);
|
||||
const backstoryMessage: BackstoryMessage = update;
|
||||
setConversation([
|
||||
...conversationRef.current, {
|
||||
...backstoryMessage,
|
||||
// role: 'user',
|
||||
// content: backstoryMessage.prompt || "",
|
||||
// }, {
|
||||
role: 'assistant',
|
||||
origin: type,
|
||||
content: backstoryMessage.response || "",
|
||||
prompt: backstoryMessage.prompt || "",
|
||||
preamble: backstoryMessage.preamble || {},
|
||||
full_content: backstoryMessage.full_content || "",
|
||||
metadata: backstoryMessage.metadata,
|
||||
actions: backstoryMessage.actions,
|
||||
}] as MessageList);
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
@ -433,13 +443,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
// Add a small delay to ensure React has time to update the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,7 +464,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
await process_line(line);
|
||||
} catch (e) {
|
||||
setSnack("Error processing query", "error")
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -472,7 +474,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
await process_line(buffer);
|
||||
} catch (e) {
|
||||
setSnack("Error processing query", "error")
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,7 +516,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
m: 1,
|
||||
mb: 1,
|
||||
}}>
|
||||
<PropagateLoader
|
||||
size="10px"
|
||||
@ -591,7 +592,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(({
|
||||
|
||||
export type {
|
||||
ConversationProps,
|
||||
ConversationHandle,
|
||||
ConversationHandle
|
||||
};
|
||||
|
||||
export {
|
||||
|
@ -28,11 +28,12 @@ import { VectorVisualizer } from './VectorVisualizer';
|
||||
import { SetSnackType } from './Snack';
|
||||
import { CopyBubble } from './CopyBubble';
|
||||
|
||||
type MessageRoles = 'info' | 'user' | 'assistant' | 'system' | 'status' | 'error' | 'content' | 'thinking' | 'processing';
|
||||
type MessageRoles = 'info' | 'user' | 'assistant' | 'system' | 'status' | 'error' | 'content';
|
||||
|
||||
type MessageData = {
|
||||
role: MessageRoles,
|
||||
content: string,
|
||||
full_content?: string,
|
||||
|
||||
disableCopy?: boolean,
|
||||
user?: string,
|
||||
@ -51,9 +52,7 @@ interface MessageMetaData {
|
||||
},
|
||||
origin: string,
|
||||
rag: any,
|
||||
tools?: {
|
||||
tool_calls: any[],
|
||||
},
|
||||
tools: any[],
|
||||
eval_count: number,
|
||||
eval_duration: number,
|
||||
prompt_eval_count: number,
|
||||
@ -76,6 +75,11 @@ interface MessageProps {
|
||||
className?: string,
|
||||
};
|
||||
|
||||
interface ChatQueryInterface {
|
||||
text: string,
|
||||
submitQuery?: (text: string) => void
|
||||
}
|
||||
|
||||
interface MessageMetaProps {
|
||||
metadata: MessageMetaData,
|
||||
messageProps: MessageProps
|
||||
@ -95,49 +99,59 @@ const MessageMeta = (props: MessageMetaProps) => {
|
||||
const message = props.messageProps.message;
|
||||
|
||||
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>
|
||||
|
||||
{
|
||||
prompt_eval_duration !== 0 && eval_duration !== 0 && <>
|
||||
<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 &&
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
Full Query
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<pre style={{ "display": "block", "position": "relative" }}><CopyBubble content={message.full_content?.trim()} />{message.full_content?.trim()}</pre>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
}
|
||||
{
|
||||
tools !== undefined && tools.tool_calls && tools.tool_calls.length !== 0 &&
|
||||
tools !== undefined && tools.length !== 0 &&
|
||||
<Accordion sx={{ boxSizing: "border-box" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
@ -145,24 +159,26 @@ const MessageMeta = (props: MessageMetaProps) => {
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{
|
||||
tools.tool_calls.map((tool: any, index: number) =>
|
||||
<Box key={index} sx={{ m: 0, p: 1, pt: 0, display: "flex", flexDirection: "column", border: "1px solid #e0e0e0" }}>
|
||||
{index !== 0 && <Divider />}
|
||||
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 1, mb: 1, fontWeight: "bold" }}>
|
||||
{tool.name}
|
||||
</Box>
|
||||
<JsonView displayDataTypes={false} objectSortKeys={true} collapsed={1} value={JSON.parse(tool.content)} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }}>
|
||||
<JsonView.String
|
||||
render={({ children, ...reset }) => {
|
||||
if (typeof (children) === "string" && children.match("\n")) {
|
||||
return <pre {...reset} style={{ display: "flex", border: "none", ...reset.style }}>{children}</pre>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</JsonView>
|
||||
</Box>)
|
||||
}
|
||||
{tools.map((tool: any, index: number) => <Box key={index}>
|
||||
{index !== 0 && <Divider />}
|
||||
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 0.5 }}>
|
||||
<div style={{ display: "flex", paddingRight: "1rem", whiteSpace: "nowrap" }}>
|
||||
{tool.tool}
|
||||
</div>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
padding: "3px",
|
||||
whiteSpace: "pre-wrap",
|
||||
flexGrow: 1,
|
||||
border: "1px solid #E0E0E0",
|
||||
wordBreak: "break-all",
|
||||
maxHeight: "5rem",
|
||||
overflow: "auto"
|
||||
}}>
|
||||
{JSON.stringify(tool.result, null, 2)}
|
||||
</div>
|
||||
</Box>
|
||||
</Box>)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
}
|
||||
@ -200,24 +216,46 @@ const MessageMeta = (props: MessageMetaProps) => {
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
Full Response Details
|
||||
All response fields
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<JsonView displayDataTypes={false} objectSortKeys={true} collapsed={1} value={message} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }}>
|
||||
<JsonView.String
|
||||
render={({ children, ...reset }) => {
|
||||
if (typeof (children) === "string" && children.match("\n")) {
|
||||
return <pre {...reset} style={{ display: "inline", border: "none", ...reset.style }}>{children.trim()}</pre>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</JsonView>
|
||||
{Object.entries(message)
|
||||
.filter(([key, value]) => key !== undefined && value !== undefined)
|
||||
.map(([key, value]) => (typeof (value) !== "string" || value?.trim() !== "") &&
|
||||
<Accordion key={key}>
|
||||
<AccordionSummary sx={{ fontSize: "1rem", fontWeight: "bold" }} expandIcon={<ExpandMoreIcon />}>
|
||||
{key}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{typeof (value) === "string" ?
|
||||
<pre>{value}</pre> :
|
||||
<JsonView collapsed={1} value={value as any} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }} />
|
||||
}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</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, submitQuery, isFullWidth, sx, className } = props;
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
@ -302,12 +340,14 @@ const Message = (props: MessageProps) => {
|
||||
export type {
|
||||
MessageProps,
|
||||
MessageList,
|
||||
ChatQueryInterface,
|
||||
MessageData,
|
||||
MessageRoles
|
||||
};
|
||||
|
||||
export {
|
||||
Message,
|
||||
ChatQuery,
|
||||
MessageMeta
|
||||
};
|
||||
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
import { ChatQuery } from './ChatQuery';
|
||||
import { ChatQuery } from './Message';
|
||||
import { MessageList, MessageData } from './Message';
|
||||
import { SetSnackType } from './Snack';
|
||||
import { Conversation } from './Conversation';
|
||||
@ -97,12 +97,6 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
if (messages === undefined || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
console.log("filterJobDescriptionMessages disabled")
|
||||
if (messages.length > 1) {
|
||||
setHasResume(true);
|
||||
}
|
||||
|
||||
return messages;
|
||||
|
||||
let reduced = messages.filter((m, i) => {
|
||||
const keep = (m.metadata?.origin || m.origin || "no origin") === 'job_description';
|
||||
@ -141,11 +135,6 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
if (messages === undefined || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
console.log("filterResumeMessages disabled")
|
||||
if (messages.length > 3) {
|
||||
setHasFacts(true);
|
||||
}
|
||||
return messages;
|
||||
|
||||
let reduced = messages.filter((m, i) => {
|
||||
const keep = (m.metadata?.origin || m.origin || "no origin") === 'resume';
|
||||
@ -193,9 +182,6 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
if (messages === undefined || messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
console.log("filterFactsMessages disabled")
|
||||
return messages;
|
||||
|
||||
// messages.forEach((m, i) => console.log(`filterFactsMessages: ${i + 1}:`, m))
|
||||
|
||||
const reduced = messages.filter(m => {
|
||||
@ -254,8 +240,8 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
console.log('renderJobDescriptionView');
|
||||
const jobDescriptionQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||
<ChatQuery prompt="What are the key skills necessary for this position?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery prompt="How much should this position pay (accounting for inflation)?" tunables={{ enable_tools: false }} submitQuery={handleJobQuery} />
|
||||
<ChatQuery text="What are the key skills necessary for this position?" submitQuery={handleJobQuery} />
|
||||
<ChatQuery text="How much should this position pay (accounting for inflation)?" submitQuery={handleJobQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
@ -303,8 +289,8 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
const renderResumeView = useCallback((small: boolean) => {
|
||||
const resumeQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||
<ChatQuery prompt="Is this resume a good fit for the provided job description?" tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery prompt="Provide a more concise resume." tunables={{ enable_tools: false }} submitQuery={handleResumeQuery} />
|
||||
<ChatQuery text="Is this resume a good fit for the provided job description?" submitQuery={handleResumeQuery} />
|
||||
<ChatQuery text="Provide a more concise resume." submitQuery={handleResumeQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
@ -312,9 +298,9 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
return <Conversation
|
||||
ref={resumeConversationRef}
|
||||
{...{
|
||||
type: "resume",
|
||||
actionLabel: "Fact Check",
|
||||
defaultQuery: "Fact check the resume.",
|
||||
multiline: true,
|
||||
type: "resume",
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterResumeMessages,
|
||||
onResponse: resumeResponse,
|
||||
@ -333,12 +319,12 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
prompt: "Ask a question about this job resume...",
|
||||
resetLabel: `job description${hasFacts ? ", resume, and fact check" : hasResume ? " and resume" : ""}`,
|
||||
messageFilter: filterResumeMessages,
|
||||
onResponse: resumeResponse,
|
||||
defaultPrompts: resumeQuestions,
|
||||
resetAction: resetResume,
|
||||
onResponse: resumeResponse,
|
||||
sessionId,
|
||||
connectionBase,
|
||||
setSnack,
|
||||
defaultPrompts: resumeQuestions,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@ -350,7 +336,7 @@ const ResumeBuilder: React.FC<ResumeBuilderProps> = ({
|
||||
const renderFactCheckView = useCallback((small: boolean) => {
|
||||
const factsQuestions = [
|
||||
<Box sx={{ display: "flex", flexDirection: small ? "column" : "row" }}>
|
||||
<ChatQuery prompt="Rewrite the resume to address any discrepancies." tunables={{ enable_tools: false }} submitQuery={handleFactsQuery} />
|
||||
<ChatQuery text="Rewrite the resume to address any discrepancies." submitQuery={handleFactsQuery} />
|
||||
</Box>,
|
||||
];
|
||||
|
||||
|
@ -2,12 +2,12 @@ import React from 'react';
|
||||
import { MuiMarkdown } from 'mui-markdown';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Link } from '@mui/material';
|
||||
import { ChatQuery, QueryOptions } from './ChatQuery';
|
||||
import { ChatQuery } from './Message';
|
||||
|
||||
interface StyledMarkdownProps {
|
||||
className?: string,
|
||||
content: string,
|
||||
submitQuery?: (prompt: string, tunables?: QueryOptions) => void,
|
||||
submitQuery?: (query: string) => void,
|
||||
[key: string]: any, // For any additional props
|
||||
};
|
||||
|
||||
@ -38,7 +38,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = ({ className, content, sub
|
||||
options.overrides.ChatQuery = {
|
||||
component: ChatQuery,
|
||||
props: {
|
||||
submitQuery,
|
||||
submitQuery
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
|
||||
useEffect(() => {
|
||||
if (!result || !result.embeddings) return;
|
||||
if (result.embeddings.length === 0) return;
|
||||
|
||||
console.log('Result:', result);
|
||||
const vectors: (number[])[] = [...result.embeddings];
|
||||
const documents = [...result.documents || []];
|
||||
const metadatas = [...result.metadatas || []];
|
||||
|
465
src/server.py
465
src/server.py
@ -1,6 +1,6 @@
|
||||
from utils import logger
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar
|
||||
|
||||
# %%
|
||||
# Imports [standard]
|
||||
@ -17,10 +17,6 @@ import re
|
||||
import math
|
||||
import warnings
|
||||
from typing import Any
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
def try_import(module_name, pip_name=None):
|
||||
try:
|
||||
@ -32,6 +28,7 @@ def try_import(module_name, pip_name=None):
|
||||
# Third-party modules with import checks
|
||||
try_import("ollama")
|
||||
try_import("requests")
|
||||
try_import("bs4", "beautifulsoup4")
|
||||
try_import("fastapi")
|
||||
try_import("uvicorn")
|
||||
try_import("numpy")
|
||||
@ -40,23 +37,29 @@ try_import("sklearn")
|
||||
|
||||
import ollama
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, BackgroundTasks # type: ignore
|
||||
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, RedirectResponse # type: ignore
|
||||
from fastapi.middleware.cors import CORSMiddleware # type: ignore
|
||||
import uvicorn # type: ignore
|
||||
import numpy as np # type: ignore
|
||||
import umap # type: ignore
|
||||
from sklearn.preprocessing import MinMaxScaler # type: ignore
|
||||
from fastapi import FastAPI, Request, BackgroundTasks
|
||||
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse, RedirectResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
import numpy as np
|
||||
import umap
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
|
||||
from utils import (
|
||||
rag as Rag,
|
||||
tools as Tools,
|
||||
Context, Conversation, Message,
|
||||
Agent,
|
||||
Tunables,
|
||||
defines,
|
||||
logger,
|
||||
logger
|
||||
)
|
||||
|
||||
from tools import (
|
||||
DateTime,
|
||||
WeatherForecast,
|
||||
TickerValue,
|
||||
tools
|
||||
)
|
||||
|
||||
CONTEXT_VERSION=2
|
||||
@ -66,25 +69,71 @@ rags = [
|
||||
# { "name": "LKML", "enabled": False, "description": "Full associative data for entire LKML mailing list archive." },
|
||||
]
|
||||
|
||||
system_message_old = f"""
|
||||
Launched on {datetime.now().isoformat()}.
|
||||
|
||||
system_message = f"""
|
||||
Launched on {DateTime()}.
|
||||
|
||||
When answering queries, follow these steps:
|
||||
|
||||
1. First analyze the query to determine if real-time information might be helpful
|
||||
2. Even when <|context|> is provided, consider whether the tools would provide more current or comprehensive information
|
||||
3. Use the provided tools whenever they would enhance your response, regardless of whether context is also available
|
||||
4. When presenting weather forecasts, include relevant emojis immediately before the corresponding text. For example, for a sunny day, say \"☀️ Sunny\" or if the forecast says there will be \"rain showers, say \"🌧️ Rain showers\". Use this mapping for weather emojis: Sunny: ☀️, Cloudy: ☁️, Rainy: 🌧️, Snowy: ❄️
|
||||
4. When both <|context|> and tool outputs are relevant, synthesize information from both sources to provide the most complete answer
|
||||
5. Always prioritize the most up-to-date and relevant information, whether it comes from <|context|> or tools
|
||||
6. If <|context|> and tool outputs contain conflicting information, prefer the tool outputs as they likely represent more current data
|
||||
- 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
|
||||
- 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 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
|
||||
- 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.
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
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"""
|
||||
Launched on {datetime.now().isoformat()}.
|
||||
Launched on {DateTime()}.
|
||||
|
||||
You are a professional resume fact checker.
|
||||
|
||||
@ -93,6 +142,18 @@ 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>.
|
||||
"""
|
||||
|
||||
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():
|
||||
try:
|
||||
with open("/proc/meminfo", "r") as f:
|
||||
@ -200,6 +261,66 @@ 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)}"
|
||||
|
||||
# %%
|
||||
|
||||
@ -211,7 +332,14 @@ def is_valid_uuid(value):
|
||||
except (ValueError, TypeError):
|
||||
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]
|
||||
|
||||
|
||||
|
||||
@ -368,27 +496,25 @@ class WebServer:
|
||||
match reset_operation:
|
||||
case "system_prompt":
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
# match agent_type:
|
||||
# case "chat":
|
||||
# prompt = system_message
|
||||
# case "job_description":
|
||||
# prompt = system_generate_resume
|
||||
# case "resume":
|
||||
# prompt = system_generate_resume
|
||||
# case "fact_check":
|
||||
# prompt = system_message
|
||||
# case _:
|
||||
# prompt = system_message
|
||||
match agent_type:
|
||||
case "chat":
|
||||
prompt = system_message
|
||||
case "job_description":
|
||||
prompt = system_generate_resume
|
||||
case "resume":
|
||||
prompt = system_generate_resume
|
||||
case "fact_check":
|
||||
prompt = system_message
|
||||
|
||||
# agent.system_prompt = prompt
|
||||
# response["system_prompt"] = { "system_prompt": prompt }
|
||||
agent.system_prompt = prompt
|
||||
response["system_prompt"] = { "system_prompt": prompt }
|
||||
case "rags":
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
context.rags = rags.copy()
|
||||
response["rags"] = context.rags
|
||||
case "tools":
|
||||
logger.info(f"Resetting {reset_operation}")
|
||||
context.tools = Tools.enabled_tools(Tools.tools)
|
||||
context.tools = default_tools(tools)
|
||||
response["tools"] = context.tools
|
||||
case "history":
|
||||
reset_map = {
|
||||
@ -507,69 +633,32 @@ class WebServer:
|
||||
@self.app.post("/api/chat/{context_id}/{agent_type}")
|
||||
async def post_chat_endpoint(context_id: str, agent_type: str, request: Request):
|
||||
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:
|
||||
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)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
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:
|
||||
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)
|
||||
|
||||
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
|
||||
async def flush_generator():
|
||||
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)
|
||||
async for message in self.generate_response(context=context, agent=agent, content=data["content"]):
|
||||
# Convert to JSON and add newline
|
||||
result = json.dumps(result) + "\n"
|
||||
message.network_packets += 1
|
||||
message.network_bytes += len(result)
|
||||
yield result
|
||||
yield json.dumps(message.model_dump(mode='json')) + "\n"
|
||||
# Save the history as its generated
|
||||
self.save_context(context_id)
|
||||
# Explicitly flush after each yield
|
||||
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(
|
||||
@ -587,15 +676,9 @@ class WebServer:
|
||||
|
||||
@self.app.post("/api/context")
|
||||
async def create_context():
|
||||
try:
|
||||
context = self.create_context()
|
||||
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)
|
||||
context = self.create_context()
|
||||
logger.info(f"Generated new agent as {context.id}")
|
||||
return JSONResponse({ "id": context.id })
|
||||
|
||||
@self.app.get("/api/history/{context_id}/{agent_type}")
|
||||
async def get_history(context_id: str, agent_type: str, request: Request):
|
||||
@ -606,7 +689,7 @@ class WebServer:
|
||||
if not agent:
|
||||
logger.info(f"Agent {agent_type} not found. Returning empty history.")
|
||||
return JSONResponse({ "messages": [] })
|
||||
logger.info(f"History for {agent_type} contains {len(agent.conversation)} entries.")
|
||||
logger.info(f"History for {agent_type} contains {len(agent.conversation.messages)} entries.")
|
||||
return agent.conversation
|
||||
except Exception as e:
|
||||
logger.error(f"get_history error: {str(e)}")
|
||||
@ -688,7 +771,7 @@ class WebServer:
|
||||
|
||||
# Serialize the data to JSON and write to file
|
||||
with open(file_path, "w") as f:
|
||||
f.write(context.model_dump_json(by_alias=True))
|
||||
f.write(context.model_dump_json())
|
||||
|
||||
return context_id
|
||||
|
||||
@ -714,9 +797,9 @@ class WebServer:
|
||||
with open(file_path, "r") as f:
|
||||
content = f.read()
|
||||
logger.info(f"Loading context from {file_path}, content length: {len(content)}")
|
||||
import json
|
||||
try:
|
||||
# Try parsing as JSON first to ensure valid JSON
|
||||
import json
|
||||
json_data = json.loads(content)
|
||||
logger.info("JSON parsed successfully, attempting model validation")
|
||||
|
||||
@ -746,19 +829,19 @@ class WebServer:
|
||||
"""
|
||||
if not self.file_watcher:
|
||||
raise Exception("File watcher not initialized")
|
||||
if not context_id:
|
||||
context_id = str(uuid4())
|
||||
|
||||
logger.info(f"Creating new context with ID: {context_id}")
|
||||
context = Context(id=context_id, file_watcher=self.file_watcher)
|
||||
|
||||
if os.path.exists(defines.resume_doc):
|
||||
context.user_resume = open(defines.resume_doc, "r").read()
|
||||
context.get_or_create_agent(agent_type="chat")
|
||||
# system_prompt=system_message)
|
||||
context.get_or_create_agent(
|
||||
agent_type="chat",
|
||||
system_prompt=system_message)
|
||||
# context.add_agent(Resume(system_prompt = system_generate_resume))
|
||||
# context.add_agent(JobDescription(system_prompt = system_job_description))
|
||||
# context.add_agent(FactCheck(system_prompt = system_fact_check))
|
||||
context.tools = Tools.enabled_tools(Tools.tools)
|
||||
context.tools = default_tools(tools)
|
||||
context.rags = rags.copy()
|
||||
|
||||
logger.info(f"{context.id} created and added to contexts.")
|
||||
@ -766,6 +849,73 @@ class WebServer:
|
||||
self.save_context(context.id)
|
||||
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:
|
||||
"""
|
||||
Upsert a context based on the provided context_id.
|
||||
@ -784,34 +934,83 @@ class WebServer:
|
||||
|
||||
logger.info(f"Context {context_id} is not yet loaded.")
|
||||
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")
|
||||
|
||||
async def generate_response(self, context : Context, agent : Agent, prompt : str, options: Tunables | None) -> AsyncGenerator[Message, None]:
|
||||
results_found = False
|
||||
|
||||
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:
|
||||
raise Exception("File watcher not initialized")
|
||||
|
||||
agent_type = agent.get_agent_type()
|
||||
logger.info(f"generate_response: type - {agent_type}")
|
||||
message = Message(prompt=prompt, options=agent.tunables)
|
||||
if options:
|
||||
message.tunables = options
|
||||
|
||||
async for message in agent.prepare_message(message):
|
||||
# logger.info(f"{agent_type}.prepare_message: {value.status} - {value.response}")
|
||||
if message.status == "error":
|
||||
yield message
|
||||
return
|
||||
if message.status != "done":
|
||||
yield message
|
||||
async for message in agent.process_message(self.llm, self.model, message):
|
||||
if message.status == "error":
|
||||
yield message
|
||||
return
|
||||
if message.status != "done":
|
||||
yield message
|
||||
logger.info(f"{agent_type}.process_message: {message.status} {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}")
|
||||
message.status = "done"
|
||||
yield message
|
||||
return
|
||||
logger.info(f"generate_response: {agent_type}")
|
||||
if agent_type == "chat":
|
||||
message = Message(prompt=content)
|
||||
async for message in agent.prepare_message(message):
|
||||
# logger.info(f"{agent_type}.prepare_message: {value.status} - {value.response}")
|
||||
if message.status == "error":
|
||||
yield message
|
||||
return
|
||||
if message.status != "done":
|
||||
yield message
|
||||
async for message in agent.process_message(self.llm, self.model, message):
|
||||
# logger.info(f"{agent_type}.process_message: {value.status} - {value.response}")
|
||||
if message.status == "error":
|
||||
yield message
|
||||
return
|
||||
if message.status != "done":
|
||||
yield message
|
||||
# async for value in agent.generate_llm_response(message):
|
||||
# logger.info(f"{agent_type}.generate_llm_response: {value.status} - {value.response}")
|
||||
# if value.status != "done":
|
||||
# yield value
|
||||
# if value.status == "error":
|
||||
# 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:
|
||||
logger.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time")
|
||||
@ -969,7 +1168,7 @@ Use to the above information to respond to this prompt:
|
||||
stuffingMessage.response = "Job description stored to use in future queries."
|
||||
stuffingMessage.metadata["origin"] = "job_description"
|
||||
stuffingMessage.metadata["display"] = "hide"
|
||||
conversation.add(stuffingMessage)
|
||||
conversation.add_message(stuffingMessage)
|
||||
|
||||
message.add_action("generate_resume")
|
||||
|
||||
@ -1055,7 +1254,7 @@ Use the above <|resume|> and <|job_description|> to answer this query:
|
||||
stuffingMessage.metadata["display"] = "hide"
|
||||
stuffingMessage.actions = [ "fact_check" ]
|
||||
logger.info("TODO: Switch this to use actions to keep the UI from showingit")
|
||||
conversation.add(stuffingMessage)
|
||||
conversation.add_message(stuffingMessage)
|
||||
|
||||
# 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")
|
||||
@ -1071,7 +1270,7 @@ Use the above <|resume|> and <|job_description|> to answer this query:
|
||||
case _:
|
||||
raise Exception(f"Invalid chat agent_type: {agent_type}")
|
||||
|
||||
conversation.add(message)
|
||||
conversation.add_message(message)
|
||||
# llm_history.append({"role": "user", "content": message.preamble + content})
|
||||
# user_history.append({"role": "user", "content": content, "origin": message.metadata["origin"]})
|
||||
# message.metadata["full_query"] = llm_history[-1]["content"]
|
||||
@ -1273,7 +1472,7 @@ def main():
|
||||
module="umap.*"
|
||||
)
|
||||
|
||||
llm = ollama.Client(host=args.ollama_server) # type: ignore
|
||||
llm = ollama.Client(host=args.ollama_server)
|
||||
model = args.ollama_model
|
||||
|
||||
web_server = WebServer(llm, model)
|
||||
|
366
src/tools.py
Normal file
366
src/tools.py
Normal file
@ -0,0 +1,366 @@
|
||||
# %%
|
||||
# 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' ]
|
@ -1,27 +1,36 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel # type: ignore
|
||||
import importlib
|
||||
from typing import Optional, Type
|
||||
|
||||
from . import defines
|
||||
from . context import Context
|
||||
from . conversation import Conversation
|
||||
from . message import Message, Tunables
|
||||
from . rag import ChromaDBFileWatcher, start_file_watcher
|
||||
from . message import Message
|
||||
from . conversation import Conversation
|
||||
from . context import Context
|
||||
from . import agents
|
||||
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__ = [
|
||||
'Agent',
|
||||
'Tunables',
|
||||
'Context',
|
||||
'Conversation',
|
||||
'Message',
|
||||
'ChromaDBFileWatcher',
|
||||
'start_file_watcher',
|
||||
'start_file_watcher'
|
||||
'logger',
|
||||
]
|
||||
] + agents_all
|
||||
|
||||
__all__.extend(agents_all) # type: ignore
|
||||
# Resolve circular dependencies by rebuilding models
|
||||
# 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)
|
||||
|
||||
@ -55,4 +64,4 @@ def rebuild_models():
|
||||
logger.error(f"Error processing {class_name} in {module_name}: {e}")
|
||||
|
||||
# Call this after all modules are imported
|
||||
rebuild_models()
|
||||
rebuild_models()
|
@ -1,25 +1,19 @@
|
||||
from __future__ import annotations
|
||||
from typing import TypeAlias, Dict, Tuple, Optional
|
||||
import importlib
|
||||
import pathlib
|
||||
import inspect
|
||||
|
||||
from . types import registry
|
||||
from .. setup_logging import setup_logging
|
||||
from .. import defines
|
||||
import logging
|
||||
from typing import TypeAlias, Dict, Tuple
|
||||
from pydantic import BaseModel
|
||||
from . base import Agent
|
||||
|
||||
logger = setup_logging(defines.logging_level)
|
||||
|
||||
__all__ = [ "AnyAgent", "Agent", "registry", "class_registry" ]
|
||||
|
||||
# Type alias for Agent or any subclass
|
||||
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_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"):
|
||||
if path.name in ("__init__.py", "base.py") or path.name.startswith("_"):
|
||||
@ -27,7 +21,7 @@ for path in package_dir.glob("*.py"):
|
||||
|
||||
module_name = path.stem
|
||||
full_module_name = f"{package_name}.{module_name}"
|
||||
|
||||
|
||||
try:
|
||||
module = importlib.import_module(full_module_name)
|
||||
|
||||
@ -41,11 +35,9 @@ for path in package_dir.glob("*.py"):
|
||||
):
|
||||
class_registry[name] = (full_module_name, name)
|
||||
globals()[name] = obj
|
||||
logger.info(f"Adding agent: {name}")
|
||||
__all__.append(name) # type: ignore
|
||||
logging.info(f"Adding agent: {name} from {full_module_name}")
|
||||
__all__.append(name)
|
||||
except ImportError as e:
|
||||
logger.error(f"Error importing {full_module_name}: {e}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {full_module_name}: {e}")
|
||||
raise e
|
||||
logging.error(f"Failed to import module {full_module_name}: {e}")
|
||||
|
||||
__all__.append("AnyAgent")
|
@ -1,18 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, PrivateAttr, Field # type: ignore
|
||||
from typing import (
|
||||
Literal, get_args, List, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar, Any,
|
||||
TypeAlias, Dict, Tuple
|
||||
)
|
||||
import json
|
||||
import time
|
||||
import inspect
|
||||
from abc import ABC
|
||||
|
||||
from pydantic import BaseModel, model_validator, PrivateAttr, Field
|
||||
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar, ForwardRef, Any
|
||||
from abc import ABC, abstractmethod
|
||||
from typing_extensions import Annotated
|
||||
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()
|
||||
|
||||
@ -23,78 +14,35 @@ if TYPE_CHECKING:
|
||||
from .types import registry
|
||||
|
||||
from .. conversation import Conversation
|
||||
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)
|
||||
from .. message import Message
|
||||
|
||||
class Agent(BaseModel, ABC):
|
||||
"""
|
||||
Base class for all agent types.
|
||||
This class defines the common attributes and methods for all agent types.
|
||||
"""
|
||||
|
||||
# Agent management with pydantic
|
||||
agent_type: Literal["base"] = "base"
|
||||
_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
|
||||
system_prompt: str # Mandatory
|
||||
conversation: Conversation = Conversation()
|
||||
context_tokens: int = 0
|
||||
context: Optional[Context] = Field(default=None, exclude=True) # Avoid circular reference, require as param, and prevent serialization
|
||||
|
||||
# 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
|
||||
_content_seed: str = PrivateAttr(default="")
|
||||
|
||||
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
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
"""Auto-register subclasses"""
|
||||
super().__init_subclass__(**kwargs)
|
||||
# Register this class if it has an agent_type
|
||||
if hasattr(cls, 'agent_type') and cls.agent_type != Agent._agent_type:
|
||||
registry.register(cls.agent_type, cls)
|
||||
|
||||
def model_dump(self, *args, **kwargs) -> Any:
|
||||
def model_dump(self, *args, **kwargs):
|
||||
# Ensure context is always excluded, even with exclude_unset=True
|
||||
kwargs.setdefault("exclude", set())
|
||||
if isinstance(kwargs["exclude"], set):
|
||||
@ -114,322 +62,56 @@ class Agent(BaseModel, ABC):
|
||||
# Agent methods
|
||||
def get_agent_type(self):
|
||||
return self._agent_type
|
||||
|
||||
|
||||
async def prepare_message(self, message:Message) -> AsyncGenerator[Message, None]:
|
||||
"""
|
||||
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
|
||||
rag_context = ""
|
||||
if message.tunables.enable_rag and message.prompt:
|
||||
if not message.disable_rag:
|
||||
# Gather RAG results, yielding each result
|
||||
# as it becomes available
|
||||
for message in self.context.generate_rag_results(message):
|
||||
logger.info(f"RAG: {message.status} - {message.response}")
|
||||
if message.status == "error":
|
||||
for value in self.context.generate_rag_results(message):
|
||||
logger.info(f"RAG: {value.status} - {value.response}")
|
||||
if value.status != "done":
|
||||
yield value
|
||||
if value.status == "error":
|
||||
message.status = "error"
|
||||
message.response = value.response
|
||||
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 message.metadata["rag"]:
|
||||
for rag_collection in message.metadata["rag"]:
|
||||
for doc in rag_collection["documents"]:
|
||||
rag_context += f"{doc}\n"
|
||||
|
||||
if rag_context:
|
||||
message.preamble["context"] = rag_context
|
||||
message["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.status = "done"
|
||||
yield message
|
||||
return
|
||||
|
||||
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.")
|
||||
|
||||
async def generate_llm_response(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
if self.context.processing:
|
||||
logger.info("TODO: Implement delay queing; busy for same agent, otherwise return queue size and estimated wait time")
|
||||
message.status = "error"
|
||||
@ -439,37 +121,138 @@ class Agent(BaseModel, ABC):
|
||||
|
||||
self.context.processing = True
|
||||
|
||||
message.metadata["system_prompt"] = f"<|system|>\n{self.system_prompt.strip()}\n"
|
||||
message.context_prompt = ""
|
||||
for p in message.preamble.keys():
|
||||
message.context_prompt += f"\n<|{p}|>\n{message.preamble[p].strip()}\n"
|
||||
message.context_prompt += f"{message.prompt}"
|
||||
messages = []
|
||||
|
||||
# Estimate token length of new messages
|
||||
message.response = f"Optimizing context..."
|
||||
message.status = "thinking"
|
||||
yield message
|
||||
|
||||
message.metadata["context_size"] = self.set_optimal_context_size(llm, model, prompt=message.context_prompt)
|
||||
|
||||
message.response = f"Processing {'RAG augmented ' if message.metadata['rag'] else ''}query..."
|
||||
message.status = "thinking"
|
||||
yield message
|
||||
|
||||
async for message in self.generate_llm_response(llm, model, message):
|
||||
# logger.info(f"LLM: {message.status} - {f'...{message.response[-20:]}' if len(message.response) > 20 else message.response}")
|
||||
if message.status == "error":
|
||||
for value in self.llm.chat(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
#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
|
||||
self.context.processing = False
|
||||
if value.status == "error":
|
||||
return
|
||||
yield message
|
||||
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 = []
|
||||
|
||||
# Done processing, add message to conversation
|
||||
message.status = "done"
|
||||
self.conversation.add(message)
|
||||
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
|
||||
message.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 = "thinking"
|
||||
yield message
|
||||
|
||||
for value in self.generate_llm_response(message):
|
||||
logger.info(f"LLM: {value.status} - {value.response}")
|
||||
if value.status != "done":
|
||||
yield value
|
||||
if value.status == "error":
|
||||
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
|
||||
|
||||
# Register the base agent
|
||||
registry.register(Agent._agent_type, Agent)
|
||||
|
||||
|
||||
|
@ -1,59 +1,244 @@
|
||||
from __future__ import annotations
|
||||
from typing import Literal, AsyncGenerator, ClassVar, Optional, Any
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
|
||||
from . base import Agent, registry
|
||||
from pydantic import BaseModel, model_validator, PrivateAttr
|
||||
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar, Any
|
||||
from typing_extensions import Annotated
|
||||
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 .. setup_logging import setup_logging
|
||||
logger = setup_logging()
|
||||
from .. import defines
|
||||
|
||||
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):
|
||||
class Chat(Agent, ABC):
|
||||
"""
|
||||
Chat Agent
|
||||
Base class for all agent types.
|
||||
This class defines the common attributes and methods for all agent types.
|
||||
"""
|
||||
agent_type: Literal["chat"] = "chat" # type: ignore
|
||||
agent_type: Literal["chat"] = "chat"
|
||||
_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]:
|
||||
logger.info(f"{self.agent_type} - {inspect.stack()[0].function}")
|
||||
"""
|
||||
Prepare message with context information in message.preamble
|
||||
"""
|
||||
if not self.context:
|
||||
raise ValueError("Context is not set for this agent.")
|
||||
raise ValueError("Context is not set for this agent.")
|
||||
|
||||
async for message in super().prepare_message(message):
|
||||
if message.status != "done":
|
||||
yield message
|
||||
# Generate RAG content if enabled, based on the content
|
||||
rag_context = ""
|
||||
if not message.disable_rag:
|
||||
# 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:
|
||||
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"""\
|
||||
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.
|
||||
- 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"] = "Respond to:"
|
||||
message.preamble["question"] = "Use that information to 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
|
||||
registry.register(Chat._agent_type, Chat)
|
||||
|
@ -1,32 +1,18 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import model_validator # type: ignore
|
||||
from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
|
||||
from . base import Agent, registry
|
||||
from pydantic import BaseModel, Field, model_validator, PrivateAttr
|
||||
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar
|
||||
from typing_extensions import Annotated
|
||||
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 .. 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):
|
||||
agent_type: Literal["fact_check"] = "fact_check" # type: ignore
|
||||
agent_type: Literal["fact_check"] = "fact_check"
|
||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||
|
||||
system_prompt:str = system_fact_check
|
||||
facts: str
|
||||
facts: str = ""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_facts(self):
|
||||
@ -34,36 +20,5 @@ class FactCheck(Agent):
|
||||
raise ValueError("Facts cannot be empty")
|
||||
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
|
||||
registry.register(FactCheck._agent_type, FactCheck)
|
||||
|
@ -1,63 +1,18 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import model_validator # type: ignore
|
||||
from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
|
||||
from . base import Agent, registry
|
||||
from pydantic import BaseModel, Field, model_validator, PrivateAttr
|
||||
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar
|
||||
from typing_extensions import Annotated
|
||||
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 .. 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):
|
||||
agent_type: Literal["job_description"] = "job_description" # type: ignore
|
||||
agent_type: Literal["job_description"] = "job_description"
|
||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||
|
||||
system_prompt: str = system_generate_resume
|
||||
job_description: str
|
||||
job_description: str = ""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_job_description(self):
|
||||
@ -65,63 +20,5 @@ class JobDescription(Agent):
|
||||
raise ValueError("Job description cannot be empty")
|
||||
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
|
||||
registry.register(JobDescription._agent_type, JobDescription)
|
||||
|
@ -1,47 +1,18 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import model_validator # type: ignore
|
||||
from typing import Literal, ClassVar, Optional, Any, AsyncGenerator, List # NOTE: You must import Optional for late binding to work
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
|
||||
from . base import Agent, registry
|
||||
from pydantic import BaseModel, Field, model_validator, PrivateAttr
|
||||
from typing import Literal, TypeAlias, get_args, List, Generator, Iterator, AsyncGenerator, TYPE_CHECKING, Optional, ClassVar
|
||||
from typing_extensions import Annotated
|
||||
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 .. 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):
|
||||
agent_type: Literal["resume"] = "resume" # type: ignore
|
||||
agent_type: Literal["resume"] = "resume"
|
||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||
|
||||
system_prompt:str = system_fact_check
|
||||
resume: str
|
||||
resume: str = ""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_resume(self):
|
||||
@ -49,65 +20,13 @@ class Resume(Agent):
|
||||
raise ValueError("Resume content cannot be empty")
|
||||
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
|
||||
def get_resume(self) -> str:
|
||||
"""Get the resume content."""
|
||||
return self.resume
|
||||
|
||||
message.preamble["generated-resume"] = self.resume
|
||||
job_description_agent = self.context.get_agent("job_description")
|
||||
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
|
||||
def set_resume(self, resume: str) -> None:
|
||||
"""Set the resume content."""
|
||||
self.resume = resume
|
||||
|
||||
# Register the base agent
|
||||
registry.register(Resume._agent_type, Resume)
|
||||
|
@ -1,5 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import List, Dict, Optional, Type
|
||||
from typing import List, Dict, Any, Union, ForwardRef, TypeVar, Optional, TYPE_CHECKING, Type, ClassVar, Literal
|
||||
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
|
||||
class AgentRegistry:
|
||||
|
@ -1,4 +1,4 @@
|
||||
import tiktoken # type: ignore
|
||||
import tiktoken
|
||||
from . import defines
|
||||
from typing import List, Dict, Any, Union
|
||||
|
||||
|
@ -1,17 +1,18 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, Field, model_validator# type: ignore
|
||||
from pydantic import BaseModel, Field, model_validator, ValidationError
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Generator
|
||||
from typing import List, Dict, Any, Optional, Generator, TYPE_CHECKING
|
||||
from typing_extensions import Annotated, Union
|
||||
import numpy as np # type: ignore
|
||||
import numpy as np
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
import re
|
||||
|
||||
from . message import Message, Tunables
|
||||
from . rag import ChromaDBFileWatcher
|
||||
from .message import Message
|
||||
from .rag import ChromaDBFileWatcher
|
||||
from . import defines
|
||||
from . import tools as Tools
|
||||
from . agents import AnyAgent
|
||||
|
||||
from .agents import AnyAgent
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -29,11 +30,12 @@ class Context(BaseModel):
|
||||
user_resume: Optional[str] = None
|
||||
user_job_description: Optional[str] = None
|
||||
user_facts: Optional[str] = None
|
||||
tools: List[dict] = Tools.enabled_tools(Tools.tools)
|
||||
tools: List[dict] = []
|
||||
rags: List[dict] = []
|
||||
message_history_length: int = 5
|
||||
context_tokens: int = 0
|
||||
# Class managed fields
|
||||
agents: List[Annotated[Union[*Agent.__subclasses__()], Field(discriminator="agent_type")]] = Field( # type: ignore
|
||||
agents: List[Annotated[Union[*Agent.__subclasses__()], Field(discriminator="agent_type")]] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
@ -56,6 +58,10 @@ class Context(BaseModel):
|
||||
agent.set_context(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]:
|
||||
"""
|
||||
Generate RAG results for the given query.
|
||||
@ -103,7 +109,6 @@ class Context(BaseModel):
|
||||
"umap_embedding_2d": umap_2d,
|
||||
"umap_embedding_3d": umap_3d
|
||||
})
|
||||
message.response = f"Results from {rag['name']} RAG: {len(chroma_results['documents'])} results."
|
||||
yield message
|
||||
|
||||
if entries == 0:
|
||||
|
@ -1,41 +1,22 @@
|
||||
from pydantic import BaseModel, Field, PrivateAttr # type: ignore
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from .message import Message
|
||||
|
||||
class Conversation(BaseModel):
|
||||
Conversation_messages: List[Message] = Field(default=[], alias="messages")
|
||||
messages: List[Message] = []
|
||||
|
||||
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:
|
||||
def add_message(self, message: Message | List[Message]) -> None:
|
||||
"""Add a Message(s) to the conversation."""
|
||||
if isinstance(message, Message):
|
||||
self.Conversation_messages.append(message)
|
||||
self.messages.append(message)
|
||||
else:
|
||||
self.Conversation_messages.extend(message)
|
||||
self.messages.extend(message)
|
||||
|
||||
def get_summary(self) -> str:
|
||||
"""Return a summary of the conversation."""
|
||||
if not self.Conversation_messages:
|
||||
if not self.messages:
|
||||
return "Conversation is empty."
|
||||
summary = f"Conversation:\n"
|
||||
for i, message in enumerate(self.Conversation_messages, 1):
|
||||
for i, message in enumerate(self.messages, 1):
|
||||
summary += f"\nMessage {i}:\n{message.get_summary()}\n"
|
||||
return summary
|
@ -1,35 +1,30 @@
|
||||
from pydantic import BaseModel, Field # type: ignore
|
||||
from pydantic import BaseModel, model_validator
|
||||
from typing import Dict, List, Optional, Any
|
||||
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):
|
||||
# Required
|
||||
prompt: str # Query to be answered
|
||||
|
||||
# Tunables
|
||||
tunables: Tunables = Field(default_factory=Tunables)
|
||||
disable_rag: bool = False
|
||||
disable_tools: bool = False
|
||||
|
||||
# Generated while processing message
|
||||
status: str = "" # Status of the message
|
||||
preamble: dict[str,str] = {} # Preamble to be prepended to the prompt
|
||||
system_prompt: str = "" # System prompt provided to the LLM
|
||||
context_prompt: str = "" # Full content of the message (preamble + prompt)
|
||||
full_content: str = "" # Full content of the message (preamble + prompt)
|
||||
response: str = "" # LLM response to the preamble + query
|
||||
metadata: Dict[str, Any] = Field(default_factory=lambda: {
|
||||
"rag": [],
|
||||
metadata: dict[str, Any] = {
|
||||
"rag": List[dict[str, Any]],
|
||||
"tools": [],
|
||||
"eval_count": 0,
|
||||
"eval_duration": 0,
|
||||
"prompt_eval_count": 0,
|
||||
"prompt_eval_duration": 0,
|
||||
"context_size": 0,
|
||||
})
|
||||
network_packets: int = 0 # Total number of streaming packets
|
||||
network_bytes: int = 0 # Total bytes sent while streaming packets
|
||||
"ctx_size": 0,
|
||||
}
|
||||
actions: List[str] = [] # Other session modifying actions performed while processing the message
|
||||
timestamp: datetime = datetime.now(timezone.utc)
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
from pydantic import BaseModel # type: ignore
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field, model_validator, PrivateAttr
|
||||
import os
|
||||
import glob
|
||||
from pathlib import Path
|
||||
@ -13,18 +12,19 @@ import time
|
||||
import hashlib
|
||||
import asyncio
|
||||
import json
|
||||
import numpy as np # type: ignore
|
||||
import pickle
|
||||
import numpy as np
|
||||
import re
|
||||
|
||||
import chromadb
|
||||
import ollama
|
||||
from langchain.text_splitter import CharacterTextSplitter # type: ignore
|
||||
from sentence_transformers import SentenceTransformer # type: ignore
|
||||
from langchain.schema import Document # type: ignore
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
from watchdog.events import FileSystemEventHandler # type: ignore
|
||||
import umap # type: ignore
|
||||
from markitdown import MarkItDown # type: ignore
|
||||
from chromadb.api.models.Collection import Collection # type: ignore
|
||||
from langchain.text_splitter import CharacterTextSplitter
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from langchain.schema import Document
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
import umap
|
||||
from markitdown import MarkItDown
|
||||
|
||||
# Import your existing modules
|
||||
if __name__ == "__main__":
|
||||
@ -42,12 +42,6 @@ __all__ = [
|
||||
DEFAULT_CHUNK_SIZE=750
|
||||
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):
|
||||
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):
|
||||
@ -58,11 +52,12 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
self.chunk_size = chunk_size
|
||||
self.chunk_overlap = chunk_overlap
|
||||
self.loop = loop
|
||||
self._umap_collection : ChromaDBGetResponse | None = None
|
||||
self._umap_embedding_2d : np.ndarray = []
|
||||
self._umap_embedding_3d : np.ndarray = []
|
||||
self._umap_model_2d : umap.UMAP = None
|
||||
self._umap_model_3d : umap.UMAP = None
|
||||
self._umap_collection = None
|
||||
self._umap_embedding_2d = []
|
||||
self._umap_embedding_3d = []
|
||||
self._umap_model_2d = None
|
||||
self._umap_model_3d = None
|
||||
self._collection = None
|
||||
self.md = MarkItDown(enable_plugins=False) # Set to True to enable plugins
|
||||
|
||||
#self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
|
||||
@ -74,7 +69,7 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
self.is_new_collection = False
|
||||
|
||||
# Initialize ChromaDB collection
|
||||
self._collection : Collection = self._get_vector_collection(recreate=recreate)
|
||||
self._collection = self._get_vector_collection(recreate=recreate)
|
||||
self._update_umaps()
|
||||
|
||||
# Setup text splitter
|
||||
@ -95,15 +90,15 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
return self._collection
|
||||
|
||||
@property
|
||||
def umap_collection(self) -> ChromaDBGetResponse | None:
|
||||
def umap_collection(self):
|
||||
return self._umap_collection
|
||||
|
||||
@property
|
||||
def umap_embedding_2d(self) -> np.ndarray:
|
||||
def umap_embedding_2d(self):
|
||||
return self._umap_embedding_2d
|
||||
|
||||
@property
|
||||
def umap_embedding_3d(self) -> np.ndarray:
|
||||
def umap_embedding_3d(self):
|
||||
return self._umap_embedding_3d
|
||||
|
||||
@property
|
||||
@ -291,12 +286,12 @@ class ChromaDBFileWatcher(FileSystemEventHandler):
|
||||
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
|
||||
|
||||
def _get_vector_collection(self, recreate=False) -> Collection:
|
||||
def _get_vector_collection(self, recreate=False):
|
||||
"""Get or create a ChromaDB collection."""
|
||||
# Initialize ChromaDB client
|
||||
chroma_client = chromadb.PersistentClient( # type: ignore
|
||||
chroma_client = chromadb.PersistentClient(
|
||||
path=self.persist_directory,
|
||||
settings=chromadb.Settings(anonymized_telemetry=False) # type: ignore
|
||||
settings=chromadb.Settings(anonymized_telemetry=False)
|
||||
)
|
||||
|
||||
# Check if the collection exists
|
||||
@ -582,7 +577,7 @@ if __name__ == "__main__":
|
||||
import defines
|
||||
|
||||
# Initialize Ollama client
|
||||
llm = ollama.Client(host=defines.ollama_api_url) # type: ignore
|
||||
llm = ollama.Client(host=defines.ollama_api_url)
|
||||
|
||||
# Start the file watcher (with initialization)
|
||||
observer, file_watcher = start_file_watcher(
|
||||
|
@ -8,10 +8,9 @@ def setup_logging(level=defines.logging_level) -> logging.Logger:
|
||||
os.environ["TORCH_CPP_LOG_LEVEL"] = "ERROR"
|
||||
warnings.filterwarnings("ignore", message="Overriding a previously registered kernel")
|
||||
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="n_jobs value 1 overridden")
|
||||
warnings.filterwarnings("ignore", message=".*websocket.*is deprecated")
|
||||
|
||||
numeric_level = getattr(logging, level.upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
|
Loading…
x
Reference in New Issue
Block a user