Spaces:
Build error
Build error
Upload folder using huggingface_hub
Browse files- README.md +154 -7
- SearchOps_Assistant/.gitattributes +35 -0
- SearchOps_Assistant/.hf/requirements.txt +14 -0
- SearchOps_Assistant/README.md +158 -0
- SearchOps_Assistant/__pycache__/search.cpython-312.pyc +0 -0
- SearchOps_Assistant/__pycache__/searcher_tools.cpython-312.pyc +0 -0
- SearchOps_Assistant/app.py +52 -0
- SearchOps_Assistant/db.ipynb +122 -0
- SearchOps_Assistant/example.db +0 -0
- SearchOps_Assistant/query_log.db +0 -0
- SearchOps_Assistant/requirements.txt +12 -0
- SearchOps_Assistant/search.py +378 -0
- SearchOps_Assistant/searcher_tools.py +102 -0
- SearchOps_Assistant/setup.sh +0 -0
- __pycache__/search.cpython-312.pyc +0 -0
- __pycache__/searcher_tools.cpython-312.pyc +0 -0
- app.py +52 -0
- db.ipynb +69 -0
- example.db +0 -0
- query_log.db +0 -0
- requirements.txt +13 -0
- search.py +378 -0
- searcher_tools.py +102 -0
- setup.sh +0 -0
README.md
CHANGED
|
@@ -1,12 +1,159 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
colorFrom: gray
|
| 5 |
-
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.29.1
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SearchOps_Assistant
|
| 3 |
+
app_file: app.py
|
|
|
|
|
|
|
| 4 |
sdk: gradio
|
| 5 |
sdk_version: 5.29.1
|
|
|
|
|
|
|
| 6 |
---
|
| 7 |
|
| 8 |
+
# Search Personal Co-Worker
|
| 9 |
+
|
| 10 |
+
An intelligent search assistant built on LangGraph that can answer questions, perform web searches, record query history, and support personalized interactions.
|
| 11 |
+
|
| 12 |
+
## Project Overview
|
| 13 |
+
|
| 14 |
+
This project creates a smart search agent capable of finding information, answering questions, and maintaining search history records. Built using the LangGraph framework, it combines various tools to provide a rich search experience.
|
| 15 |
+
|
| 16 |
+
## Key Features
|
| 17 |
+
|
| 18 |
+
- **Intelligent Search**: Answer complex questions like "Where to find the best Chinese food in New York" or "What are the strongest AI technologies in 2025"
|
| 19 |
+
- **Search History Management**: Automatically saves all search records, allowing users to query their history using natural language (e.g., "give me all the search history")
|
| 20 |
+
- **Multi-Tool Integration**: Combines web search, Wikipedia, Python computation and other tools to provide comprehensive answers
|
| 21 |
+
- **Personalized Experience**: Recognizes users by username and saves individual search histories
|
| 22 |
+
- **Evaluation Mechanism**: Supports custom success criteria to evaluate search result quality
|
| 23 |
+
- **Automated Execution**: Uses Docker for automated program running and web scraping capabilities
|
| 24 |
+
- **Mobile Notifications**: Integrates with Pushover API to send real-time notifications to mobile devices
|
| 25 |
+
- **Web Scraping**: Employs Pydantic for structured data extraction from websites
|
| 26 |
+
|
| 27 |
+
## Project Structure
|
| 28 |
+
|
| 29 |
+
The project consists of three main files:
|
| 30 |
+
|
| 31 |
+
- **app.py**: Frontend interface responsible for user interaction
|
| 32 |
+
- **search.py**: LangGraph core backend that manages the search process and graph nodes
|
| 33 |
+
- **search_tools.py**: Defines all tools and agent functionalities
|
| 34 |
+
|
| 35 |
+
## LangGraph Architecture
|
| 36 |
+
|
| 37 |
+
The search flow consists of the following nodes:
|
| 38 |
+
|
| 39 |
+
- **StartNode**: Initializes the search process
|
| 40 |
+
- **WorkerNode**: Handles the main search logic
|
| 41 |
+
- **ToolsNode**: Manages and executes various external tools
|
| 42 |
+
- **EvaluatorNode**: Evaluates whether search results meet success criteria
|
| 43 |
+
- **SQLFormatterNode**: Processes database-related queries
|
| 44 |
+
- **EndNode**: Completes the search process and returns results
|
| 45 |
+
|
| 46 |
+
## Integrated Tools
|
| 47 |
+
|
| 48 |
+
This project integrates several powerful tools:
|
| 49 |
+
|
| 50 |
+
1. **Search Tool**: Online web search functionality
|
| 51 |
+
2. **History Query Tool**: Retrieves user's past search history
|
| 52 |
+
3. **Wikipedia Tool**: Directly queries Wikipedia content
|
| 53 |
+
4. **Python REPL**: Executes Python code for calculations or data processing
|
| 54 |
+
5. **Push Notification Tool**: Sends push notifications to mobile devices via Pushover API
|
| 55 |
+
6. **File Tools**: File read/write operations
|
| 56 |
+
7. **Docker Execution**: Securely runs code in isolated containers
|
| 57 |
+
8. **Web Scraping**: Uses Pydantic models for structured web data extraction
|
| 58 |
+
|
| 59 |
+
## Installation & Setup
|
| 60 |
+
|
| 61 |
+
### Requirements
|
| 62 |
+
|
| 63 |
+
- Python 3.8+
|
| 64 |
+
- Docker (for automated program execution)
|
| 65 |
+
- Required dependencies (see requirements.txt)
|
| 66 |
+
- Pushover account and API key (for mobile notifications)
|
| 67 |
+
|
| 68 |
+
### Installation Steps
|
| 69 |
+
|
| 70 |
+
1. Clone the repository:
|
| 71 |
+
```bash
|
| 72 |
+
git clone [repository-url]
|
| 73 |
+
cd [project-folder]
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
2. Install dependencies:
|
| 77 |
+
```bash
|
| 78 |
+
pip install -r requirements.txt
|
| 79 |
+
playwright install chromium
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
3. Set up environment variables:
|
| 83 |
+
```bash
|
| 84 |
+
# Required for Pushover notifications
|
| 85 |
+
export PUSHOVER_USER_KEY=your_user_key
|
| 86 |
+
export PUSHOVER_API_TOKEN=your_api_token
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
4. Install Docker (if not already installed):
|
| 90 |
+
```bash
|
| 91 |
+
# For Ubuntu
|
| 92 |
+
sudo apt-get update
|
| 93 |
+
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
| 94 |
+
|
| 95 |
+
# For macOS and Windows, download and install Docker Desktop
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### Running the Application
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python app.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
The application will start locally. Access the URL displayed in the terminal (typically http://localhost:7860) to use the search assistant.
|
| 105 |
+
|
| 106 |
+
## Usage Guide
|
| 107 |
+
|
| 108 |
+
1. Enter your username (used to record search history)
|
| 109 |
+
2. Type your question in the search box
|
| 110 |
+
3. Specify success criteria (optional, used to evaluate result quality)
|
| 111 |
+
4. Click the "Search" button
|
| 112 |
+
5. View your search results
|
| 113 |
+
6. Query your history by entering commands like "give me all the search history"
|
| 114 |
+
7. Receive push notifications on your mobile device for important alerts
|
| 115 |
+
|
| 116 |
+
## Deploying to Hugging Face
|
| 117 |
+
|
| 118 |
+
To deploy this project to Hugging Face Spaces:
|
| 119 |
+
|
| 120 |
+
1. Create a setup.sh file with the following content:
|
| 121 |
+
```bash
|
| 122 |
+
#!/bin/bash
|
| 123 |
+
pip install -r requirements.txt
|
| 124 |
+
playwright install chromium
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
2. Create a .hf directory with a requirements.txt file containing:
|
| 128 |
+
```
|
| 129 |
+
execute_setup_sh==True
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
3. Set up repository secrets in Hugging Face for sensitive API keys:
|
| 133 |
+
- PUSHOVER_USER_KEY
|
| 134 |
+
- PUSHOVER_API_TOKEN
|
| 135 |
+
|
| 136 |
+
4. Ensure your code runs in headless mode for Playwright:
|
| 137 |
+
```python
|
| 138 |
+
browser = playwright.chromium.launch(headless=True)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
5. Note: Docker execution features may have limited functionality in the Hugging Face environment
|
| 142 |
+
|
| 143 |
+
## Troubleshooting
|
| 144 |
+
|
| 145 |
+
If you encounter issues during deployment, check:
|
| 146 |
+
- Browser binary installation (Playwright requires additional setup)
|
| 147 |
+
- Environment differences between local and deployment
|
| 148 |
+
- Path configurations and dependencies
|
| 149 |
+
- API key configuration for Pushover notifications
|
| 150 |
+
- Docker availability and permissions in your deployment environment
|
| 151 |
+
|
| 152 |
+
## Contributing
|
| 153 |
+
|
| 154 |
+
Contributions to improve the project are welcome. Please feel free to submit a pull request or open an issue.
|
| 155 |
+
|
| 156 |
+
## License
|
| 157 |
+
|
| 158 |
+
Benny
|
| 159 |
+
|
SearchOps_Assistant/.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
SearchOps_Assistant/.hf/requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.29.1
|
| 2 |
+
langchain==0.3.25
|
| 3 |
+
langchain_community==0.3.24
|
| 4 |
+
langchain_core==0.3.60
|
| 5 |
+
langchain_experimental==0.3.4
|
| 6 |
+
langchain_openai==0.3.17
|
| 7 |
+
langgraph==0.4.5
|
| 8 |
+
playwright==1.52.0
|
| 9 |
+
pydantic==2.11.4
|
| 10 |
+
python-dotenv==1.1.0
|
| 11 |
+
Requests==2.32.3
|
| 12 |
+
typing_extensions==4.13.2
|
| 13 |
+
|
| 14 |
+
execute_setup_sh==True
|
SearchOps_Assistant/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Search Personal Co-Worker
|
| 2 |
+
|
| 3 |
+
An intelligent search assistant built on LangGraph that can answer questions, perform web searches, record query history, and support personalized interactions.
|
| 4 |
+
|
| 5 |
+
## Project Overview
|
| 6 |
+
|
| 7 |
+
This project creates a smart search agent capable of finding information, answering questions, and maintaining search history records. Built using the LangGraph framework, it combines various tools to provide a rich search experience.
|
| 8 |
+
|
| 9 |
+
## Key Features
|
| 10 |
+
|
| 11 |
+
- **Intelligent Search**: Answer complex questions like "Where to find the best Chinese food in New York" or "What are the strongest AI technologies in 2025"
|
| 12 |
+
- **Search History Management**: Automatically saves all search records, allowing users to query their history using natural language (e.g., "give me all the search history")
|
| 13 |
+
- **Multi-Tool Integration**: Combines web search, Wikipedia, Python computation and other tools to provide comprehensive answers
|
| 14 |
+
- **Personalized Experience**: Recognizes users by username and saves individual search histories
|
| 15 |
+
- **Evaluation Mechanism**: Supports custom success criteria to evaluate search result quality
|
| 16 |
+
- **Automated Execution**: Uses Docker for automated program running and web scraping capabilities
|
| 17 |
+
- **Mobile Notifications**: Integrates with Pushover API to send real-time notifications to mobile devices
|
| 18 |
+
- **Web Scraping**: Employs Pydantic for structured data extraction from websites
|
| 19 |
+
|
| 20 |
+
## Project Structure
|
| 21 |
+
|
| 22 |
+
The project consists of three main files:
|
| 23 |
+
|
| 24 |
+
- **app.py**: Frontend interface responsible for user interaction
|
| 25 |
+
- **search.py**: LangGraph core backend that manages the search process and graph nodes
|
| 26 |
+
- **search_tools.py**: Defines all tools and agent functionalities
|
| 27 |
+
|
| 28 |
+
## LangGraph Architecture
|
| 29 |
+
|
| 30 |
+
The search flow consists of the following nodes:
|
| 31 |
+
|
| 32 |
+
- **StartNode**: Initializes the search process
|
| 33 |
+
- **WorkerNode**: Handles the main search logic
|
| 34 |
+
- **ToolsNode**: Manages and executes various external tools
|
| 35 |
+
- **EvaluatorNode**: Evaluates whether search results meet success criteria
|
| 36 |
+
- **SQLFormatterNode**: Processes database-related queries
|
| 37 |
+
- **EndNode**: Completes the search process and returns results
|
| 38 |
+
|
| 39 |
+
## Integrated Tools
|
| 40 |
+
|
| 41 |
+
This project integrates several powerful tools:
|
| 42 |
+
|
| 43 |
+
1. **Search Tool**: Online web search functionality
|
| 44 |
+
2. **History Query Tool**: Retrieves user's past search history
|
| 45 |
+
3. **Wikipedia Tool**: Directly queries Wikipedia content
|
| 46 |
+
4. **Python REPL**: Executes Python code for calculations or data processing
|
| 47 |
+
5. **Push Notification Tool**: Sends push notifications to mobile devices via Pushover API
|
| 48 |
+
6. **File Tools**: File read/write operations
|
| 49 |
+
7. **Docker Execution**: Securely runs code in isolated containers
|
| 50 |
+
8. **Web Scraping**: Uses Pydantic models for structured web data extraction
|
| 51 |
+
|
| 52 |
+
## Installation & Setup
|
| 53 |
+
|
| 54 |
+
### Requirements
|
| 55 |
+
|
| 56 |
+
- Python 3.8+
|
| 57 |
+
- Docker (for automated program execution)
|
| 58 |
+
- Required dependencies (see requirements.txt)
|
| 59 |
+
- Pushover account and API key (for mobile notifications)
|
| 60 |
+
|
| 61 |
+
### Installation Steps
|
| 62 |
+
|
| 63 |
+
1. Clone the repository:
|
| 64 |
+
```bash
|
| 65 |
+
git clone [repository-url]
|
| 66 |
+
cd [project-folder]
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
2. Install dependencies:
|
| 70 |
+
```bash
|
| 71 |
+
pip install -r requirements.txt
|
| 72 |
+
playwright install chromium
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
3. Set up environment variables:
|
| 76 |
+
```bash
|
| 77 |
+
# Required for Pushover notifications
|
| 78 |
+
export PUSHOVER_USER_KEY=your_user_key
|
| 79 |
+
export PUSHOVER_API_TOKEN=your_api_token
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
4. Install Docker (if not already installed):
|
| 83 |
+
```bash
|
| 84 |
+
# For Ubuntu
|
| 85 |
+
sudo apt-get update
|
| 86 |
+
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
| 87 |
+
|
| 88 |
+
# For macOS and Windows, download and install Docker Desktop
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Running the Application
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
python app.py
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
The application will start locally. Access the URL displayed in the terminal (typically http://localhost:7860) to use the search assistant.
|
| 98 |
+
|
| 99 |
+
## Usage Guide
|
| 100 |
+
|
| 101 |
+
1. Enter your username (used to record search history)
|
| 102 |
+
2. Type your question in the search box
|
| 103 |
+
3. Specify success criteria (optional, used to evaluate result quality)
|
| 104 |
+
4. Click the "Search" button
|
| 105 |
+
5. View your search results
|
| 106 |
+
6. Query your history by entering commands like "give me all the search history"
|
| 107 |
+
7. Receive push notifications on your mobile device for important alerts
|
| 108 |
+
|
| 109 |
+
## Deploying to Hugging Face
|
| 110 |
+
|
| 111 |
+
To deploy this project to Hugging Face Spaces:
|
| 112 |
+
|
| 113 |
+
1. Create a setup.sh file with the following content:
|
| 114 |
+
```bash
|
| 115 |
+
#!/bin/bash
|
| 116 |
+
pip install -r requirements.txt
|
| 117 |
+
playwright install chromium
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
2. Create a .hf directory with a requirements.txt file containing:
|
| 121 |
+
```
|
| 122 |
+
execute_setup_sh==True
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
3. Set up repository secrets in Hugging Face for sensitive API keys:
|
| 126 |
+
- PUSHOVER_USER_KEY
|
| 127 |
+
- PUSHOVER_API_TOKEN
|
| 128 |
+
|
| 129 |
+
4. Ensure your code runs in headless mode for Playwright:
|
| 130 |
+
```python
|
| 131 |
+
browser = playwright.chromium.launch(headless=True)
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
5. Note: Docker execution features may have limited functionality in the Hugging Face environment
|
| 135 |
+
|
| 136 |
+
## Troubleshooting
|
| 137 |
+
|
| 138 |
+
If you encounter issues during deployment, check:
|
| 139 |
+
- Browser binary installation (Playwright requires additional setup)
|
| 140 |
+
- Environment differences between local and deployment
|
| 141 |
+
- Path configurations and dependencies
|
| 142 |
+
- API key configuration for Pushover notifications
|
| 143 |
+
- Docker availability and permissions in your deployment environment
|
| 144 |
+
|
| 145 |
+
## Contributing
|
| 146 |
+
|
| 147 |
+
Contributions to improve the project are welcome. Please feel free to submit a pull request or open an issue.
|
| 148 |
+
|
| 149 |
+
## License
|
| 150 |
+
|
| 151 |
+
[Add your license information here]
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
title: SearchOps_Assistant
|
| 155 |
+
app_file: app.py
|
| 156 |
+
sdk: gradio
|
| 157 |
+
sdk_version: 5.29.1
|
| 158 |
+
---
|
SearchOps_Assistant/__pycache__/search.cpython-312.pyc
ADDED
|
Binary file (18.4 kB). View file
|
|
|
SearchOps_Assistant/__pycache__/searcher_tools.cpython-312.pyc
ADDED
|
Binary file (4.81 kB). View file
|
|
|
SearchOps_Assistant/app.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from search import Search
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
async def setup():
|
| 6 |
+
search = Search()
|
| 7 |
+
await search.setup()
|
| 8 |
+
return search
|
| 9 |
+
|
| 10 |
+
async def process_message(search, username, message, success_criteria, history):
|
| 11 |
+
results = await search.run_superstep(message, username, success_criteria, history)
|
| 12 |
+
return results, search
|
| 13 |
+
|
| 14 |
+
async def reset():
|
| 15 |
+
new_search = Search()
|
| 16 |
+
await new_search.setup()
|
| 17 |
+
return "", "","", None, new_search
|
| 18 |
+
|
| 19 |
+
def free_resources(search):
|
| 20 |
+
print("Cleaning up")
|
| 21 |
+
try:
|
| 22 |
+
if search:
|
| 23 |
+
search.free_resources()
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f"Exception during cleanup: {e}")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
with gr.Blocks(title="Search", theme=gr.themes.Default(primary_hue="emerald")) as ui:
|
| 29 |
+
gr.Markdown("## Search Personal Co-Worker")
|
| 30 |
+
search = gr.State(delete_callback=free_resources)
|
| 31 |
+
|
| 32 |
+
with gr.Row():
|
| 33 |
+
chatbot = gr.Chatbot(label="Search", height=300, type="messages")
|
| 34 |
+
with gr.Group():
|
| 35 |
+
with gr.Row():
|
| 36 |
+
username = gr.Textbox(show_label=False, placeholder="Enter your username")
|
| 37 |
+
with gr.Row():
|
| 38 |
+
message = gr.Textbox(show_label=False, placeholder="Your request to the Search")
|
| 39 |
+
with gr.Row():
|
| 40 |
+
success_criteria = gr.Textbox(show_label=False, placeholder="What are your success critiera?")
|
| 41 |
+
with gr.Row():
|
| 42 |
+
reset_button = gr.Button("Reset", variant="stop")
|
| 43 |
+
go_button = gr.Button("Go!", variant="primary")
|
| 44 |
+
|
| 45 |
+
ui.load(setup, [], [search])
|
| 46 |
+
message.submit(process_message, [search,username, message, success_criteria, chatbot], [chatbot, search])
|
| 47 |
+
success_criteria.submit(process_message, [search, username, message, success_criteria, chatbot], [chatbot, search])
|
| 48 |
+
go_button.click(process_message, [search, username, message, success_criteria, chatbot], [chatbot, search])
|
| 49 |
+
reset_button.click(reset, [], [username, message, success_criteria, chatbot, search])
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
ui.launch(inbrowser=True)
|
SearchOps_Assistant/db.ipynb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"outputs": [],
|
| 8 |
+
"source": [
|
| 9 |
+
"from langchain.agents.agent_toolkits import SQLDatabaseToolkit\n",
|
| 10 |
+
"from langchain.sql_database import SQLDatabase\n",
|
| 11 |
+
"from langchain.agents import Tool"
|
| 12 |
+
]
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"cell_type": "code",
|
| 16 |
+
"execution_count": 4,
|
| 17 |
+
"metadata": {},
|
| 18 |
+
"outputs": [
|
| 19 |
+
{
|
| 20 |
+
"ename": "AttributeError",
|
| 21 |
+
"evalue": "'SQLDatabase' object has no attribute 'close'",
|
| 22 |
+
"output_type": "error",
|
| 23 |
+
"traceback": [
|
| 24 |
+
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
| 25 |
+
"\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)",
|
| 26 |
+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# 初始化数据库连接\u001b[39;00m\n\u001b[32m 2\u001b[39m db = SQLDatabase.from_uri(\u001b[33m\"\u001b[39m\u001b[33msqlite:///example.db\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m \u001b[43mdb\u001b[49m\u001b[43m.\u001b[49m\u001b[43mclose\u001b[49m()\n",
|
| 27 |
+
"\u001b[31mAttributeError\u001b[39m: 'SQLDatabase' object has no attribute 'close'"
|
| 28 |
+
]
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"source": [
|
| 32 |
+
"\n",
|
| 33 |
+
"# 初始化数据库连接\n",
|
| 34 |
+
"db = SQLDatabase.from_uri(\"sqlite:///example.db\")\n",
|
| 35 |
+
"\n",
|
| 36 |
+
"\n",
|
| 37 |
+
"db.close()"
|
| 38 |
+
]
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"cell_type": "code",
|
| 42 |
+
"execution_count": 7,
|
| 43 |
+
"metadata": {},
|
| 44 |
+
"outputs": [],
|
| 45 |
+
"source": [
|
| 46 |
+
"import sqlite3\n",
|
| 47 |
+
"\n",
|
| 48 |
+
"conn = sqlite3.connect(\"query_log.db\")\n",
|
| 49 |
+
"cursor = conn.cursor()\n",
|
| 50 |
+
"\n",
|
| 51 |
+
"cursor.execute('''\n",
|
| 52 |
+
"CREATE TABLE IF NOT EXISTS search_history (\n",
|
| 53 |
+
" id TEXT PRIMARY KEY,\n",
|
| 54 |
+
" username TEXT,\n",
|
| 55 |
+
" feedback TEXT,\n",
|
| 56 |
+
" reply TEXT,\n",
|
| 57 |
+
" timestamp DATETIME DEFAULT CURRENT_TIMESTAMP\n",
|
| 58 |
+
")\n",
|
| 59 |
+
"''')\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"conn.commit()\n",
|
| 62 |
+
"conn.close()\n"
|
| 63 |
+
]
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"cell_type": "code",
|
| 67 |
+
"execution_count": 9,
|
| 68 |
+
"metadata": {},
|
| 69 |
+
"outputs": [
|
| 70 |
+
{
|
| 71 |
+
"name": "stdout",
|
| 72 |
+
"output_type": "stream",
|
| 73 |
+
"text": [
|
| 74 |
+
"('benny', 'b43cb216-d36d-4ba3-87f7-75fdf5790487', \"The assistant has indicated an inability to retrieve the search history, which means it did not fulfill the user's request to provide a table of the search history. Additionally, while the assistant asked the user if they have specific search queries, this does not meet the requirement for providing the requested information. Therefore, the response does not meet the success criteria.\", \"It seems that I'm unable to retrieve the search history at this moment. If you have any specific search queries or topics you're interested in, please let me know!\", '2025-05-16 08:53:20')\n"
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
],
|
| 78 |
+
"source": [
|
| 79 |
+
"conn = sqlite3.connect(\"query_log.db\")\n",
|
| 80 |
+
"cursor = conn.cursor()\n",
|
| 81 |
+
"\n",
|
| 82 |
+
"cursor.execute('''\n",
|
| 83 |
+
"select * from search_history\n",
|
| 84 |
+
"''')\n",
|
| 85 |
+
"results = cursor.fetchall()\n",
|
| 86 |
+
"\n",
|
| 87 |
+
"for row in results:\n",
|
| 88 |
+
" print(row) \n",
|
| 89 |
+
"conn.commit()\n",
|
| 90 |
+
"conn.close()"
|
| 91 |
+
]
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"cell_type": "code",
|
| 95 |
+
"execution_count": null,
|
| 96 |
+
"metadata": {},
|
| 97 |
+
"outputs": [],
|
| 98 |
+
"source": []
|
| 99 |
+
}
|
| 100 |
+
],
|
| 101 |
+
"metadata": {
|
| 102 |
+
"kernelspec": {
|
| 103 |
+
"display_name": ".venv",
|
| 104 |
+
"language": "python",
|
| 105 |
+
"name": "python3"
|
| 106 |
+
},
|
| 107 |
+
"language_info": {
|
| 108 |
+
"codemirror_mode": {
|
| 109 |
+
"name": "ipython",
|
| 110 |
+
"version": 3
|
| 111 |
+
},
|
| 112 |
+
"file_extension": ".py",
|
| 113 |
+
"mimetype": "text/x-python",
|
| 114 |
+
"name": "python",
|
| 115 |
+
"nbconvert_exporter": "python",
|
| 116 |
+
"pygments_lexer": "ipython3",
|
| 117 |
+
"version": "3.12.7"
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
"nbformat": 4,
|
| 121 |
+
"nbformat_minor": 2
|
| 122 |
+
}
|
SearchOps_Assistant/example.db
ADDED
|
File without changes
|
SearchOps_Assistant/query_log.db
ADDED
|
Binary file (49.2 kB). View file
|
|
|
SearchOps_Assistant/requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.29.1
|
| 2 |
+
langchain==0.3.25
|
| 3 |
+
langchain_community==0.3.24
|
| 4 |
+
langchain_core==0.3.60
|
| 5 |
+
langchain_experimental==0.3.4
|
| 6 |
+
langchain_openai==0.3.17
|
| 7 |
+
langgraph==0.4.5
|
| 8 |
+
playwright==1.52.0
|
| 9 |
+
pydantic==2.11.4
|
| 10 |
+
python-dotenv==1.1.0
|
| 11 |
+
Requests==2.32.3
|
| 12 |
+
typing_extensions==4.13.2
|
SearchOps_Assistant/search.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated
|
| 2 |
+
from typing_extensions import TypedDict
|
| 3 |
+
from langgraph.graph import StateGraph, START, END
|
| 4 |
+
from langgraph.graph.message import add_messages
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from langgraph.prebuilt import ToolNode
|
| 7 |
+
from langchain_openai import ChatOpenAI
|
| 8 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 9 |
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
| 10 |
+
from typing import List, Any, Optional, Dict
|
| 11 |
+
from pydantic import BaseModel, Field
|
| 12 |
+
from searcher_tools import playwright_tools, other_tools
|
| 13 |
+
import uuid
|
| 14 |
+
import asyncio
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
import sqlite3
|
| 17 |
+
import json
|
| 18 |
+
load_dotenv(override=True)
|
| 19 |
+
|
| 20 |
+
class State(TypedDict):
|
| 21 |
+
messages: Annotated[List[Any], add_messages]
|
| 22 |
+
username: str
|
| 23 |
+
success_criteria: str
|
| 24 |
+
feedback_on_work: Optional[str]
|
| 25 |
+
success_criteria_met: bool
|
| 26 |
+
user_input_needed: bool
|
| 27 |
+
tool_name: Optional[str]
|
| 28 |
+
|
| 29 |
+
class EvaluatorOutput(BaseModel):
|
| 30 |
+
feedback: str = Field(description="Feedback on the assistant's response")
|
| 31 |
+
success_criteria_met: bool = Field(description="Whether the success criteria have been met")
|
| 32 |
+
user_input_needed: bool = Field(description="True if more input is needed from the user, or clarifications, or the assistant is stuck")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Search:
|
| 36 |
+
def __init__(self):
|
| 37 |
+
self.worker_llm_with_tools = None
|
| 38 |
+
self.evaluator_llm_with_output = None
|
| 39 |
+
self.tools = None
|
| 40 |
+
self.llm_with_tools = None
|
| 41 |
+
self.graph = None
|
| 42 |
+
self.search_id = str(uuid.uuid4())
|
| 43 |
+
self.memory = MemorySaver()
|
| 44 |
+
self.browser = None
|
| 45 |
+
self.playwright = None
|
| 46 |
+
self.formatting_llm = None
|
| 47 |
+
async def setup(self):
|
| 48 |
+
self.tools, self.browser, self.playwright = await playwright_tools()
|
| 49 |
+
self.tools += await other_tools()
|
| 50 |
+
worker_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 51 |
+
self.worker_llm_with_tools = worker_llm.bind_tools(self.tools)
|
| 52 |
+
evaluator_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 53 |
+
self.evaluator_llm_with_output = evaluator_llm.with_structured_output(EvaluatorOutput)
|
| 54 |
+
self.formatting_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 55 |
+
await self.build_graph()
|
| 56 |
+
|
| 57 |
+
def worker(self, state: State) -> Dict[str, Any]:
|
| 58 |
+
|
| 59 |
+
if not state.get("username") or state["username"].strip() == "":
|
| 60 |
+
# Username not provided, return response asking for username
|
| 61 |
+
response = AIMessage(content="Please provide a username before search or accessing search history.")
|
| 62 |
+
new_state = dict(state)
|
| 63 |
+
new_state["messages"] = [response]
|
| 64 |
+
new_state["tool_name"] = None
|
| 65 |
+
new_state["user_input_needed"] = True # Signal that we need user input
|
| 66 |
+
return new_state
|
| 67 |
+
|
| 68 |
+
system_message = f"""You are a helpful assistant that can use tools to complete tasks.
|
| 69 |
+
You keep working on a task until either you have a question or clarification for the user, or the success criteria is met.
|
| 70 |
+
You have many tools to help you, including tools to browse the internet, navigating and retrieving web pages.
|
| 71 |
+
You have a tool to run python code, but note that you would need to include a print() statement if you wanted to receive output.
|
| 72 |
+
The username is {state['username']}The current date and time is {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 73 |
+
|
| 74 |
+
This is the success criteria:
|
| 75 |
+
{state['success_criteria']}
|
| 76 |
+
You should reply either with a question for the user about this assignment, or with your final response.
|
| 77 |
+
If you have a question for the user, you need to reply by clearly stating your question. An example might be:
|
| 78 |
+
|
| 79 |
+
Question: please clarify whether you want a summary or a detailed answer
|
| 80 |
+
|
| 81 |
+
If you've finished, reply with the final answer, and don't ask a question; simply reply with the answer.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
if state.get("feedback_on_work"):
|
| 85 |
+
system_message += f"""
|
| 86 |
+
Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.
|
| 87 |
+
Here is the feedback on why this was rejected:
|
| 88 |
+
{state['feedback_on_work']}
|
| 89 |
+
With this feedback, please continue the assignment, ensuring that you meet the success criteria or have a question for the user."""
|
| 90 |
+
|
| 91 |
+
# Add in the system message
|
| 92 |
+
|
| 93 |
+
found_system_message = False
|
| 94 |
+
messages = state["messages"]
|
| 95 |
+
for message in messages:
|
| 96 |
+
if isinstance(message, SystemMessage):
|
| 97 |
+
message.content = system_message
|
| 98 |
+
found_system_message = True
|
| 99 |
+
|
| 100 |
+
if not found_system_message:
|
| 101 |
+
messages = [SystemMessage(content=system_message)] + messages
|
| 102 |
+
|
| 103 |
+
# Invoke the LLM with tools
|
| 104 |
+
response = self.worker_llm_with_tools.invoke(messages)
|
| 105 |
+
tool_name = None
|
| 106 |
+
if hasattr(response, "tool_calls") and response.tool_calls:
|
| 107 |
+
tool_name = response.tool_calls[0]["name"]
|
| 108 |
+
print(f"Next tool to run: {tool_name}")
|
| 109 |
+
# Return updated state
|
| 110 |
+
new_state = dict(state)
|
| 111 |
+
new_state["messages"] = [response]
|
| 112 |
+
new_state["tool_name"] = tool_name
|
| 113 |
+
|
| 114 |
+
return new_state
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def worker_router(self, state: State) -> str:
|
| 118 |
+
last_message = state["messages"][-1]
|
| 119 |
+
|
| 120 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 121 |
+
return "tools"
|
| 122 |
+
else:
|
| 123 |
+
return "evaluator"
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def format_conversation(self, messages: List[Any]) -> str:
|
| 127 |
+
conversation = "Conversation history:\n\n"
|
| 128 |
+
for message in messages:
|
| 129 |
+
if isinstance(message, HumanMessage):
|
| 130 |
+
conversation += f"User: {message.content}\n"
|
| 131 |
+
elif isinstance(message, AIMessage):
|
| 132 |
+
text = message.content or "[Tools use]"
|
| 133 |
+
conversation += f"Assistant: {text}\n"
|
| 134 |
+
return conversation
|
| 135 |
+
|
| 136 |
+
def evaluator(self, state: State) -> State:
|
| 137 |
+
last_response = state["messages"][-1].content
|
| 138 |
+
|
| 139 |
+
system_message = f"""You are an evaluator that determines if a task has been completed successfully by an Assistant.
|
| 140 |
+
Assess the Assistant's last response based on the given criteria. Respond with your feedback, and with your decision on whether the success criteria has been met,
|
| 141 |
+
and whether more input is needed from the user."""
|
| 142 |
+
|
| 143 |
+
user_message = f"""You are evaluating a conversation between the User and Assistant. You decide what action to take based on the last response from the Assistant.
|
| 144 |
+
|
| 145 |
+
The entire conversation with the assistant, with the user's original request and all replies, is:
|
| 146 |
+
{self.format_conversation(state['messages'])}
|
| 147 |
+
|
| 148 |
+
The success criteria for this assignment is:
|
| 149 |
+
{state['success_criteria']}
|
| 150 |
+
|
| 151 |
+
And the final response from the Assistant that you are evaluating is:
|
| 152 |
+
{last_response}
|
| 153 |
+
|
| 154 |
+
Respond with your feedback, and decide if the success criteria is met by this response.
|
| 155 |
+
Also, decide if more user input is required, either because the assistant has a question, needs clarification, or seems to be stuck and unable to answer without help.
|
| 156 |
+
|
| 157 |
+
The Assistant has access to tools, including a tool to retrieve search history. If the Assistant correctly uses this tool, regardless of the result (which depends on permissions),
|
| 158 |
+
then consider the success criteria met if the Assistant did its job correctly and communicated results or limitations properly.
|
| 159 |
+
"""
|
| 160 |
+
if state["feedback_on_work"]:
|
| 161 |
+
user_message += f"Also, note that in a prior attempt from the Assistant, you provided this feedback: {state['feedback_on_work']}\n"
|
| 162 |
+
user_message += "If you're seeing the Assistant repeating the same mistakes, then consider responding that user input is required."
|
| 163 |
+
|
| 164 |
+
evaluator_messages = [SystemMessage(content=system_message), HumanMessage(content=user_message)]
|
| 165 |
+
|
| 166 |
+
eval_result = self.evaluator_llm_with_output.invoke(evaluator_messages)
|
| 167 |
+
new_state = {
|
| 168 |
+
"messages": [{"role": "assistant", "content": f"Evaluator Feedback on this answer: {eval_result.feedback}"}],
|
| 169 |
+
"feedback_on_work": eval_result.feedback,
|
| 170 |
+
"success_criteria_met": eval_result.success_criteria_met,
|
| 171 |
+
"user_input_needed": eval_result.user_input_needed
|
| 172 |
+
}
|
| 173 |
+
return new_state
|
| 174 |
+
|
| 175 |
+
def route_based_on_evaluation(self, state: State) -> str:
|
| 176 |
+
is_admin = state['username'] == 'admin'
|
| 177 |
+
|
| 178 |
+
if state["success_criteria_met"] or state["user_input_needed"]:
|
| 179 |
+
# Log search for non-admin users immediately
|
| 180 |
+
if not is_admin:
|
| 181 |
+
self.log_search(
|
| 182 |
+
state['username'],
|
| 183 |
+
self.search_id,
|
| 184 |
+
state['feedback_on_work'],
|
| 185 |
+
state["messages"][-1].content
|
| 186 |
+
)
|
| 187 |
+
return "END"
|
| 188 |
+
else:
|
| 189 |
+
return "worker"
|
| 190 |
+
|
| 191 |
+
def sqlformator(self, state: State) -> Dict[str, Any]:
|
| 192 |
+
"""
|
| 193 |
+
A node that completely replaces the state with just the formatted table
|
| 194 |
+
"""
|
| 195 |
+
print("SQL Formatter activated, completely replacing history with formatted table")
|
| 196 |
+
|
| 197 |
+
# Get the last message which should contain the JSON data
|
| 198 |
+
last_message = state["messages"][-1]
|
| 199 |
+
original_message = None
|
| 200 |
+
|
| 201 |
+
# Find the original user request for search history
|
| 202 |
+
for msg in state["messages"]:
|
| 203 |
+
if isinstance(msg, HumanMessage) and "search history" in msg.content.lower():
|
| 204 |
+
original_message = msg
|
| 205 |
+
break
|
| 206 |
+
|
| 207 |
+
# If no request found, use a generic one
|
| 208 |
+
if not original_message:
|
| 209 |
+
original_message = HumanMessage(content="give me the search history")
|
| 210 |
+
|
| 211 |
+
# Format the JSON data
|
| 212 |
+
try:
|
| 213 |
+
# Parse JSON to ensure we have valid data
|
| 214 |
+
import json
|
| 215 |
+
json_data = json.loads(last_message.content)
|
| 216 |
+
|
| 217 |
+
# Create system message for the formatting LLM
|
| 218 |
+
system_message = """You are a data formatting specialist. Format the provided JSON data as a clean ASCII table.
|
| 219 |
+
ONLY return the formatted table, nothing else. Do not reference or include the original JSON."""
|
| 220 |
+
|
| 221 |
+
# Create formatting prompt
|
| 222 |
+
user_message = f"""Format this JSON data as a readable ASCII table:
|
| 223 |
+
|
| 224 |
+
{last_message.content}
|
| 225 |
+
|
| 226 |
+
Create a table with these columns: ID, Thread ID, Username, Timestamp
|
| 227 |
+
Truncate long values for readability.
|
| 228 |
+
ONLY return the formatted table, nothing else."""
|
| 229 |
+
|
| 230 |
+
# Create a new LLM instance for formatting
|
| 231 |
+
formatting_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 232 |
+
|
| 233 |
+
# Get formatted response
|
| 234 |
+
formatting_response = formatting_llm.invoke([
|
| 235 |
+
SystemMessage(content=system_message),
|
| 236 |
+
HumanMessage(content=user_message)
|
| 237 |
+
])
|
| 238 |
+
|
| 239 |
+
# Create an enhanced formatted response
|
| 240 |
+
formatted_table = "# Search History Results\n\n" + formatting_response.content
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
print(f"Error formatting table: {e}")
|
| 244 |
+
formatted_table = f"Error formatting search history: {str(e)}"
|
| 245 |
+
|
| 246 |
+
# Create completely new state with minimal message history
|
| 247 |
+
# This is the key part - we're replacing the entire message history
|
| 248 |
+
new_state = {
|
| 249 |
+
"messages": [
|
| 250 |
+
SystemMessage(content="SQL assistant."),
|
| 251 |
+
original_message, # Original user request
|
| 252 |
+
AIMessage(content=formatted_table) # Only the formatted table
|
| 253 |
+
],
|
| 254 |
+
"username": state["username"],
|
| 255 |
+
"success_criteria": state["success_criteria"],
|
| 256 |
+
"feedback_on_work": None, # Reset feedback
|
| 257 |
+
"success_criteria_met": True, # Mark as completed
|
| 258 |
+
"user_input_needed": False,
|
| 259 |
+
"tool_name": None # Reset tool name
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
return new_state
|
| 263 |
+
|
| 264 |
+
def route_based_on_tools(self, state: State) -> str:
|
| 265 |
+
last_message = state["messages"][-1]
|
| 266 |
+
print("Tool output:", last_message.content)
|
| 267 |
+
|
| 268 |
+
print("State keys:", list(state.keys()))
|
| 269 |
+
|
| 270 |
+
tool_name = state.get("tool_name")
|
| 271 |
+
print("Tool name detected:", tool_name)
|
| 272 |
+
|
| 273 |
+
if tool_name == "get_user_history":
|
| 274 |
+
if not state.get("username") or state["username"].strip() == "":
|
| 275 |
+
print("Username not provided, search history access denied")
|
| 276 |
+
# Add a message to state explaining why access is denied
|
| 277 |
+
state["messages"].append(AIMessage(content="Error: Username is required to access search history. Please provide a username."))
|
| 278 |
+
return "END" # End the flow with error message
|
| 279 |
+
print("Detected get_user_history tool, routing to SQL formatter")
|
| 280 |
+
return "sqlformator"
|
| 281 |
+
else:
|
| 282 |
+
print("Routing to worker")
|
| 283 |
+
return "worker"
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def log_search(self,username:str,thread_id: str, feedback: str, reply: str):
|
| 288 |
+
conn = sqlite3.connect("query_log.db")
|
| 289 |
+
cursor = conn.cursor()
|
| 290 |
+
cursor.execute('''
|
| 291 |
+
INSERT INTO search_history (thread_id, username, feedback, reply)
|
| 292 |
+
VALUES (?, ?, ?, ?)
|
| 293 |
+
''', (thread_id,username, feedback, reply))
|
| 294 |
+
conn.commit()
|
| 295 |
+
conn.close()
|
| 296 |
+
|
| 297 |
+
async def build_graph(self):
|
| 298 |
+
# Set up Graph Builder with State
|
| 299 |
+
graph_builder = StateGraph(State)
|
| 300 |
+
|
| 301 |
+
# Add nodes
|
| 302 |
+
graph_builder.add_node("worker", self.worker)
|
| 303 |
+
graph_builder.add_node("tools", ToolNode(tools=self.tools))
|
| 304 |
+
graph_builder.add_node("evaluator", self.evaluator)
|
| 305 |
+
graph_builder.add_node("sqlformator", self.sqlformator)
|
| 306 |
+
|
| 307 |
+
# Add edges
|
| 308 |
+
graph_builder.add_conditional_edges("worker", self.worker_router, {"tools": "tools", "evaluator": "evaluator"})
|
| 309 |
+
graph_builder.add_conditional_edges("tools", self.route_based_on_tools, {"worker": "worker", "sqlformator": 'sqlformator'})
|
| 310 |
+
graph_builder.add_edge("sqlformator", END)
|
| 311 |
+
graph_builder.add_conditional_edges("evaluator", self.route_based_on_evaluation, {"worker": "worker", "END": END})
|
| 312 |
+
|
| 313 |
+
graph_builder.add_edge(START, "worker")
|
| 314 |
+
|
| 315 |
+
# Compile the graph
|
| 316 |
+
self.graph = graph_builder.compile(checkpointer=self.memory)
|
| 317 |
+
|
| 318 |
+
async def run_superstep(self, message, username, success_criteria, history):
|
| 319 |
+
config = {"configurable": {"thread_id": self.search_id}}
|
| 320 |
+
|
| 321 |
+
state = {
|
| 322 |
+
"messages": message,
|
| 323 |
+
"username": username,
|
| 324 |
+
"success_criteria": success_criteria or "The answer should be clear and accurate",
|
| 325 |
+
"feedback_on_work": None,
|
| 326 |
+
"success_criteria_met": False,
|
| 327 |
+
"user_input_needed": False,
|
| 328 |
+
"tool_name": None
|
| 329 |
+
}
|
| 330 |
+
result = await self.graph.ainvoke(state, config=config)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
user = {"role": "user", "content": message}
|
| 335 |
+
|
| 336 |
+
# For search history requests, find the formatted table response
|
| 337 |
+
if "search history" in message.lower():
|
| 338 |
+
formatted_table = None
|
| 339 |
+
for msg in reversed(result["messages"]):
|
| 340 |
+
content = msg.content if hasattr(msg, "content") else msg.get("content", "")
|
| 341 |
+
|
| 342 |
+
# Look for a message that contains table formatting indicators
|
| 343 |
+
if any(marker in content for marker in ["|", "+-", "ID", "Thread ID", "Username", "Timestamp"]):
|
| 344 |
+
formatted_table = content
|
| 345 |
+
break
|
| 346 |
+
|
| 347 |
+
if formatted_table:
|
| 348 |
+
reply = {"role": "assistant", "content": formatted_table}
|
| 349 |
+
else:
|
| 350 |
+
# Fallback to last message if no table found
|
| 351 |
+
reply = {"role": "assistant", "content": result["messages"][-1].content}
|
| 352 |
+
else:
|
| 353 |
+
# For non-search history requests, use normal processing
|
| 354 |
+
reply = {"role": "assistant", "content": result["messages"][-2].content}
|
| 355 |
+
|
| 356 |
+
reply = {"role": "assistant", "content": result["messages"][-2].content}
|
| 357 |
+
feedback = {"role": "assistant", "content": result["messages"][-1].content}
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
return history + [user, reply, feedback]
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def cleanup(self):
|
| 368 |
+
if self.browser:
|
| 369 |
+
try:
|
| 370 |
+
loop = asyncio.get_running_loop()
|
| 371 |
+
loop.create_task(self.browser.close())
|
| 372 |
+
if self.playwright:
|
| 373 |
+
loop.create_task(self.playwright.stop())
|
| 374 |
+
except RuntimeError:
|
| 375 |
+
# If no loop is running, do a direct run
|
| 376 |
+
asyncio.run(self.browser.close())
|
| 377 |
+
if self.playwright:
|
| 378 |
+
asyncio.run(self.playwright.stop())
|
SearchOps_Assistant/searcher_tools.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from playwright.async_api import async_playwright
|
| 2 |
+
from langchain_community.agent_toolkits import PlayWrightBrowserToolkit
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import os
|
| 5 |
+
import requests
|
| 6 |
+
from langchain.agents import Tool
|
| 7 |
+
from langchain_community.agent_toolkits import FileManagementToolkit
|
| 8 |
+
from langchain_community.tools.wikipedia.tool import WikipediaQueryRun
|
| 9 |
+
from langchain_experimental.tools import PythonREPLTool
|
| 10 |
+
from langchain_community.utilities import GoogleSerperAPIWrapper
|
| 11 |
+
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
|
| 12 |
+
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
|
| 13 |
+
from langchain.sql_database import SQLDatabase
|
| 14 |
+
import uuid
|
| 15 |
+
import sqlite3
|
| 16 |
+
load_dotenv(override=True)
|
| 17 |
+
pushover_token = os.getenv("PUSHOVER_TOKEN")
|
| 18 |
+
pushover_user = os.getenv("PUSHOVER_USER")
|
| 19 |
+
pushover_url = "https://api.pushover.net/1/messages.json"
|
| 20 |
+
serper = GoogleSerperAPIWrapper()
|
| 21 |
+
|
| 22 |
+
async def playwright_tools():
|
| 23 |
+
playwright = await async_playwright().start()
|
| 24 |
+
browser = await playwright.chromium.launch(headless=False)
|
| 25 |
+
toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=browser)
|
| 26 |
+
return toolkit.get_tools(), browser, playwright
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def push(text: str):
|
| 30 |
+
"""Send a push notification to the user"""
|
| 31 |
+
requests.post(pushover_url, data = {"token": pushover_token, "user": pushover_user, "message": text})
|
| 32 |
+
return "success"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_file_tools():
|
| 36 |
+
toolkit = FileManagementToolkit(root_dir="sandbox")
|
| 37 |
+
return toolkit.get_tools()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_user_history(username: str):
|
| 43 |
+
"""
|
| 44 |
+
Retrieves search history from the database for the given username only.
|
| 45 |
+
Users can only view their own search records.
|
| 46 |
+
"""
|
| 47 |
+
try:
|
| 48 |
+
conn = sqlite3.connect("query_log.db")
|
| 49 |
+
cursor = conn.cursor()
|
| 50 |
+
|
| 51 |
+
# Ensure table exists
|
| 52 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='search_history'")
|
| 53 |
+
if not cursor.fetchone():
|
| 54 |
+
print("[ERROR] search_history table does not exist")
|
| 55 |
+
return []
|
| 56 |
+
|
| 57 |
+
# Query the user's own records
|
| 58 |
+
query = '''
|
| 59 |
+
SELECT * FROM search_history
|
| 60 |
+
WHERE username = ?
|
| 61 |
+
ORDER BY timestamp DESC
|
| 62 |
+
'''
|
| 63 |
+
cursor.execute(query, (username,))
|
| 64 |
+
results = cursor.fetchall()
|
| 65 |
+
|
| 66 |
+
# Format the results into dictionaries
|
| 67 |
+
formatted_results = []
|
| 68 |
+
for row in results:
|
| 69 |
+
record = {}
|
| 70 |
+
for i, col in enumerate(cursor.description):
|
| 71 |
+
record[col[0]] = row[i]
|
| 72 |
+
formatted_results.append(record)
|
| 73 |
+
|
| 74 |
+
conn.close()
|
| 75 |
+
return formatted_results
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"[ERROR] Database error: {str(e)}")
|
| 79 |
+
return []
|
| 80 |
+
|
| 81 |
+
async def other_tools():
|
| 82 |
+
push_tool = Tool(name="send_push_notification", func=push, description="Use this tool when you want to send a push notification")
|
| 83 |
+
file_tools = get_file_tools()
|
| 84 |
+
|
| 85 |
+
tool_search =Tool(
|
| 86 |
+
name="search",
|
| 87 |
+
func=serper.run,
|
| 88 |
+
description="Use this tool when you want to get the results of an online web search"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
get_history_tool = Tool(
|
| 92 |
+
name="get_user_history",
|
| 93 |
+
func=get_user_history,
|
| 94 |
+
description="Use this tool when user wants to view their own search history"
|
| 95 |
+
)
|
| 96 |
+
wikipedia = WikipediaAPIWrapper()
|
| 97 |
+
wiki_tool = WikipediaQueryRun(api_wrapper=wikipedia)
|
| 98 |
+
|
| 99 |
+
python_repl = PythonREPLTool()
|
| 100 |
+
|
| 101 |
+
return file_tools + [push_tool, tool_search, python_repl, wiki_tool, get_history_tool]
|
| 102 |
+
|
SearchOps_Assistant/setup.sh
ADDED
|
File without changes
|
__pycache__/search.cpython-312.pyc
ADDED
|
Binary file (18.4 kB). View file
|
|
|
__pycache__/searcher_tools.cpython-312.pyc
ADDED
|
Binary file (4.81 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from search import Search
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
async def setup():
|
| 6 |
+
search = Search()
|
| 7 |
+
await search.setup()
|
| 8 |
+
return search
|
| 9 |
+
|
| 10 |
+
async def process_message(search, username, message, success_criteria, history):
|
| 11 |
+
results = await search.run_superstep(message, username, success_criteria, history)
|
| 12 |
+
return results, search
|
| 13 |
+
|
| 14 |
+
async def reset():
|
| 15 |
+
new_search = Search()
|
| 16 |
+
await new_search.setup()
|
| 17 |
+
return "", "","", None, new_search
|
| 18 |
+
|
| 19 |
+
def free_resources(search):
|
| 20 |
+
print("Cleaning up")
|
| 21 |
+
try:
|
| 22 |
+
if search:
|
| 23 |
+
search.free_resources()
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f"Exception during cleanup: {e}")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
with gr.Blocks(title="Search", theme=gr.themes.Default(primary_hue="emerald")) as ui:
|
| 29 |
+
gr.Markdown("## Search Personal Co-Worker")
|
| 30 |
+
search = gr.State(delete_callback=free_resources)
|
| 31 |
+
|
| 32 |
+
with gr.Row():
|
| 33 |
+
chatbot = gr.Chatbot(label="Search", height=300, type="messages")
|
| 34 |
+
with gr.Group():
|
| 35 |
+
with gr.Row():
|
| 36 |
+
username = gr.Textbox(show_label=False, placeholder="Enter your username")
|
| 37 |
+
with gr.Row():
|
| 38 |
+
message = gr.Textbox(show_label=False, placeholder="Your request to the Search")
|
| 39 |
+
with gr.Row():
|
| 40 |
+
success_criteria = gr.Textbox(show_label=False, placeholder="What are your success critiera?")
|
| 41 |
+
with gr.Row():
|
| 42 |
+
reset_button = gr.Button("Reset", variant="stop")
|
| 43 |
+
go_button = gr.Button("Go!", variant="primary")
|
| 44 |
+
|
| 45 |
+
ui.load(setup, [], [search])
|
| 46 |
+
message.submit(process_message, [search,username, message, success_criteria, chatbot], [chatbot, search])
|
| 47 |
+
success_criteria.submit(process_message, [search, username, message, success_criteria, chatbot], [chatbot, search])
|
| 48 |
+
go_button.click(process_message, [search, username, message, success_criteria, chatbot], [chatbot, search])
|
| 49 |
+
reset_button.click(reset, [], [username, message, success_criteria, chatbot, search])
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
ui.launch(inbrowser=True)
|
db.ipynb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"outputs": [],
|
| 8 |
+
"source": [
|
| 9 |
+
"from langchain.agents.agent_toolkits import SQLDatabaseToolkit\n",
|
| 10 |
+
"from langchain.sql_database import SQLDatabase\n",
|
| 11 |
+
"from langchain.agents import Tool"
|
| 12 |
+
]
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"cell_type": "code",
|
| 16 |
+
"execution_count": 11,
|
| 17 |
+
"metadata": {},
|
| 18 |
+
"outputs": [],
|
| 19 |
+
"source": [
|
| 20 |
+
"import sqlite3\n",
|
| 21 |
+
"\n",
|
| 22 |
+
"conn = sqlite3.connect(\"query_log.db\")\n",
|
| 23 |
+
"cursor = conn.cursor()\n",
|
| 24 |
+
"\n",
|
| 25 |
+
"cursor.execute('''\n",
|
| 26 |
+
"CREATE TABLE IF NOT EXISTS search_history (\n",
|
| 27 |
+
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n",
|
| 28 |
+
" thread_id TEXT,\n",
|
| 29 |
+
" username TEXT,\n",
|
| 30 |
+
" feedback TEXT,\n",
|
| 31 |
+
" reply TEXT,\n",
|
| 32 |
+
" timestamp DATETIME DEFAULT CURRENT_TIMESTAMP\n",
|
| 33 |
+
")\n",
|
| 34 |
+
"''')\n",
|
| 35 |
+
"\n",
|
| 36 |
+
"conn.commit()\n",
|
| 37 |
+
"conn.close()\n"
|
| 38 |
+
]
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"cell_type": "code",
|
| 42 |
+
"execution_count": null,
|
| 43 |
+
"metadata": {},
|
| 44 |
+
"outputs": [],
|
| 45 |
+
"source": []
|
| 46 |
+
}
|
| 47 |
+
],
|
| 48 |
+
"metadata": {
|
| 49 |
+
"kernelspec": {
|
| 50 |
+
"display_name": ".venv",
|
| 51 |
+
"language": "python",
|
| 52 |
+
"name": "python3"
|
| 53 |
+
},
|
| 54 |
+
"language_info": {
|
| 55 |
+
"codemirror_mode": {
|
| 56 |
+
"name": "ipython",
|
| 57 |
+
"version": 3
|
| 58 |
+
},
|
| 59 |
+
"file_extension": ".py",
|
| 60 |
+
"mimetype": "text/x-python",
|
| 61 |
+
"name": "python",
|
| 62 |
+
"nbconvert_exporter": "python",
|
| 63 |
+
"pygments_lexer": "ipython3",
|
| 64 |
+
"version": "3.12.7"
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"nbformat": 4,
|
| 68 |
+
"nbformat_minor": 2
|
| 69 |
+
}
|
example.db
ADDED
|
File without changes
|
query_log.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.29.1
|
| 2 |
+
langchain==0.3.25
|
| 3 |
+
langchain_community==0.3.24
|
| 4 |
+
langchain_core==0.3.60
|
| 5 |
+
langchain_experimental==0.3.4
|
| 6 |
+
langchain_openai==0.3.17
|
| 7 |
+
langgraph==0.4.5
|
| 8 |
+
playwright==1.52.0
|
| 9 |
+
pydantic==2.11.4
|
| 10 |
+
python-dotenv==1.1.0
|
| 11 |
+
Requests==2.32.3
|
| 12 |
+
typing_extensions==4.13.2
|
| 13 |
+
|
search.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated
|
| 2 |
+
from typing_extensions import TypedDict
|
| 3 |
+
from langgraph.graph import StateGraph, START, END
|
| 4 |
+
from langgraph.graph.message import add_messages
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from langgraph.prebuilt import ToolNode
|
| 7 |
+
from langchain_openai import ChatOpenAI
|
| 8 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 9 |
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
| 10 |
+
from typing import List, Any, Optional, Dict
|
| 11 |
+
from pydantic import BaseModel, Field
|
| 12 |
+
from searcher_tools import playwright_tools, other_tools
|
| 13 |
+
import uuid
|
| 14 |
+
import asyncio
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
import sqlite3
|
| 17 |
+
import json
|
| 18 |
+
load_dotenv(override=True)
|
| 19 |
+
|
| 20 |
+
class State(TypedDict):
|
| 21 |
+
messages: Annotated[List[Any], add_messages]
|
| 22 |
+
username: str
|
| 23 |
+
success_criteria: str
|
| 24 |
+
feedback_on_work: Optional[str]
|
| 25 |
+
success_criteria_met: bool
|
| 26 |
+
user_input_needed: bool
|
| 27 |
+
tool_name: Optional[str]
|
| 28 |
+
|
| 29 |
+
class EvaluatorOutput(BaseModel):
|
| 30 |
+
feedback: str = Field(description="Feedback on the assistant's response")
|
| 31 |
+
success_criteria_met: bool = Field(description="Whether the success criteria have been met")
|
| 32 |
+
user_input_needed: bool = Field(description="True if more input is needed from the user, or clarifications, or the assistant is stuck")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Search:
|
| 36 |
+
def __init__(self):
|
| 37 |
+
self.worker_llm_with_tools = None
|
| 38 |
+
self.evaluator_llm_with_output = None
|
| 39 |
+
self.tools = None
|
| 40 |
+
self.llm_with_tools = None
|
| 41 |
+
self.graph = None
|
| 42 |
+
self.search_id = str(uuid.uuid4())
|
| 43 |
+
self.memory = MemorySaver()
|
| 44 |
+
self.browser = None
|
| 45 |
+
self.playwright = None
|
| 46 |
+
self.formatting_llm = None
|
| 47 |
+
async def setup(self):
|
| 48 |
+
self.tools, self.browser, self.playwright = await playwright_tools()
|
| 49 |
+
self.tools += await other_tools()
|
| 50 |
+
worker_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 51 |
+
self.worker_llm_with_tools = worker_llm.bind_tools(self.tools)
|
| 52 |
+
evaluator_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 53 |
+
self.evaluator_llm_with_output = evaluator_llm.with_structured_output(EvaluatorOutput)
|
| 54 |
+
self.formatting_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 55 |
+
await self.build_graph()
|
| 56 |
+
|
| 57 |
+
def worker(self, state: State) -> Dict[str, Any]:
|
| 58 |
+
|
| 59 |
+
if not state.get("username") or state["username"].strip() == "":
|
| 60 |
+
# Username not provided, return response asking for username
|
| 61 |
+
response = AIMessage(content="Please provide a username before search or accessing search history.")
|
| 62 |
+
new_state = dict(state)
|
| 63 |
+
new_state["messages"] = [response]
|
| 64 |
+
new_state["tool_name"] = None
|
| 65 |
+
new_state["user_input_needed"] = True # Signal that we need user input
|
| 66 |
+
return new_state
|
| 67 |
+
|
| 68 |
+
system_message = f"""You are a helpful assistant that can use tools to complete tasks.
|
| 69 |
+
You keep working on a task until either you have a question or clarification for the user, or the success criteria is met.
|
| 70 |
+
You have many tools to help you, including tools to browse the internet, navigating and retrieving web pages.
|
| 71 |
+
You have a tool to run python code, but note that you would need to include a print() statement if you wanted to receive output.
|
| 72 |
+
The username is {state['username']}The current date and time is {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 73 |
+
|
| 74 |
+
This is the success criteria:
|
| 75 |
+
{state['success_criteria']}
|
| 76 |
+
You should reply either with a question for the user about this assignment, or with your final response.
|
| 77 |
+
If you have a question for the user, you need to reply by clearly stating your question. An example might be:
|
| 78 |
+
|
| 79 |
+
Question: please clarify whether you want a summary or a detailed answer
|
| 80 |
+
|
| 81 |
+
If you've finished, reply with the final answer, and don't ask a question; simply reply with the answer.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
if state.get("feedback_on_work"):
|
| 85 |
+
system_message += f"""
|
| 86 |
+
Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.
|
| 87 |
+
Here is the feedback on why this was rejected:
|
| 88 |
+
{state['feedback_on_work']}
|
| 89 |
+
With this feedback, please continue the assignment, ensuring that you meet the success criteria or have a question for the user."""
|
| 90 |
+
|
| 91 |
+
# Add in the system message
|
| 92 |
+
|
| 93 |
+
found_system_message = False
|
| 94 |
+
messages = state["messages"]
|
| 95 |
+
for message in messages:
|
| 96 |
+
if isinstance(message, SystemMessage):
|
| 97 |
+
message.content = system_message
|
| 98 |
+
found_system_message = True
|
| 99 |
+
|
| 100 |
+
if not found_system_message:
|
| 101 |
+
messages = [SystemMessage(content=system_message)] + messages
|
| 102 |
+
|
| 103 |
+
# Invoke the LLM with tools
|
| 104 |
+
response = self.worker_llm_with_tools.invoke(messages)
|
| 105 |
+
tool_name = None
|
| 106 |
+
if hasattr(response, "tool_calls") and response.tool_calls:
|
| 107 |
+
tool_name = response.tool_calls[0]["name"]
|
| 108 |
+
print(f"Next tool to run: {tool_name}")
|
| 109 |
+
# Return updated state
|
| 110 |
+
new_state = dict(state)
|
| 111 |
+
new_state["messages"] = [response]
|
| 112 |
+
new_state["tool_name"] = tool_name
|
| 113 |
+
|
| 114 |
+
return new_state
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def worker_router(self, state: State) -> str:
|
| 118 |
+
last_message = state["messages"][-1]
|
| 119 |
+
|
| 120 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 121 |
+
return "tools"
|
| 122 |
+
else:
|
| 123 |
+
return "evaluator"
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def format_conversation(self, messages: List[Any]) -> str:
|
| 127 |
+
conversation = "Conversation history:\n\n"
|
| 128 |
+
for message in messages:
|
| 129 |
+
if isinstance(message, HumanMessage):
|
| 130 |
+
conversation += f"User: {message.content}\n"
|
| 131 |
+
elif isinstance(message, AIMessage):
|
| 132 |
+
text = message.content or "[Tools use]"
|
| 133 |
+
conversation += f"Assistant: {text}\n"
|
| 134 |
+
return conversation
|
| 135 |
+
|
| 136 |
+
def evaluator(self, state: State) -> State:
|
| 137 |
+
last_response = state["messages"][-1].content
|
| 138 |
+
|
| 139 |
+
system_message = f"""You are an evaluator that determines if a task has been completed successfully by an Assistant.
|
| 140 |
+
Assess the Assistant's last response based on the given criteria. Respond with your feedback, and with your decision on whether the success criteria has been met,
|
| 141 |
+
and whether more input is needed from the user."""
|
| 142 |
+
|
| 143 |
+
user_message = f"""You are evaluating a conversation between the User and Assistant. You decide what action to take based on the last response from the Assistant.
|
| 144 |
+
|
| 145 |
+
The entire conversation with the assistant, with the user's original request and all replies, is:
|
| 146 |
+
{self.format_conversation(state['messages'])}
|
| 147 |
+
|
| 148 |
+
The success criteria for this assignment is:
|
| 149 |
+
{state['success_criteria']}
|
| 150 |
+
|
| 151 |
+
And the final response from the Assistant that you are evaluating is:
|
| 152 |
+
{last_response}
|
| 153 |
+
|
| 154 |
+
Respond with your feedback, and decide if the success criteria is met by this response.
|
| 155 |
+
Also, decide if more user input is required, either because the assistant has a question, needs clarification, or seems to be stuck and unable to answer without help.
|
| 156 |
+
|
| 157 |
+
The Assistant has access to tools, including a tool to retrieve search history. If the Assistant correctly uses this tool, regardless of the result (which depends on permissions),
|
| 158 |
+
then consider the success criteria met if the Assistant did its job correctly and communicated results or limitations properly.
|
| 159 |
+
"""
|
| 160 |
+
if state["feedback_on_work"]:
|
| 161 |
+
user_message += f"Also, note that in a prior attempt from the Assistant, you provided this feedback: {state['feedback_on_work']}\n"
|
| 162 |
+
user_message += "If you're seeing the Assistant repeating the same mistakes, then consider responding that user input is required."
|
| 163 |
+
|
| 164 |
+
evaluator_messages = [SystemMessage(content=system_message), HumanMessage(content=user_message)]
|
| 165 |
+
|
| 166 |
+
eval_result = self.evaluator_llm_with_output.invoke(evaluator_messages)
|
| 167 |
+
new_state = {
|
| 168 |
+
"messages": [{"role": "assistant", "content": f"Evaluator Feedback on this answer: {eval_result.feedback}"}],
|
| 169 |
+
"feedback_on_work": eval_result.feedback,
|
| 170 |
+
"success_criteria_met": eval_result.success_criteria_met,
|
| 171 |
+
"user_input_needed": eval_result.user_input_needed
|
| 172 |
+
}
|
| 173 |
+
return new_state
|
| 174 |
+
|
| 175 |
+
def route_based_on_evaluation(self, state: State) -> str:
|
| 176 |
+
is_admin = state['username'] == 'admin'
|
| 177 |
+
|
| 178 |
+
if state["success_criteria_met"] or state["user_input_needed"]:
|
| 179 |
+
# Log search for non-admin users immediately
|
| 180 |
+
if not is_admin:
|
| 181 |
+
self.log_search(
|
| 182 |
+
state['username'],
|
| 183 |
+
self.search_id,
|
| 184 |
+
state['feedback_on_work'],
|
| 185 |
+
state["messages"][-1].content
|
| 186 |
+
)
|
| 187 |
+
return "END"
|
| 188 |
+
else:
|
| 189 |
+
return "worker"
|
| 190 |
+
|
| 191 |
+
def sqlformator(self, state: State) -> Dict[str, Any]:
|
| 192 |
+
"""
|
| 193 |
+
A node that completely replaces the state with just the formatted table
|
| 194 |
+
"""
|
| 195 |
+
print("SQL Formatter activated, completely replacing history with formatted table")
|
| 196 |
+
|
| 197 |
+
# Get the last message which should contain the JSON data
|
| 198 |
+
last_message = state["messages"][-1]
|
| 199 |
+
original_message = None
|
| 200 |
+
|
| 201 |
+
# Find the original user request for search history
|
| 202 |
+
for msg in state["messages"]:
|
| 203 |
+
if isinstance(msg, HumanMessage) and "search history" in msg.content.lower():
|
| 204 |
+
original_message = msg
|
| 205 |
+
break
|
| 206 |
+
|
| 207 |
+
# If no request found, use a generic one
|
| 208 |
+
if not original_message:
|
| 209 |
+
original_message = HumanMessage(content="give me the search history")
|
| 210 |
+
|
| 211 |
+
# Format the JSON data
|
| 212 |
+
try:
|
| 213 |
+
# Parse JSON to ensure we have valid data
|
| 214 |
+
import json
|
| 215 |
+
json_data = json.loads(last_message.content)
|
| 216 |
+
|
| 217 |
+
# Create system message for the formatting LLM
|
| 218 |
+
system_message = """You are a data formatting specialist. Format the provided JSON data as a clean ASCII table.
|
| 219 |
+
ONLY return the formatted table, nothing else. Do not reference or include the original JSON."""
|
| 220 |
+
|
| 221 |
+
# Create formatting prompt
|
| 222 |
+
user_message = f"""Format this JSON data as a readable ASCII table:
|
| 223 |
+
|
| 224 |
+
{last_message.content}
|
| 225 |
+
|
| 226 |
+
Create a table with these columns: ID, Thread ID, Username, Timestamp
|
| 227 |
+
Truncate long values for readability.
|
| 228 |
+
ONLY return the formatted table, nothing else."""
|
| 229 |
+
|
| 230 |
+
# Create a new LLM instance for formatting
|
| 231 |
+
formatting_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 232 |
+
|
| 233 |
+
# Get formatted response
|
| 234 |
+
formatting_response = formatting_llm.invoke([
|
| 235 |
+
SystemMessage(content=system_message),
|
| 236 |
+
HumanMessage(content=user_message)
|
| 237 |
+
])
|
| 238 |
+
|
| 239 |
+
# Create an enhanced formatted response
|
| 240 |
+
formatted_table = "# Search History Results\n\n" + formatting_response.content
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
print(f"Error formatting table: {e}")
|
| 244 |
+
formatted_table = f"Error formatting search history: {str(e)}"
|
| 245 |
+
|
| 246 |
+
# Create completely new state with minimal message history
|
| 247 |
+
# This is the key part - we're replacing the entire message history
|
| 248 |
+
new_state = {
|
| 249 |
+
"messages": [
|
| 250 |
+
SystemMessage(content="SQL assistant."),
|
| 251 |
+
original_message, # Original user request
|
| 252 |
+
AIMessage(content=formatted_table) # Only the formatted table
|
| 253 |
+
],
|
| 254 |
+
"username": state["username"],
|
| 255 |
+
"success_criteria": state["success_criteria"],
|
| 256 |
+
"feedback_on_work": None, # Reset feedback
|
| 257 |
+
"success_criteria_met": True, # Mark as completed
|
| 258 |
+
"user_input_needed": False,
|
| 259 |
+
"tool_name": None # Reset tool name
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
return new_state
|
| 263 |
+
|
| 264 |
+
def route_based_on_tools(self, state: State) -> str:
|
| 265 |
+
last_message = state["messages"][-1]
|
| 266 |
+
print("Tool output:", last_message.content)
|
| 267 |
+
|
| 268 |
+
print("State keys:", list(state.keys()))
|
| 269 |
+
|
| 270 |
+
tool_name = state.get("tool_name")
|
| 271 |
+
print("Tool name detected:", tool_name)
|
| 272 |
+
|
| 273 |
+
if tool_name == "get_user_history":
|
| 274 |
+
if not state.get("username") or state["username"].strip() == "":
|
| 275 |
+
print("Username not provided, search history access denied")
|
| 276 |
+
# Add a message to state explaining why access is denied
|
| 277 |
+
state["messages"].append(AIMessage(content="Error: Username is required to access search history. Please provide a username."))
|
| 278 |
+
return "END" # End the flow with error message
|
| 279 |
+
print("Detected get_user_history tool, routing to SQL formatter")
|
| 280 |
+
return "sqlformator"
|
| 281 |
+
else:
|
| 282 |
+
print("Routing to worker")
|
| 283 |
+
return "worker"
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def log_search(self,username:str,thread_id: str, feedback: str, reply: str):
|
| 288 |
+
conn = sqlite3.connect("query_log.db")
|
| 289 |
+
cursor = conn.cursor()
|
| 290 |
+
cursor.execute('''
|
| 291 |
+
INSERT INTO search_history (thread_id, username, feedback, reply)
|
| 292 |
+
VALUES (?, ?, ?, ?)
|
| 293 |
+
''', (thread_id,username, feedback, reply))
|
| 294 |
+
conn.commit()
|
| 295 |
+
conn.close()
|
| 296 |
+
|
| 297 |
+
async def build_graph(self):
|
| 298 |
+
# Set up Graph Builder with State
|
| 299 |
+
graph_builder = StateGraph(State)
|
| 300 |
+
|
| 301 |
+
# Add nodes
|
| 302 |
+
graph_builder.add_node("worker", self.worker)
|
| 303 |
+
graph_builder.add_node("tools", ToolNode(tools=self.tools))
|
| 304 |
+
graph_builder.add_node("evaluator", self.evaluator)
|
| 305 |
+
graph_builder.add_node("sqlformator", self.sqlformator)
|
| 306 |
+
|
| 307 |
+
# Add edges
|
| 308 |
+
graph_builder.add_conditional_edges("worker", self.worker_router, {"tools": "tools", "evaluator": "evaluator"})
|
| 309 |
+
graph_builder.add_conditional_edges("tools", self.route_based_on_tools, {"worker": "worker", "sqlformator": 'sqlformator'})
|
| 310 |
+
graph_builder.add_edge("sqlformator", END)
|
| 311 |
+
graph_builder.add_conditional_edges("evaluator", self.route_based_on_evaluation, {"worker": "worker", "END": END})
|
| 312 |
+
|
| 313 |
+
graph_builder.add_edge(START, "worker")
|
| 314 |
+
|
| 315 |
+
# Compile the graph
|
| 316 |
+
self.graph = graph_builder.compile(checkpointer=self.memory)
|
| 317 |
+
|
| 318 |
+
async def run_superstep(self, message, username, success_criteria, history):
|
| 319 |
+
config = {"configurable": {"thread_id": self.search_id}}
|
| 320 |
+
|
| 321 |
+
state = {
|
| 322 |
+
"messages": message,
|
| 323 |
+
"username": username,
|
| 324 |
+
"success_criteria": success_criteria or "The answer should be clear and accurate",
|
| 325 |
+
"feedback_on_work": None,
|
| 326 |
+
"success_criteria_met": False,
|
| 327 |
+
"user_input_needed": False,
|
| 328 |
+
"tool_name": None
|
| 329 |
+
}
|
| 330 |
+
result = await self.graph.ainvoke(state, config=config)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
user = {"role": "user", "content": message}
|
| 335 |
+
|
| 336 |
+
# For search history requests, find the formatted table response
|
| 337 |
+
if "search history" in message.lower():
|
| 338 |
+
formatted_table = None
|
| 339 |
+
for msg in reversed(result["messages"]):
|
| 340 |
+
content = msg.content if hasattr(msg, "content") else msg.get("content", "")
|
| 341 |
+
|
| 342 |
+
# Look for a message that contains table formatting indicators
|
| 343 |
+
if any(marker in content for marker in ["|", "+-", "ID", "Thread ID", "Username", "Timestamp"]):
|
| 344 |
+
formatted_table = content
|
| 345 |
+
break
|
| 346 |
+
|
| 347 |
+
if formatted_table:
|
| 348 |
+
reply = {"role": "assistant", "content": formatted_table}
|
| 349 |
+
else:
|
| 350 |
+
# Fallback to last message if no table found
|
| 351 |
+
reply = {"role": "assistant", "content": result["messages"][-1].content}
|
| 352 |
+
else:
|
| 353 |
+
# For non-search history requests, use normal processing
|
| 354 |
+
reply = {"role": "assistant", "content": result["messages"][-2].content}
|
| 355 |
+
|
| 356 |
+
reply = {"role": "assistant", "content": result["messages"][-2].content}
|
| 357 |
+
feedback = {"role": "assistant", "content": result["messages"][-1].content}
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
return history + [user, reply, feedback]
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def cleanup(self):
|
| 368 |
+
if self.browser:
|
| 369 |
+
try:
|
| 370 |
+
loop = asyncio.get_running_loop()
|
| 371 |
+
loop.create_task(self.browser.close())
|
| 372 |
+
if self.playwright:
|
| 373 |
+
loop.create_task(self.playwright.stop())
|
| 374 |
+
except RuntimeError:
|
| 375 |
+
# If no loop is running, do a direct run
|
| 376 |
+
asyncio.run(self.browser.close())
|
| 377 |
+
if self.playwright:
|
| 378 |
+
asyncio.run(self.playwright.stop())
|
searcher_tools.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from playwright.async_api import async_playwright
|
| 2 |
+
from langchain_community.agent_toolkits import PlayWrightBrowserToolkit
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import os
|
| 5 |
+
import requests
|
| 6 |
+
from langchain.agents import Tool
|
| 7 |
+
from langchain_community.agent_toolkits import FileManagementToolkit
|
| 8 |
+
from langchain_community.tools.wikipedia.tool import WikipediaQueryRun
|
| 9 |
+
from langchain_experimental.tools import PythonREPLTool
|
| 10 |
+
from langchain_community.utilities import GoogleSerperAPIWrapper
|
| 11 |
+
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
|
| 12 |
+
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
|
| 13 |
+
from langchain.sql_database import SQLDatabase
|
| 14 |
+
import uuid
|
| 15 |
+
import sqlite3
|
| 16 |
+
load_dotenv(override=True)
|
| 17 |
+
pushover_token = os.getenv("PUSHOVER_TOKEN")
|
| 18 |
+
pushover_user = os.getenv("PUSHOVER_USER")
|
| 19 |
+
pushover_url = "https://api.pushover.net/1/messages.json"
|
| 20 |
+
serper = GoogleSerperAPIWrapper()
|
| 21 |
+
|
| 22 |
+
async def playwright_tools():
|
| 23 |
+
playwright = await async_playwright().start()
|
| 24 |
+
browser = await playwright.chromium.launch(headless=False)
|
| 25 |
+
toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=browser)
|
| 26 |
+
return toolkit.get_tools(), browser, playwright
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def push(text: str):
|
| 30 |
+
"""Send a push notification to the user"""
|
| 31 |
+
requests.post(pushover_url, data = {"token": pushover_token, "user": pushover_user, "message": text})
|
| 32 |
+
return "success"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_file_tools():
|
| 36 |
+
toolkit = FileManagementToolkit(root_dir="sandbox")
|
| 37 |
+
return toolkit.get_tools()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def get_user_history(username: str):
|
| 43 |
+
"""
|
| 44 |
+
Retrieves search history from the database for the given username only.
|
| 45 |
+
Users can only view their own search records.
|
| 46 |
+
"""
|
| 47 |
+
try:
|
| 48 |
+
conn = sqlite3.connect("query_log.db")
|
| 49 |
+
cursor = conn.cursor()
|
| 50 |
+
|
| 51 |
+
# Ensure table exists
|
| 52 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='search_history'")
|
| 53 |
+
if not cursor.fetchone():
|
| 54 |
+
print("[ERROR] search_history table does not exist")
|
| 55 |
+
return []
|
| 56 |
+
|
| 57 |
+
# Query the user's own records
|
| 58 |
+
query = '''
|
| 59 |
+
SELECT * FROM search_history
|
| 60 |
+
WHERE username = ?
|
| 61 |
+
ORDER BY timestamp DESC
|
| 62 |
+
'''
|
| 63 |
+
cursor.execute(query, (username,))
|
| 64 |
+
results = cursor.fetchall()
|
| 65 |
+
|
| 66 |
+
# Format the results into dictionaries
|
| 67 |
+
formatted_results = []
|
| 68 |
+
for row in results:
|
| 69 |
+
record = {}
|
| 70 |
+
for i, col in enumerate(cursor.description):
|
| 71 |
+
record[col[0]] = row[i]
|
| 72 |
+
formatted_results.append(record)
|
| 73 |
+
|
| 74 |
+
conn.close()
|
| 75 |
+
return formatted_results
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"[ERROR] Database error: {str(e)}")
|
| 79 |
+
return []
|
| 80 |
+
|
| 81 |
+
async def other_tools():
|
| 82 |
+
push_tool = Tool(name="send_push_notification", func=push, description="Use this tool when you want to send a push notification")
|
| 83 |
+
file_tools = get_file_tools()
|
| 84 |
+
|
| 85 |
+
tool_search =Tool(
|
| 86 |
+
name="search",
|
| 87 |
+
func=serper.run,
|
| 88 |
+
description="Use this tool when you want to get the results of an online web search"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
get_history_tool = Tool(
|
| 92 |
+
name="get_user_history",
|
| 93 |
+
func=get_user_history,
|
| 94 |
+
description="Use this tool when user wants to view their own search history"
|
| 95 |
+
)
|
| 96 |
+
wikipedia = WikipediaAPIWrapper()
|
| 97 |
+
wiki_tool = WikipediaQueryRun(api_wrapper=wikipedia)
|
| 98 |
+
|
| 99 |
+
python_repl = PythonREPLTool()
|
| 100 |
+
|
| 101 |
+
return file_tools + [push_tool, tool_search, python_repl, wiki_tool, get_history_tool]
|
| 102 |
+
|
setup.sh
ADDED
|
File without changes
|