Commit 862ec611 authored by nextime's avatar nextime

Initial commit: Add OLProxy with comprehensive documentation and GPLv3 licensing

- Add olproxy.py: Python proxy server bridging web chatbots to Ollama API
- Add README.md: Comprehensive documentation with logo, usage examples, and API reference
- Add CHANGELOG.md: Structured changelog following Keep a Changelog format
- Add LICENSE: GPLv3 license with copyright attribution to Stefy Lanza
- Add olproxy.jpg: Project logo
- All files include proper GPLv3 licensing and attribution
parents
# Changelog
All notable changes to the OLProxy project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Initial project documentation
- GPLv3 license implementation
- Comprehensive README with usage examples
## [0.1.0] - 2025-08-23
### Added
- Initial release of OLProxy
- Ollama API compatibility layer
- OpenAI API compatibility layer
- Playwright-based web chatbot integration
- Support for Grok AI via X/Twitter interface
- Support for Grok AI via grok.com interface
- Advanced response extraction with spy word technique
- Progressive content detection fallback system
- Heuristic response detection as final fallback
- CORS middleware for web application integration
- Persistent browser context for session management
- Command-line interface with configurable IP and port
- Support for connecting to existing browser instances via CDP
- Health check endpoint
- Comprehensive error handling and logging
- Multiple model configuration support
### Features
- **API Endpoints**:
- `GET /api/tags` - List available models
- `GET /api/show/{model}` - Show model details
- `POST /api/generate` - Generate text completion
- `POST /api/chat` - Chat completion
- `GET /api/ps` - List loaded models
- `GET /api/version` - Get version information
- `GET /` - Health check
- `GET /v1/models` - OpenAI-compatible model listing
- `POST /v1/chat/completions` - OpenAI-compatible chat completion
- **Response Extraction Strategies**:
- Primary: Spy word detection with flexible pattern matching
- Secondary: Progressive content monitoring with stability detection
- Fallback: Heuristic latest response detection
- **Browser Management**:
- Persistent browser context in `./playwright_data/`
- Automatic page management per chatbot model
- Support for headless and headed browser modes
- CDP connection support for external browser instances
- **Configuration**:
- Configurable chatbot endpoints via `CHATBOT_CONFIG`
- Customizable CSS selectors for different web interfaces
- Flexible spy word configuration per model
- Command-line argument support
### Technical Details
- Built with Python 3.7+ compatibility
- Uses `aiohttp` for async HTTP server
- Uses `playwright` for browser automation
- Implements comprehensive error handling
- Supports concurrent requests with proper browser page management
- Includes detailed logging for debugging and monitoring
### Known Limitations
- Dependent on web interface stability of target chatbot services
- Subject to rate limiting of underlying web services
- May require manual authentication for some services
- Response extraction reliability depends on consistent web UI patterns
---
## Version History
### Version Numbering
This project uses [Semantic Versioning](https://semver.org/):
- **MAJOR** version for incompatible API changes
- **MINOR** version for backwards-compatible functionality additions
- **PATCH** version for backwards-compatible bug fixes
### Release Notes
- **v0.1.0**: Initial stable release with core functionality
- Future versions will include additional chatbot integrations, improved response extraction, and enhanced error handling
---
## Contributing
When contributing to this project, please:
1. Update this changelog with your changes
2. Follow the established format and categorization
3. Include version bumps according to semantic versioning
4. Document breaking changes clearly
5. Add entries under "Unreleased" section until release
### Categories
- **Added** for new features
- **Changed** for changes in existing functionality
- **Deprecated** for soon-to-be removed features
- **Removed** for now removed features
- **Fixed** for any bug fixes
- **Security** for vulnerability fixes
\ No newline at end of file
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2025 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For the full text of the GNU General Public License version 3,
please visit: https://www.gnu.org/licenses/gpl-3.0.html
\ No newline at end of file
# OLProxy - Ollama API Proxy for Web-based AI Chatbots
![OLProxy Logo](olproxy.jpg)
**OLProxy** is a Python-based proxy server that bridges web-based AI chatbots (like Grok on X/Twitter) to the standardized Ollama API format. This allows developers to use familiar Ollama API calls while leveraging free or accessible web-based AI services, providing a cost-effective solution for AI integration.
## Features
- **Ollama API Compatibility**: Full support for Ollama API endpoints (`/api/generate`, `/api/chat`, `/api/tags`, etc.)
- **OpenAI API Compatibility**: Support for OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/models`)
- **Web Chatbot Integration**: Uses Playwright to interact with web-based AI interfaces
- **Multiple AI Models**: Configurable support for different chatbot services
- **Smart Response Extraction**: Advanced "spy word" technique for reliable response extraction
- **CORS Support**: Built-in CORS middleware for web application integration
- **Persistent Browser Sessions**: Maintains browser context for efficient interactions
## Supported Models
Currently configured models include:
- `grok:latest` - Grok AI via X/Twitter interface
- `grok-beta:latest` - Grok AI via grok.com
- `llama2:latest` - Mapped to Grok interface
- `codellama:latest` - Mapped to Grok interface
## Installation
### Prerequisites
- Python 3.7+
- Playwright
- aiohttp
### Install Dependencies
```bash
pip install playwright aiohttp
playwright install chromium
```
### Clone and Run
```bash
git clone <repository-url>
cd olproxy
python olproxy.py
```
## Usage
### Basic Usage
Start the proxy server:
```bash
python olproxy.py
```
The server will start on `localhost:11434` by default (same as Ollama).
### Command Line Options
```bash
python olproxy.py --help
```
Options:
- `--ip`: Server IP address (default: localhost)
- `--port`: Server port (default: 11434)
- `--connect`: Connect to existing browser via CDP (e.g., ws://localhost:9222)
### API Examples
#### Using Ollama API Format
```bash
# List available models
curl http://localhost:11434/api/tags
# Generate text
curl -X POST http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "grok:latest",
"prompt": "Explain quantum computing"
}'
# Chat completion
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "grok:latest",
"messages": [
{"role": "user", "content": "Hello, how are you?"}
]
}'
```
#### Using OpenAI API Format
```bash
# List models
curl http://localhost:11434/v1/models
# Chat completion
curl -X POST http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "grok:latest",
"messages": [
{"role": "user", "content": "Write a Python function to calculate fibonacci"}
]
}'
```
## Configuration
### Adding New Chatbot Services
Edit the `CHATBOT_CONFIG` dictionary in `olproxy.py`:
```python
CHATBOT_CONFIG = {
"your-model:latest": {
"url": "https://your-chatbot-site.com",
"input_selector": "textarea", # CSS selector for input field
"send_button_selector": "button[type='submit']", # CSS selector for send button
"container_selector": "#chat-container", # CSS selector for chat container
"spy_word_base": "SPYWORD_123" # Base word for response detection
}
}
```
### Browser Configuration
The proxy uses Playwright with a persistent browser context stored in `./playwright_data/`. This allows:
- Session persistence across restarts
- Login state maintenance
- Reduced setup time for subsequent requests
## How It Works
### Response Extraction Strategy
OLProxy uses a sophisticated multi-layered approach to extract AI responses:
1. **Spy Word Detection**: Injects unique markers into prompts to identify response boundaries
2. **Progressive Content Monitoring**: Tracks content changes in real-time
3. **Heuristic Fallback**: Uses DOM analysis to identify likely bot responses
### Request Flow
1. Client sends request to OLProxy API endpoint
2. OLProxy extracts model and prompt information
3. Browser navigates to configured chatbot URL (if not already there)
4. Prompt is injected with spy words and submitted
5. Response is monitored and extracted using multiple detection strategies
6. Clean response is returned in Ollama/OpenAI format
## API Endpoints
### Ollama API Endpoints
- `GET /api/tags` - List available models
- `GET /api/show/{model}` - Show model details
- `POST /api/generate` - Generate text completion
- `POST /api/chat` - Chat completion
- `GET /api/ps` - List loaded models
- `GET /api/version` - Get version information
- `GET /` - Health check
### OpenAI API Endpoints
- `GET /v1/models` - List available models
- `POST /v1/chat/completions` - Chat completion
## Limitations
- **Web Interface Dependency**: Relies on web chatbot interfaces which may change
- **Rate Limiting**: Subject to the rate limits of underlying web services
- **Authentication**: May require manual login to web services
- **Reliability**: Web scraping approach may be affected by UI changes
## Troubleshooting
### Common Issues
1. **Browser fails to start**: Ensure Playwright is properly installed
```bash
playwright install chromium
```
2. **Response extraction fails**: Check if the web interface has changed
- Update CSS selectors in `CHATBOT_CONFIG`
- Check browser console for errors
3. **Authentication required**:
- Run with `--headless=False` to manually log in
- Browser state is persisted in `./playwright_data/`
### Debugging
Enable debug logging:
```python
logging.basicConfig(level=logging.DEBUG)
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Add support for new chatbot services
4. Submit a pull request
## License
This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details.
## Author
**Stefy Lanza** - stefy@nexlab.net
## Disclaimer
This tool is for educational and development purposes. Users are responsible for complying with the terms of service of the underlying chatbot platforms. The authors are not responsible for any misuse or violations of third-party terms of service.
## Support
For issues, questions, or contributions, please open an issue on the project repository.
\ No newline at end of file
#!/usr/bin/env python3
"""
OLProxy - Ollama API Proxy for Web-based AI Chatbots
Copyright (C) 2025 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import json
import asyncio
import datetime # Import datetime module
from playwright.async_api import async_playwright, BrowserContext
from aiohttp import web
import logging
import time
# Configuration dictionary for chatbot sites
CHATBOT_CONFIG = {
"grok:latest": {
"url": "https://x.com/i/grok",
"input_selector": "textarea",
"send_button_selector": "button[aria-label='Grok something']",
"container_selector": "#react-root main",
"spy_word_base": "SPYWORD_123"
},
"grok-beta:latest": {
"url": "https://grok.com",
"input_selector": "textarea",
"send_button_selector": "button[aria-label='Grok something']",
"container_selector": "#react-root main",
"spy_word_base": "SPYWORD_123"
},
"llama2:latest": {
"url": "https://x.com/i/grok",
"input_selector": "textarea",
"send_button_selector": "button[aria-label='Grok something']",
"container_selector": "#react-root main",
"spy_word_base": "SPYWORD_123"
},
"codellama:latest": {
"url": "https://x.com/i/grok",
"input_selector": "textarea",
"send_button_selector": "button[aria-label='Grok something']",
"container_selector": "#react-root main",
"spy_word_base": "SPYWORD_123"
}
}
"""
{"name":"deepseek-coder_large:6.7b","model":"deepseek-coder_large:6.7b","modified_at":"2025-08-21T01:37:49.664985024+02:00","size":3827834614,"digest":"74749dc9f6c142deaf7cb0a56b59ed1ec7458b387ec6661495c4af7ea031a610","details":{"parent_model":"","format":"gguf","family":"llama","families":["llama"],"parameter_size":"6.7B","quantization_level":"Q4_0"}}
"""
async def handle_tags_request(request):
"""Handle GET /api/tags to simulate Ollama's model listing."""
try:
current_time = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+02:00")
response = {
"models": [
{
"name": model,
"modified_at": current_time,
"size": 0, # Placeholder, as size is not relevant for simulation
"digest": "" # Placeholder, as digest is not relevant
}
for model in CHATBOT_CONFIG.keys()
]
}
logging.info(f"Returning tags: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_tags_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_show_request(request):
"""Handle GET /api/show to simulate Ollama's model details."""
try:
# Get the model name from URL parameters
model_name = request.match_info.get('model', '')
if not model_name or model_name not in CHATBOT_CONFIG:
return web.json_response(
{"error": f"Model '{model_name}' not found. Available models: {list(CHATBOT_CONFIG.keys())}"},
status=404
)
current_time = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+02:00")
# Return model details in Ollama format
response = {
"name": model_name,
"model": model_name,
"modified_at": current_time,
"size": 0, # Placeholder, as size is not relevant for simulation
"digest": "", # Placeholder, as digest is not relevant
"details": {
"parent_model": "",
"format": "unknown",
"family": "unknown",
"families": ["unknown"],
"parameter_size": "unknown",
"quantization_level": "unknown"
}
}
logging.info(f"Returning show details for {model_name}: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_show_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_generate_request(request):
"""Handle POST /api/generate for direct text generation."""
try:
request_json = await request.json()
model = request_json.get('model', None)
prompt = request_json.get('prompt', '')
if not model or model not in CHATBOT_CONFIG:
return web.json_response(
{"error": f"Model '{model}' not found. Available models: {list(CHATBOT_CONFIG.keys())}"},
status=400
)
logging.info(f"Generate request for model {model}: {prompt}")
response_text = await forward_to_chatbot(model, CHATBOT_CONFIG[model], prompt)
# Return in Ollama generate format
response = {
"model": model,
"created_at": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"response": response_text,
"done": True,
"context": [],
"total_duration": 1000000000, # 1 second in nanoseconds
"load_duration": 100000000, # 0.1 second
"prompt_eval_count": len(prompt.split()),
"prompt_eval_duration": 200000000, # 0.2 second
"eval_count": len(response_text.split()),
"eval_duration": 700000000 # 0.7 second
}
logging.info(f"Returning generate response: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_generate_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_ps_request(request):
"""Handle GET /api/ps to list currently loaded models."""
try:
current_time = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
response = {
"models": [
{
"name": model,
"model": model,
"size": 0,
"digest": "",
"details": {
"parent_model": "",
"format": "unknown",
"family": "unknown",
"families": ["unknown"],
"parameter_size": "unknown",
"quantization_level": "unknown"
},
"expires_at": current_time,
"size_vram": 0
}
for model in CHATBOT_CONFIG.keys()
]
}
logging.info(f"Returning ps: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_ps_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_version_request(request):
"""Handle GET /api/version to return version information."""
try:
response = {
"version": "0.1.0-proxy"
}
logging.info(f"Returning version: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_version_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_chat_request(request):
"""Handle POST /api/chat for Ollama-native chat format."""
try:
request_json = await request.json()
model = request_json.get('model', None)
messages = request_json.get('messages', [])
if not model or model not in CHATBOT_CONFIG:
return web.json_response(
{"error": f"Model '{model}' not found. Available models: {list(CHATBOT_CONFIG.keys())}"},
status=400
)
# Extract the last user message
prompt = ""
for message in reversed(messages):
if message.get('role') == 'user':
prompt = message.get('content', '')
break
logging.info(f"Chat request for model {model}: {prompt}")
response_text = await forward_to_chatbot(model, CHATBOT_CONFIG[model], prompt)
# Return in Ollama chat format
response = {
"model": model,
"created_at": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"message": {
"role": "assistant",
"content": response_text
},
"done": True,
"total_duration": 1000000000, # 1 second in nanoseconds
"load_duration": 100000000, # 0.1 second
"prompt_eval_count": len(prompt.split()),
"prompt_eval_duration": 200000000, # 0.2 second
"eval_count": len(response_text.split()),
"eval_duration": 700000000 # 0.7 second
}
logging.info(f"Returning chat response: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_chat_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_health_check(request):
"""Handle GET / for health check."""
try:
response = {
"status": "ok",
"message": "Ollama proxy server is running"
}
logging.info("Health check requested")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_health_check: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_models_request(request):
"""Handle GET /v1/models to list available models in OpenAI format."""
try:
current_time = int(datetime.datetime.now().timestamp())
model_data = []
for model in CHATBOT_CONFIG.keys():
model_data.append({
"id": model,
"object": "model",
"created": current_time,
"owned_by": "grok-proxy",
"permission": [
{
"id": f"modelperm-{model.replace(':', '-')}",
"object": "model_permission",
"created": current_time,
"allow_create_engine": False,
"allow_sampling": True,
"allow_logprobs": True,
"allow_search_indices": False,
"allow_view": True,
"allow_fine_tuning": False,
"organization": "*",
"group": None,
"is_blocking": False
}
],
"root": model,
"parent": None
})
response = {
"object": "list",
"data": model_data
}
logging.info(f"Returning detailed models: {response}")
return web.json_response(response)
except Exception as e:
logging.error(f"Error in handle_models_request: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def handle_chat_completion(request):
if request.path != "/v1/chat/completions":
return web.Response(status=404, text="Not Found")
try:
request_json = await request.json()
messages = request_json.get('messages', [])
model = request_json.get('model', None)
if not model or model not in CHATBOT_CONFIG:
return web.json_response(
{"error": f"Model '{model}' not found. Available models: {list(CHATBOT_CONFIG.keys())}"},
status=400
)
logging.info(f"Config for model {model}: {CHATBOT_CONFIG[model]}")
prompt = next((msg['content'] for msg in messages if msg['role'] == 'user'), '')
response_text = await forward_to_chatbot(model, CHATBOT_CONFIG[model], prompt)
return web.json_response({
"id": f"chatcmpl-{id(request)}",
"object": "chat.completion",
"created": int(asyncio.get_event_loop().time()),
"model": model,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": response_text
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": len(prompt.split()),
"completion_tokens": len(response_text.split()),
"total_tokens": len(prompt.split()) + len(response_text.split())
}
})
except Exception as e:
logging.error(f"Error in handle_chat_completion: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def forward_to_chatbot(chatbot_name, config, prompt):
global browser_context, pages
if chatbot_name not in pages:
page = await browser_context.new_page()
await page.goto(config['url'])
pages[chatbot_name] = page
page = pages[chatbot_name]
# Generate unique ID for this request
request_id = int(time.time() * 1000) # Millisecond timestamp
spy_word_start = f"{config.get('spy_word_base', 'SPYWORD_DEFAULT')}_{request_id}"
spy_word_end = f"{spy_word_start}_END"
# Modify prompt to include unique spy word instructions
modified_prompt = f"{prompt}\n\nIMPORTANT: Please start your response with exactly '{spy_word_start}: ' and end with exactly ' {spy_word_end}' (including the colon and spaces)."
logging.info(f"Request ID: {request_id}, Modified prompt: {modified_prompt}")
try:
await page.fill(config['input_selector'], modified_prompt)
await page.click(config['send_button_selector'])
except Exception as e:
logging.error(f"Error interacting with page for {chatbot_name}: {str(e)}")
return f"Error: Failed to input prompt or click send button: {str(e)}"
container_selector = config['container_selector']
try:
await page.wait_for_selector(container_selector, timeout=60000)
except Exception as e:
logging.error(f"Error waiting for container selector {container_selector}: {str(e)}")
return f"Error: Container selector not found: {str(e)}"
# Wait for user's prompt to appear in the chat area
try:
# Use a more flexible approach to detect user prompt
await asyncio.sleep(2) # Give time for the prompt to appear
logging.info(f"User prompt submitted for {chatbot_name}")
except Exception as e:
logging.warning(f"Failed to detect user prompt for {chatbot_name}: {str(e)}")
# Enhanced response detection with multiple strategies
response_text = None
logging.info(f"Searching for spy pattern: {spy_word_start} ... {spy_word_end}")
# Strategy 1: Advanced spy word detection with flexible matching
response_text = await detect_response_with_spy_words(page, container_selector, spy_word_start, spy_word_end, prompt, modified_prompt, request_id, chatbot_name)
# Strategy 2: If spy words fail, try progressive content detection
if not response_text or "Error:" in response_text:
logging.info(f"Spy word detection failed, trying progressive content detection for {chatbot_name} (Request ID: {request_id})")
response_text = await detect_progressive_response(page, container_selector, prompt, modified_prompt, request_id, chatbot_name)
# Strategy 3: Final fallback - latest response detection
if not response_text or "Error:" in response_text:
logging.info(f"Progressive detection failed, using latest response fallback for {chatbot_name} (Request ID: {request_id})")
response_text = await detect_latest_response(page, container_selector, prompt, modified_prompt, request_id, chatbot_name)
logging.info(f"Final response from {chatbot_name} (Request ID: {request_id}): {response_text[:200] if response_text else 'None'}...")
return (response_text or "Error: No response detected").strip()
async def detect_response_with_spy_words(page, container_selector, spy_word_start, spy_word_end, prompt, modified_prompt, request_id, chatbot_name):
"""Enhanced spy word detection with flexible pattern matching."""
try:
return await page.evaluate(
"""([containerSelector, spyWordStart, spyWordEnd, prompt, modifiedPrompt, requestId]) => {
const container = document.querySelector(containerSelector);
if (!container) return "Error: Container not found";
return new Promise((resolve) => {
let resolved = false;
let bestMatch = null;
let partialContent = '';
let hasStartWord = false;
let hasEndWord = false;
const resolveOnce = (result) => {
if (!resolved) {
resolved = true;
resolve(result);
}
};
// Flexible text checking - handles content split across elements
const checkAndExtractResponse = (fullText) => {
if (!fullText || fullText === prompt || fullText === modifiedPrompt) return null;
// Look for spy word patterns with various spacing/formatting
const startPatterns = [
`${spyWordStart}: `,
`${spyWordStart}:`,
`${spyWordStart} :`,
spyWordStart
];
const endPatterns = [
` ${spyWordEnd}`,
`${spyWordEnd}`,
` ${spyWordEnd} `
];
let startIndex = -1;
let endIndex = -1;
let usedStartPattern = '';
let usedEndPattern = '';
// Find start pattern
for (const pattern of startPatterns) {
const idx = fullText.indexOf(pattern);
if (idx !== -1) {
startIndex = idx + pattern.length;
usedStartPattern = pattern;
break;
}
}
// Find end pattern
for (const pattern of endPatterns) {
const idx = fullText.lastIndexOf(pattern);
if (idx !== -1 && idx > startIndex) {
endIndex = idx;
usedEndPattern = pattern;
break;
}
}
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
const extracted = fullText.substring(startIndex, endIndex).trim();
if (extracted.length > 10) {
console.log(`Found complete spy word response: ${extracted.substring(0, 100)}...`);
return extracted;
}
}
// Check for partial matches to track progress
if (fullText.includes(spyWordStart)) {
hasStartWord = true;
// Extract everything after the start word
for (const pattern of startPatterns) {
const idx = fullText.indexOf(pattern);
if (idx !== -1) {
partialContent = fullText.substring(idx + pattern.length);
break;
}
}
}
if (fullText.includes(spyWordEnd)) {
hasEndWord = true;
}
return null;
};
// Combine text from multiple elements intelligently
const getCombinedText = () => {
const chatElements = container.querySelectorAll([
'div[data-testid*="cellInnerDiv"]',
'div[data-testid*="tweetText"]',
'article',
'div[data-testid="primaryColumn"] div',
'main div[role="article"]',
'div[class*="css-"]',
'span[class*="css-"]',
'p', 'div', 'span'
].join(', '));
let combinedTexts = [];
let currentResponseText = '';
for (const element of chatElements) {
const text = element.textContent ? element.textContent.trim() : '';
if (text && text !== prompt && text !== modifiedPrompt && text.length > 5) {
// Check if this might be part of bot response
if (text.includes(spyWordStart) || text.includes(spyWordEnd) ||
(hasStartWord && !text.includes('Grok something'))) {
combinedTexts.push(text);
}
}
}
// Try different combinations
const combinations = [
combinedTexts.join(' '),
combinedTexts.join('\\n'),
combinedTexts.join(''),
...combinedTexts // Individual texts
];
for (const combo of combinations) {
const result = checkAndExtractResponse(combo);
if (result) return result;
}
return null;
};
const observer = new MutationObserver((mutations) => {
const result = getCombinedText();
if (result) {
observer.disconnect();
resolveOnce(result);
return;
}
// Update best partial match
if (hasStartWord && partialContent.length > (bestMatch?.length || 0)) {
bestMatch = partialContent;
}
});
observer.observe(container, {
childList: true,
subtree: true,
characterData: true
});
// Initial check
setTimeout(() => {
const result = getCombinedText();
if (result) {
observer.disconnect();
resolveOnce(result);
}
}, 1000);
// Progressive timeout with partial results
setTimeout(() => {
const result = getCombinedText();
if (result) {
observer.disconnect();
resolveOnce(result);
} else if (bestMatch && bestMatch.length > 50) {
observer.disconnect();
resolveOnce(bestMatch);
}
}, 30000); // 30 seconds
// Final timeout
setTimeout(() => {
observer.disconnect();
if (bestMatch && bestMatch.length > 20) {
resolveOnce(bestMatch);
} else {
resolveOnce("Error: Spy word timeout");
}
}, 90000); // 90 seconds for complex responses
});
}""",
[container_selector, spy_word_start, spy_word_end, prompt, modified_prompt, request_id]
)
except Exception as e:
logging.error(f"Error in spy word detection: {e}")
return "Error: Spy word detection failed"
async def detect_progressive_response(page, container_selector, prompt, modified_prompt, request_id, chatbot_name):
"""Detect responses by monitoring progressive content changes."""
try:
return await page.evaluate(
"""([containerSelector, prompt, modifiedPrompt, requestId]) => {
const container = document.querySelector(containerSelector);
if (!container) return "Error: Container not found";
return new Promise((resolve) => {
let resolved = false;
let lastContent = '';
let stableCount = 0;
let progressiveContent = '';
const resolveOnce = (result) => {
if (!resolved) {
resolved = true;
resolve(result);
}
};
const extractLatestResponse = () => {
// Look for the most recent bot response
const responseElements = container.querySelectorAll([
'div[data-testid*="cellInnerDiv"]',
'article div',
'div[role="article"] div',
'div[class*="css-"] span',
'main div div span'
].join(', '));
let responses = [];
for (const element of responseElements) {
const text = element.textContent ? element.textContent.trim() : '';
if (text && text !== prompt && text !== modifiedPrompt &&
text.length > 20 && !text.includes('Grok something') &&
!text.includes('What can I help') && !text.includes('@')) {
responses.push({
text: text,
element: element,
timestamp: Date.now()
});
}
}
// Sort by DOM order and find the last substantial response
responses.sort((a, b) => {
const position = a.element.compareDocumentPosition(b.element);
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
// Return the longest response that seems complete
for (const response of responses.reverse()) {
if (response.text.length > 100) {
return response.text;
}
}
return responses.length > 0 ? responses[responses.length - 1].text : '';
};
const checkForResponse = () => {
const currentResponse = extractLatestResponse();
if (currentResponse && currentResponse.length > lastContent.length) {
progressiveContent = currentResponse;
lastContent = currentResponse;
stableCount = 0;
} else if (currentResponse === lastContent && currentResponse.length > 0) {
stableCount++;
if (stableCount >= 3 && progressiveContent.length > 50) {
// Content seems stable and substantial
return progressiveContent;
}
}
return null;
};
const observer = new MutationObserver((mutations) => {
const result = checkForResponse();
if (result) {
observer.disconnect();
resolveOnce(result);
}
});
observer.observe(container, {
childList: true,
subtree: true,
characterData: true
});
// Periodic checks
const interval = setInterval(() => {
const result = checkForResponse();
if (result) {
clearInterval(interval);
observer.disconnect();
resolveOnce(result);
}
}, 2000);
// Timeout
setTimeout(() => {
clearInterval(interval);
observer.disconnect();
if (progressiveContent.length > 20) {
resolveOnce(progressiveContent);
} else {
resolveOnce("Error: Progressive detection timeout");
}
}, 60000);
});
}""",
[container_selector, prompt, modified_prompt, request_id]
)
except Exception as e:
logging.error(f"Error in progressive detection: {e}")
return "Error: Progressive detection failed"
async def detect_latest_response(page, container_selector, prompt, modified_prompt, request_id, chatbot_name):
"""Final fallback - detect the latest bot response by heuristics."""
try:
return await page.evaluate(
"""([containerSelector, prompt, modifiedPrompt]) => {
const container = document.querySelector(containerSelector);
if (!container) return "Error: Container not found";
// Get all text elements and find the most likely bot response
const allElements = container.querySelectorAll('*');
let candidateResponses = [];
for (const element of allElements) {
const text = element.textContent ? element.textContent.trim() : '';
if (text && text !== prompt && text !== modifiedPrompt && text.length > 50) {
// Skip user interface elements
if (!text.includes('Grok something') &&
!text.includes('What can I help') &&
!text.includes('@') &&
!text.includes('Tweet') &&
!text.includes('Follow') &&
!element.querySelector('input') &&
!element.querySelector('button')) {
candidateResponses.push({
text: text,
length: text.length,
element: element
});
}
}
}
if (candidateResponses.length === 0) {
return "Error: No candidate responses found";
}
// Sort by length and DOM position to get the most substantial recent response
candidateResponses.sort((a, b) => {
// First prefer longer responses
if (Math.abs(a.length - b.length) > 100) {
return b.length - a.length;
}
// Then prefer later DOM elements
const position = a.element.compareDocumentPosition(b.element);
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
return candidateResponses[0].text;
}""",
[container_selector, prompt, modified_prompt]
)
except Exception as e:
logging.error(f"Error in latest response detection: {e}")
return "Error: Latest response detection failed"
async def start_browser(args):
global browser_context, pages
async with async_playwright() as p:
if args.connect:
browser = await p.chromium.connect_over_cdp(args.connect)
else:
browser = await p.chromium.launch_persistent_context(
user_data_dir="./playwright_data",
headless=False
)
browser_context = browser
pages = {}
await browser_context.wait_for_event('close', timeout=0)
@web.middleware
async def cors_middleware(request, handler):
"""Add CORS headers to all responses."""
try:
response = await handler(request)
except Exception as ex:
response = web.json_response({'error': str(ex)}, status=500)
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
response.headers['Access-Control-Max-Age'] = '86400'
return response
async def handle_options(request):
"""Handle CORS preflight requests."""
return web.Response(
status=200,
headers={
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
'Access-Control-Max-Age': '86400'
}
)
async def start_proxy_server(ip, port, args):
app = web.Application(middlewares=[cors_middleware])
# OPTIONS handlers for preflight
app.router.add_options('/{path:.*}', handle_options)
# Ollama API endpoints
app.router.add_get('/api/tags', handle_tags_request)
app.router.add_get('/api/show/{model}', handle_show_request)
app.router.add_post('/api/generate', handle_generate_request)
app.router.add_post('/api/chat', handle_chat_request)
app.router.add_get('/api/ps', handle_ps_request)
app.router.add_get('/api/version', handle_version_request)
app.router.add_get('/', handle_health_check)
# OpenAI API endpoints
app.router.add_get('/v1/models', handle_models_request)
app.router.add_post('/v1/chat/completions', handle_chat_completion)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, ip, port)
print(f"Starting proxy server on {ip}:{port}")
await site.start()
await asyncio.Event().wait()
async def main(args):
parser = argparse.ArgumentParser(description="Ollama API emulator with Playwright")
parser.add_argument('--ip', default='localhost', help='Proxy server IP (default: localhost)')
parser.add_argument('--port', type=int, default=11434, help='Proxy server port (default: 11434)')
parser.add_argument('--connect', help='Connect to existing browser via CDP (e.g., ws://localhost:9222)')
args = parser.parse_args()
logging.basicConfig(level=logging.INFO)
logging.info(f"CHATBOT_CONFIG: {CHATBOT_CONFIG}")
await asyncio.gather(
start_proxy_server(args.ip, args.port, args),
start_browser(args)
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Ollama API emulator with Playwright")
parser.add_argument('--ip', default='localhost', help='Proxy server IP (default: localhost)')
parser.add_argument('--port', type=int, default=11434, help='Proxy server port (default: 11434)')
parser.add_argument('--connect', help='Connect to existing browser via CDP (e.g., ws://localhost:9222)')
args = parser.parse_args()
if args.port != 11434:
print(f"Running proxy server on a different port: {args.port}")
asyncio.run(main(args))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment