removing redunant demos

This commit is contained in:
Salman Paracha 2025-12-21 22:28:31 -08:00
parent 4b4ba50e5f
commit 6aac41a729
24 changed files with 0 additions and 20123 deletions

View file

@ -1,19 +0,0 @@
FROM python:3.12 AS base
FROM base AS builder
WORKDIR /src
COPY requirements.txt /src/
RUN pip install --prefix=/runtime --force-reinstall -r requirements.txt
COPY ../. /src
FROM python:3.12-slim AS output
COPY --from=builder /runtime /usr/local
COPY ../. /app
WORKDIR /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80", "--log-level", "info"]

View file

@ -1,42 +0,0 @@
# Network Agent Demo
This demo illustrates how **Arch** can be used to perform function calling with network-related tasks. In this demo, you act as a **network assistant** that provides factual information, without offering advice on manufacturers or purchasing decisions.
The assistant can perform several key operations, including rebooting devices, answering general networking questions, and retrieving device statistics. By default, the system prompt ensures that the assistant's responses are factual and neutral.
## Available Functions:
- **Reboot Devices**: Allows rebooting specific devices or device groups, with an optional time range for scheduling the reboot.
- Parameters:
- `device_ids` (required): A list of device IDs to reboot.
- `time_range` (optional): Specifies the time range in days, defaulting to 7 days if not provided.
- **Network Q/A**: Handles general Q&A related to networking. This function is the default target for general networking queries.
- **Device Summary**: Retrieves statistics for specific devices within a given time range.
- Parameters:
- `device_ids` (required): A list of device IDs for which statistics will be retrieved.
- `time_range` (optional): Specifies the time range in days for gathering statistics, with a default of 7 days.
# Starting the demo
1. Please make sure the [pre-requisites](https://github.com/katanemo/arch/?tab=readme-ov-file#prerequisites) are installed correctly
2. Start Arch
```sh
sh run_demo.sh
```
3. Navigate to http://localhost:18080/agent/chat
4. Tell me what can you do for me?"
# Observability
Arch gateway publishes stats endpoint at http://localhost:19901/stats. In this demo we are using prometheus to pull stats from arch and we are using grafana to visualize the stats in dashboard. To see grafana dashboard follow instructions below,
1. Start grafana and prometheus using following command
```yaml
docker compose --profile monitoring up
```
1. Navigate to http://localhost:3000/ to open grafana UI (use admin/grafana as credentials)
1. From grafana left nav click on dashboards and select "Intelligent Gateway Overview" to view arch gateway stats
Here is sample interaction
![alt text](image.png)

View file

@ -1,61 +0,0 @@
version: v0.1.0
listeners:
ingress_traffic:
address: 0.0.0.0
port: 10000
message_format: openai
timeout: 30s
# Centralized way to manage LLMs, manage keys, retry logic, failover and limits in a central way
llm_providers:
- access_key: $OPENAI_API_KEY
model: openai/gpt-4o
default: true
# default system prompt used by all prompt targets
system_prompt: |
You are a network assistant that helps operators with a better understanding of network traffic flow and perform actions on networking operations. No advice on manufacturers or purchasing decisions.
prompt_targets:
- name: device_summary
description: Retrieve network statistics for specific devices within a time range
endpoint:
name: app_server
path: /agent/device_summary
http_method: POST
parameters:
- name: device_id
type: str
description: A device identifier to retrieve statistics for.
required: true # device_ids are required to get device statistics
- name: days
type: int
description: The number of days for which to gather device statistics.
default: 7
- name: reboot_device
description: Reboot a device
endpoint:
name: app_server
path: /agent/device_reboot
http_method: POST
parameters:
- name: device_id
type: str
description: the device identifier
required: true
system_prompt: You will get a status JSON object. Simply summarize it
# Arch creates a round-robin load balancing between different endpoints, managed via the cluster subsystem.
endpoints:
app_server:
# value could be ip address or a hostname with port
# this could also be a list of endpoints for load balancing
# for example endpoint: [ ip1:port, ip2:port ]
endpoint: host.docker.internal:18083
# max time to wait for a connection to be established
connect_timeout: 0.005s
tracing:
random_sampling: 100
trace_arch_internal: true

View file

@ -1,28 +0,0 @@
services:
api_server:
build:
context: .
dockerfile: Dockerfile
ports:
- "18083:80"
chatbot_ui:
build:
context: ../../shared/chatbot_ui
dockerfile: Dockerfile
ports:
- "18080:8080"
environment:
- CHAT_COMPLETION_ENDPOINT=http://host.docker.internal:10000/v1
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./arch_config.yaml:/app/arch_config.yaml
jaeger:
build:
context: ../../shared/jaeger
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

View file

@ -1,92 +0,0 @@
import os
from typing import List, Optional
from fastapi import FastAPI, HTTPException
from openai import OpenAI
from pydantic import BaseModel, Field
app = FastAPI()
DEMO_DESCRIPTION = """This demo illustrates how **Arch** can be used to perform function calling
with network-related tasks. In this demo, you act as a **network assistant** that provides factual
information, without offering advice on manufacturers or purchasing decisions."""
# Define the request model
class DeviceSummaryRequest(BaseModel):
device_id: str
time_range: Optional[int] = Field(
default=7, description="Time range in days, defaults to 7"
)
# Define the response model
class DeviceStatistics(BaseModel):
device_id: str
time_range: str
data: str
class DeviceSummaryResponse(BaseModel):
statistics: List[DeviceStatistics]
# Request model for device reboot
class DeviceRebootRequest(BaseModel):
device_id: str
# Response model for the device reboot
class CoverageResponse(BaseModel):
status: str
summary: dict
@app.post("/agent/device_reboot", response_model=CoverageResponse)
def reboot_network_device(request_data: DeviceRebootRequest):
"""
Endpoint to reboot network devices based on device IDs and an optional time range.
"""
# Access data from the Pydantic model
device_id = request_data.device_id
# Validate 'device_id'
# (This is already validated by Pydantic, but additional logic can be added if needed)
if not device_id:
raise HTTPException(status_code=400, detail="'device_id' parameter is required")
# Simulate reboot operation and return the response
statistics = []
# Placeholder for actual data retrieval or device reboot logic
stats = {"data": f"Device {device_id} has been successfully rebooted."}
statistics.append(stats)
# Return the response with a summary
return CoverageResponse(status="success", summary={"device_id": device_id})
# Post method for device summary
@app.post("/agent/device_summary", response_model=DeviceSummaryResponse)
def get_device_summary(request: DeviceSummaryRequest):
"""
Endpoint to retrieve device statistics based on device IDs and an optional time range.
"""
# Extract 'device_id' and 'time_range' from the request
device_id = request.device_id
time_range = request.time_range
# Simulate retrieving statistics for the given device IDs and time range
statistics = []
minutes = 4
stats = {
"device_id": device_id,
"time_range": f"Last {time_range} days",
"data": f"""Device {device_id} over the last {time_range} days experienced {minutes}
minutes of downtime.""",
}
statistics.append(DeviceStatistics(**stats))
return DeviceSummaryResponse(statistics=statistics)

View file

@ -1,14 +0,0 @@
fastapi
uvicorn
pydantic
typing
pandas
gradio==5.3.0
huggingface_hub<1.0.0
async_timeout==4.0.3
loguru==0.7.2
asyncio==3.4.3
httpx==0.27.0
python-dotenv==1.0.1
pydantic==2.8.2
openai==1.51.0

View file

@ -1,47 +0,0 @@
#!/bin/bash
set -e
# Function to start the demo
start_demo() {
# Step 1: Check if .env file exists
if [ -f ".env" ]; then
echo ".env file already exists. Skipping creation."
else
# Step 2: Create `.env` file and set OpenAI key
if [ -z "$OPENAI_API_KEY" ]; then
echo "Error: OPENAI_API_KEY environment variable is not set for the demo."
exit 1
fi
echo "Creating .env file..."
echo "OPENAI_API_KEY=$OPENAI_API_KEY" > .env
echo ".env file created with OPENAI_API_KEY."
fi
# Step 3: Start Arch
echo "Starting Arch with arch_config.yaml..."
archgw up arch_config.yaml
# Step 4: Start developer services
echo "Starting Network Agent using Docker Compose..."
docker compose up -d # Run in detached mode
}
# Function to stop the demo
stop_demo() {
# Step 1: Stop Docker Compose services
echo "Stopping Network Agent using Docker Compose..."
docker compose down
# Step 2: Stop Arch
echo "Stopping Arch..."
archgw down
}
# Main script logic
if [ "$1" == "down" ]; then
stop_demo
else
# Default action is to bring the demo up
start_demo
fi

View file

@ -1,116 +0,0 @@
# 🗝️ RouteGPT (Beta)
**RouteGPT** is a dynamic model selector Chrome extension for ChatGPT. It intercepts your prompts, detects the user's intent, and automatically routes requests to the most appropriate model — based on preferences you define. Powered by the lightweight [Arch-Router](https://huggingface.co/katanemo/Arch-Router-1.5B.gguf), it makes multi-model usage seamless.
Think of it this way: changing models manually is like shifting gears on your bike every few pedals. RouteGPT automates that for you — so you can focus on the ride, not the mechanics.
---
## 📁 Project Name
Folder: `chatgpt-preference-model-selector`
---
## 🚀 Features
* 🧠 Preference-based routing (e.g., "code generation" → GPT-4, "travel help" → Gemini)
* 🤖 Local inference using [Ollama](https://ollama.com)
* 📙 Chrome extension interface for setting route preferences
* ⚡ Runs with [Arch-Router-1.5B.gguf](https://huggingface.co/katanemo/Arch-Router-1.5B.gguf)
---
## 📦 Installation
### 1. Clone and install dependencies
```
git clone https://github.com/katanemo/archgw/
cd demos/use_cases/chatgpt-preference-model-selector
```
### 2. Build the extension
```
npm install
npm run build
```
This will create a `build/` directory that contains the unpacked Chrome extension.
---
## 🧠 Set Up Arch-Router in Ollama
Ensure [Ollama](https://ollama.com/download) is installed and running.
Then pull the Arch-Router model:
```
ollama pull hf.co/katanemo/Arch-Router-1.5B.gguf:Q4_K_M
```
### 🌐 Allow Chrome to Access Ollama
Start Ollama with appropriate network settings:
```
OLLAMA_ORIGINS=* ollama serve
```
This:
* Sets CORS to allow requests from Chrome
---
## 📩 Load the Extension into Chrome
1. Open `chrome://extensions`
2. Enable **Developer mode** (top-right toggle)
3. Click **"Load unpacked"**
4. Select the `build` folder inside `chatgpt-preference-model-selector`
Once loaded, RouteGPT will begin intercepting and routing your ChatGPT messages based on the preferences you define.
---
## ⚙️ Configure Routing Preferences
1. In ChatGPT, click the model dropdown.
2. A RouteGPT modal will appear.
3. Define your routing logic using natural language (e.g., `brainstorm startup ideas → gpt-4`, `summarize news articles → claude`).
4. Save your preferences. Routing begins immediately.
---
## 💸 Profit
RouteGPT helps you:
* Use expensive models only when needed
* Automatically shift to cheaper, faster, or more capable models based on task type
* Streamline multi-model workflows without extra clicks
---
## 🧪 Troubleshooting
* Make sure Ollama is reachable at `http://localhost:11434`
* If routing doesnt seem to trigger, check DevTools console logs for `[ModelSelector]`
* Reload the extension and refresh the ChatGPT tab after updating preferences
---
## 🧱 Built With
* 🧠 [Arch-Router (1.5B)](https://huggingface.co/katanemo/Arch-Router-1.5B.gguf)
* 📙 Chrome Extensions API
* 🛠️ Ollama
* ⚛️ React + TypeScript
---
## 📜 License
Apache 2.0 © Katanemo Labs, Inc.

File diff suppressed because it is too large Load diff

View file

@ -1,33 +0,0 @@
{
"name": "preference-selector-extension",
"version": "0.1.0",
"private": true,
"homepage": ".",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"scripts": {
"start": "react-scripts start",
"build": "node src/build.js",
"test": "react-scripts test"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"react-scripts": "5.0.1",
"tailwindcss": "^3.4.4"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<!-- ✅ External JS to configure Tailwind and set dark mode -->
<script src="%PUBLIC_URL%/init-theme.js"></script>
<title>RouteGPT</title>
</head>
<body class="bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-100">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -1,9 +0,0 @@
// Apply dark mode based on system preference
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}

View file

@ -1,36 +0,0 @@
{
"manifest_version": 3,
"name": "RouteGPT",
"version": "0.1.2",
"description": "RouteGPT: Smart Model Routing for ChatGPT.",
"permissions": [
"storage"
],
"host_permissions": [
"https://chatgpt.com/*",
"http://localhost:12000/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' http://localhost:12000"
},
"web_accessible_resources": [
{
"resources": ["index.html", "logo.png"],
"matches": ["https://chatgpt.com/*"]
},
{
"resources": ["pageFetchOverride.js"],
"matches": ["https://chatgpt.com/*"]
}
],
"action": {
"default_popup": "index.html"
},
"content_scripts": [
{
"matches": ["https://chatgpt.com/*"],
"js": ["static/js/content.js"],
"run_at": "document_start"
}
]
}

View file

@ -1,28 +0,0 @@
import React from 'react';
import PreferenceBasedModelSelector from './components/PreferenceBasedModelSelector';
export default function App() {
return (
<div className="bg-gray-100 dark:bg-gray-900 min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-6xl">
<div className="text-center mb-8">
<div className="flex justify-center items-center gap-3 -ml-12">
<img src="/logo.png" alt="RouteGPT Logo" className="w-10 h-10" />
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">RouteGPT</h1>
</div>
<p className="text-gray-600 dark:text-gray-300 mt-2">
Dynamically route to GPT models based on usage preferences.
</p>
<a
target="_blank"
href="https://github.com/katanemo/archgw"
className="text-blue-500 dark:text-blue-400 hover:underline"
>
powered by Arch Router
</a>
</div>
<PreferenceBasedModelSelector />
</div>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,64 +0,0 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
console.log('Starting the custom build process for the Chrome Extension...');
const reactAppDir = path.join(__dirname, '..');
const contentScriptSource = path.join(reactAppDir, 'src', 'scripts', 'content.js');
const pageOverrideSource = path.join(reactAppDir, 'src', 'scripts', 'pageFetchOverride.js');
const buildDir = path.join(reactAppDir, 'build');
const contentScriptDest = path.join(buildDir, 'static', 'js');
// 1⃣ Run React build
try {
console.log('Running react-scripts build...');
execSync('react-scripts build', { stdio: 'inherit' });
console.log('React build completed successfully.');
} catch (err) {
console.error('React build failed:', err);
process.exit(1);
}
// 2⃣ Copy content.js
try {
if (!fs.existsSync(contentScriptDest)) {
throw new Error(`Missing directory: ${contentScriptDest}`);
}
fs.copyFileSync(contentScriptSource, path.join(contentScriptDest, 'content.js'));
console.log(`Copied content.js → ${contentScriptDest}`);
} catch (err) {
console.error('Failed to copy content.js:', err);
process.exit(1);
}
// 3⃣ Copy pageFetchOverride.js
try {
if (!fs.existsSync(buildDir)) {
throw new Error(`Missing build directory: ${buildDir}`);
}
fs.copyFileSync(pageOverrideSource, path.join(buildDir, 'pageFetchOverride.js'));
console.log(`Copied pageFetchOverride.js → ${buildDir}`);
} catch (err) {
console.error('Failed to copy pageFetchOverride.js:', err);
process.exit(1);
}
// 4⃣ Copy logo.png from src/assets to build root
try {
const logoSource = path.join(reactAppDir, 'src', 'assets', 'logo.png');
const logoDest = path.join(buildDir, 'logo.png');
if (!fs.existsSync(logoSource)) {
throw new Error(`Missing logo.png at ${logoSource}`);
}
fs.copyFileSync(logoSource, logoDest);
console.log(`Copied logo.png → ${logoDest}`);
} catch (err) {
console.error('Failed to copy logo.png:', err);
process.exit(1);
}
console.log('Extension build process finished successfully!');

View file

@ -1,327 +0,0 @@
/*global chrome*/
import React, { useState, useEffect } from 'react';
// --- Hardcoded list of ChatGPT models ---
const MODEL_LIST = [
'gpt-4o',
'gpt-4.1',
'gpt-4.1-mini',
'gpt-4.5-preview',
'o3',
'o4-mini',
'o4-mini-high'
];
// --- Mocked lucide-react icons as SVG components ---
const Trash2 = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M3 6h18" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
</svg>
);
const PlusCircle = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="8" y1="12" x2="16" y2="12" />
</svg>
);
// --- Mocked UI Components ---
const Card = ({ children, className = '' }) => (
<div className={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm ${className}`}>
{children}
</div>
);
const CardContent = ({ children, className = '' }) => (
<div className={`p-4 text-gray-800 dark:text-gray-100 ${className}`}>
{children}
</div>
);
const Input = (props) => (
<input
{...props}
className={`w-full h-9 px-3 text-sm
text-gray-800 dark:text-white
bg-white dark:bg-gray-700
border border-gray-300 dark:border-gray-600
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
${props.className || ''}`}
/>
);
const Button = ({ children, variant = 'default', size = 'default', className = '', ...props }) => {
const baseClasses = `
inline-flex items-center justify-center
rounded-md text-sm font-medium
transition-colors
focus:outline-none focus:ring-2 focus:ring-offset-2
`;
const variantClasses = {
default: `
bg-gray-900 text-white
hover:bg-gray-800
focus:ring-gray-900
`,
outline: `
border border-gray-300 dark:border-gray-600
bg-transparent
text-gray-800 dark:text-white
hover:bg-gray-100 dark:hover:bg-gray-700
focus:ring-blue-500
focus:ring-offset-2
dark:focus:ring-offset-gray-900
`,
ghost: `
text-gray-800 dark:text-gray-200
hover:bg-gray-100 dark:hover:bg-gray-700
focus:ring-gray-400
`
};
const sizeClasses = {
default: 'h-9 px-3',
icon: 'h-9 w-9'
};
return (
<button
{...props}
className={`
${baseClasses}
${variantClasses[variant]}
${sizeClasses[size]}
${className}
`}
>
{children}
</button>
);
};
const Switch = ({ checked, onCheckedChange, id }) => (
<div className="flex items-center gap-2">
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onCheckedChange(!checked)}
id={id}
className={`
relative inline-flex items-center justify-start
h-6 w-11 rounded-full
transition-colors duration-200 ease-in-out
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
border-2 border-transparent
overflow-hidden
${checked ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}
`}
>
<span
aria-hidden="true"
className={`
pointer-events-none
inline-block h-5 w-5 transform rounded-full bg-white
shadow-md ring-0 transition-transform duration-200 ease-in-out
${checked ? 'translate-x-[20px]' : 'translate-x-0'}
`}
/>
</button>
<span className="inline-block w-8 text-sm text-gray-700 dark:text-gray-300 text-center select-none">
{checked ? 'On' : 'Off'}
</span>
</div>
);
const Label = (props) => (
<label {...props} className={`text-sm font-medium leading-none text-gray-700 ${props.className || ''}`} />
);
export default function PreferenceBasedModelSelector() {
const [routingEnabled, setRoutingEnabled] = useState(false);
const [preferences, setPreferences] = useState([
{ id: 1, usage: 'generate code snippets', model: 'gpt-4o' }
]);
const [defaultModel, setDefaultModel] = useState('gpt-4o');
const [modelOptions] = useState(MODEL_LIST); // static list, no dynamic fetch
// Load saved settings
useEffect(() => {
chrome.storage.sync.get(['routingEnabled', 'preferences', 'defaultModel'], (result) => {
if (result.routingEnabled !== undefined) setRoutingEnabled(result.routingEnabled);
if (result.preferences) {
// add ids if they were missing
const withIds = result.preferences.map((p, i) => ({
id: p.id ?? i + 1,
...p,
}));
setPreferences(withIds);
}
if (result.defaultModel) setDefaultModel(result.defaultModel);
});
}, []);
const updatePreference = (id, key, value) => {
setPreferences((prev) => prev.map((p) => (p.id === id ? { ...p, [key]: value } : p)));
};
const addPreference = () => {
const newId = preferences.reduce((max, p) => Math.max(max, p.id ?? 0), 0) + 1;
setPreferences((prev) => [
...prev,
{ id: newId, usage: '', model: defaultModel }
]);
};
const removePreference = (id) => {
if (preferences.length > 1) {
setPreferences((prev) => prev.filter((p) => p.id !== id));
}
};
// Save settings: generate name slug and store tuples
const handleSave = () => {
const slugCounts = {};
const tuples = [];
preferences
.filter(p => p.usage?.trim())
.forEach(p => {
const baseSlug = p.usage
.split(/\s+/)
.slice(0, 3)
.join('-')
.toLowerCase()
.replace(/[^\w-]/g, '');
const count = slugCounts[baseSlug] || 0;
slugCounts[baseSlug] = count + 1;
const dedupedSlug = count === 0 ? baseSlug : `${baseSlug}-${count}`;
tuples.push({
name: dedupedSlug,
usage: p.usage.trim(),
model: p.model?.trim?.() || ''
});
});
chrome.storage.sync.set({ routingEnabled, preferences: tuples, defaultModel }, () => {
if (chrome.runtime.lastError) {
console.error('[PBMS] Storage error:', chrome.runtime.lastError);
} else {
console.log('[PBMS] Saved tuples:', tuples);
}
});
// Send message to background script to apply the default model
window.parent.postMessage({ action: 'applyModelSelection', model: defaultModel }, "*");
// Close the modal after saving
window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*');
};
const handleCancel = () => {
window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*');
};
return (
<div className="w-full max-w-[600px] h-[65vh] flex flex-col bg-gray-50 dark:bg-gray-800 p-4 mx-auto">
{/* Scrollable preferences only */}
<div className="space-y-4 overflow-y-auto flex-1 pr-1 max-h-[60vh]">
<Card className="w-full">
<CardContent>
<div className="flex items-center justify-between">
<Label>Enable preference-based routing</Label>
<Switch checked={routingEnabled} onCheckedChange={setRoutingEnabled} />
</div>
{routingEnabled && (
<div className="pt-4 mt-4 space-y-3 border-t border-gray-200 dark:border-gray-700">
{preferences.map((pref) => (
<div key={pref.id} className="grid grid-cols-[3fr_1.5fr_auto] gap-4 items-center">
<Input
placeholder="(e.g. generating fictional stories or poems)"
value={pref.usage}
onChange={(e) => updatePreference(pref.id, 'usage', e.target.value)}
/>
<select
value={pref.model}
onChange={(e) => updatePreference(pref.id, 'model', e.target.value)}
className="h-9 w-full px-3 text-sm
bg-white dark:bg-gray-700
text-gray-800 dark:text-white
border border-gray-300 dark:border-gray-600
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option disabled value="">
Select Model
</option>
{modelOptions.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
<Button
variant="ghost"
size="icon"
onClick={() => removePreference(pref.id)}
disabled={preferences.length <= 1}
>
<Trash2 className="h-4 w-4 text-gray-500 hover:text-red-600" />
</Button>
</div>
))}
<Button
variant="outline"
onClick={addPreference}
className="flex items-center gap-2 text-sm mt-2"
>
<PlusCircle className="h-4 w-4" /> Add Preference
</Button>
</div>
)}
</CardContent>
</Card>
</div>
{/* Default model selector (static) */}
<Card className="w-full mt-4">
<CardContent>
<Label>Default Model</Label>
<select
value={defaultModel}
onChange={(e) => setDefaultModel(e.target.value)}
className="h-9 w-full mt-2 px-3 text-sm
bg-white dark:bg-gray-700
text-gray-800 dark:text-white
border border-gray-300 dark:border-gray-600
rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{modelOptions.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</CardContent>
</Card>
{/* Save/Cancel footer (static) */}
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 mt-4">
<Button variant="ghost" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleSave}>Save and Apply</Button>
</div>
</div>
);
}

View file

@ -1,12 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -1,11 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View file

@ -1,407 +0,0 @@
(() => {
const TAG = '[ModelSelector]';
// Content script to intercept fetch requests and modify them based on user preferences
async function streamToPort(response, port) {
const reader = response.body?.getReader();
if (!reader) {
port.postMessage({ done: true });
return;
}
while (true) {
const { done, value } = await reader.read();
if (done) {
port.postMessage({ done: true });
break;
}
port.postMessage({ chunk: value.buffer }, [value.buffer]);
}
}
// Extract messages from the DOM, falling back to requestMessages if DOM is empty
function getMessagesFromDom(requestMessages = null) {
const bubbles = [...document.querySelectorAll('[data-message-author-role]')];
const domMessages = bubbles
.map(b => {
const role = b.getAttribute('data-message-author-role');
const content =
role === 'assistant'
? (b.querySelector('.markdown')?.innerText ?? b.innerText ?? '').trim()
: (b.innerText ?? '').trim();
return content ? { role, content } : null;
})
.filter(Boolean);
// Fallback: If DOM is empty but we have requestMessages, use those
if (domMessages.length === 0 && requestMessages?.length > 0) {
return requestMessages
.map(msg => {
const role = msg.author?.role;
const parts = msg.content?.parts ?? [];
const textPart = parts.find(p => typeof p === 'string');
return role && textPart ? { role, content: textPart.trim() } : null;
})
.filter(Boolean);
}
return domMessages;
}
// Insert a route label for the last user message in the chat
function insertRouteLabelForLastUserMessage(routeName) {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
// Find the most recent user bubble
const bubbles = [...document.querySelectorAll('[data-message-author-role="user"]')];
const lastBubble = bubbles[bubbles.length - 1];
if (!lastBubble) return;
// Skip if weve already added a label
if (lastBubble.querySelector('.arch-route-label')) {
console.log('[RouteLabel] Label already exists, skipping');
return;
}
// Default label text
let labelText = 'RouteGPT: preference = default';
// Try to override with preference-based usage if we have a routeName
if (routeName && Array.isArray(preferences)) {
const match = preferences.find(p => p.name === routeName);
if (match && match.usage) {
labelText = `RouteGPT: preference = ${match.usage}`;
} else {
console.log('[RouteLabel] No usage found for route (falling back to default):', routeName);
}
}
// Build and attach the label
const label = document.createElement('span');
label.textContent = labelText;
label.className = 'arch-route-label';
label.style.fontWeight = '350';
label.style.fontSize = '0.85rem';
label.style.marginTop = '2px';
label.style.fontStyle = 'italic';
label.style.alignSelf = 'end';
label.style.marginRight = '5px';
lastBubble.appendChild(label);
console.log('[RouteLabel] Inserted label:', labelText);
});
}
// Prepare the system prompt for the proxy request
function prepareProxyRequest(messages, routes, maxTokenLength = 2048) {
const SYSTEM_PROMPT_TEMPLATE = `
You are a helpful assistant designed to find the best suited route.
You are provided with route description within <routes></routes> XML tags:
<routes>
{routes}
</routes>
<conversation>
{conversation}
</conversation>
Your task is to decide which route is best suit with user intent on the conversation in <conversation></conversation> XML tags. Follow the instruction:
1. If the latest intent from user is irrelevant or user intent is full filled, response with other route {"route": "other"}.
2. You must analyze the route descriptions and find the best match route for user latest intent.
3. You only response the name of the route that best matches the user's request, use the exact name in the <routes></routes>.
Based on your analysis, provide your response in the following JSON formats if you decide to match any route:
{"route": "route_name"}
`;
const TOKEN_DIVISOR = 4;
const filteredMessages = messages.filter(
m => m.role !== 'system' && m.role !== 'tool' && m.content?.trim()
);
let tokenCount = SYSTEM_PROMPT_TEMPLATE.length / TOKEN_DIVISOR;
const selected = [];
for (let i = filteredMessages.length - 1; i >= 0; i--) {
const msg = filteredMessages[i];
tokenCount += msg.content.length / TOKEN_DIVISOR;
if (tokenCount > maxTokenLength) {
if (msg.role === 'user') selected.push(msg);
break;
}
selected.push(msg);
}
if (selected.length === 0 && filteredMessages.length > 0) {
selected.push(filteredMessages[filteredMessages.length - 1]);
}
const selectedOrdered = selected.reverse();
const systemPrompt = SYSTEM_PROMPT_TEMPLATE
.replace('{routes}', JSON.stringify(routes, null, 2))
.replace('{conversation}', JSON.stringify(selectedOrdered, null, 2));
return systemPrompt;
}
function getRoutesFromStorage() {
return new Promise(resolve => {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
if (!preferences || !Array.isArray(preferences)) {
console.warn('[ModelSelector] No preferences found in storage');
return resolve([]);
}
const routes = preferences.map(p => ({
name: p.name,
description: p.usage
}));
resolve(routes);
});
});
}
function getModelIdForRoute(routeName) {
return new Promise(resolve => {
chrome.storage.sync.get(['preferences'], ({ preferences }) => {
const match = (preferences || []).find(p => p.name === routeName);
if (match) resolve(match.model);
else resolve(null);
});
});
}
(function injectPageFetchOverride() {
const injectorTag = '[ModelSelector][Injector]';
const s = document.createElement('script');
s.src = chrome.runtime.getURL('pageFetchOverride.js');
s.onload = () => {
console.log(`${injectorTag} loaded pageFetchOverride.js`);
s.remove();
};
(document.head || document.documentElement).appendChild(s);
})();
window.addEventListener('message', ev => {
if (ev.source !== window || ev.data?.type !== 'ARCHGW_FETCH') return;
const { url, init } = ev.data;
const port = ev.ports[0];
(async () => {
try {
console.log(`${TAG} Intercepted fetch from page:`, url);
let originalBody = {};
try {
originalBody = JSON.parse(init.body);
} catch {
console.warn(`${TAG} Could not parse original fetch body`);
}
const { routingEnabled, preferences, defaultModel } = await new Promise(resolve => {
chrome.storage.sync.get(['routingEnabled', 'preferences', 'defaultModel'], resolve);
});
if (!routingEnabled) {
console.log(`${TAG} Routing disabled — applying default model if present`);
const modifiedBody = { ...originalBody };
if (defaultModel) {
modifiedBody.model = defaultModel;
console.log(`${TAG} Routing disabled — overriding with default model: ${defaultModel}`);
} else {
console.log(`${TAG} Routing disabled — no default model found`);
}
await streamToPort(await fetch(url, {
method: init.method,
headers: init.headers,
credentials: init.credentials,
body: JSON.stringify(modifiedBody)
}), port);
return;
}
const scrapedMessages = getMessagesFromDom(originalBody.messages);
const routes = (preferences || []).map(p => ({
name: p.name,
description: p.usage
}));
const prompt = prepareProxyRequest(scrapedMessages, routes);
let selectedRoute = null;
try {
const res = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'hf.co/katanemo/Arch-Router-1.5B.gguf:Q4_K_M',
prompt: prompt,
temperature: 0.01,
top_p: 0.95,
top_k: 20,
stream: false
})
});
if (res.ok) {
const data = await res.json();
console.log(`${TAG} Ollama router response:`, data.response);
try {
let parsed = data.response;
if (typeof data.response === 'string') {
try {
parsed = JSON.parse(data.response);
} catch (jsonErr) {
const safe = data.response.replace(/'/g, '"');
parsed = JSON.parse(safe);
}
}
selectedRoute = parsed.route || null;
if (!selectedRoute) console.warn(`${TAG} Route missing in parsed response`);
} catch (e) {
console.warn(`${TAG} Failed to parse or extract route from response`, e);
}
} else {
console.warn(`${TAG} Ollama router failed:`, res.status);
}
} catch (err) {
console.error(`${TAG} Ollama request error`, err);
}
let targetModel = null;
if (selectedRoute) {
targetModel = await getModelIdForRoute(selectedRoute);
if (!targetModel) {
const { defaultModel } = await new Promise(resolve =>
chrome.storage.sync.get(['defaultModel'], resolve)
);
targetModel = defaultModel || null;
if (targetModel) {
console.log(`${TAG} Falling back to default model: ${targetModel}`);
}
} else {
console.log(`${TAG} Resolved model for route "${selectedRoute}" →`, targetModel);
}
}
insertRouteLabelForLastUserMessage(selectedRoute);
const modifiedBody = { ...originalBody };
if (targetModel) {
modifiedBody.model = targetModel;
console.log(`${TAG} Overriding request with model: ${targetModel}`);
} else {
console.log(`${TAG} No route/model override applied`);
}
await streamToPort(await fetch(url, {
method: init.method,
headers: init.headers,
credentials: init.credentials,
body: JSON.stringify(modifiedBody)
}), port);
} catch (err) {
console.error(`${TAG} Proxy fetch error`, err);
port.postMessage({ done: true });
}
})();
});
let desiredModel = null;
function patchDom() {
if (!desiredModel) return;
const btn = document.querySelector('[data-testid="model-switcher-dropdown-button"]');
if (!btn) return;
const span = btn.querySelector('div > span');
const wantLabel = `Model selector, current model is ${desiredModel}`;
if (span && span.textContent !== desiredModel) {
span.textContent = desiredModel;
}
if (btn.getAttribute('aria-label') !== wantLabel) {
btn.setAttribute('aria-label', wantLabel);
}
}
// Observe DOM mutations and reactively patch
const observer = new MutationObserver(patchDom);
observer.observe(document.body || document.documentElement, {
subtree: true,
childList: true,
characterData: true,
attributes: true
});
// Set initial model from storage (optional default)
chrome.storage.sync.get(['defaultModel'], ({ defaultModel }) => {
if (defaultModel) {
desiredModel = defaultModel;
patchDom();
}
});
// ✅ Only listen for messages from iframe via window.postMessage
window.addEventListener('message', (event) => {
const data = event.data;
if (
typeof data === 'object' &&
data?.action === 'applyModelSelection' &&
typeof data.model === 'string'
) {
desiredModel = data.model;
patchDom();
}
});
function showModal() {
if (document.getElementById('pbms-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'pbms-overlay';
Object.assign(overlay.style, {
position: 'fixed', top: 0, left: 0,
width: '100vw', height: '100vh',
background: 'rgba(0,0,0,0.4)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 2147483647
});
const iframe = document.createElement('iframe');
iframe.src = chrome.runtime.getURL('index.html');
Object.assign(iframe.style, {
width: '500px', height: '600px',
border: 0, borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
background: 'white', zIndex: 2147483648
});
overlay.addEventListener('click', e => e.target === overlay && overlay.remove());
overlay.appendChild(iframe);
document.body.appendChild(overlay);
}
function interceptDropdown(ev) {
const btn = ev.target.closest('button[data-testid="model-switcher-dropdown-button"]');
if (!btn) return;
ev.preventDefault();
ev.stopPropagation();
showModal();
}
document.addEventListener('pointerdown', interceptDropdown, true);
document.addEventListener('mousedown', interceptDropdown, true);
window.addEventListener('message', ev => {
if (ev.data?.action === 'CLOSE_PBMS_MODAL') {
document.getElementById('pbms-overlay')?.remove();
}
});
console.log(`${TAG} content script initialized`);
})();

View file

@ -1,61 +0,0 @@
(function() {
const TAG = '[ModelSelector][Page]';
console.log(`${TAG} installing fetch override`);
const origFetch = window.fetch;
window.fetch = async function(input, init = {}) {
const urlString = typeof input === 'string' ? input : input.url;
const urlObj = new URL(urlString, window.location.origin);
const pathname = urlObj.pathname;
console.log(`${TAG} fetch →`, pathname);
const method = (init.method || 'GET').toUpperCase();
if (method === 'OPTIONS') {
console.log(`${TAG} OPTIONS request → bypassing completely`);
return origFetch(input, init);
}
// Only intercept conversation fetches
if (pathname === '/backend-api/conversation' || pathname === '/backend-api/f/conversation') {
console.log(`${TAG} matched → proxy via content script`);
const { port1, port2 } = new MessageChannel();
// ✅ Remove non-cloneable properties like 'signal'
const safeInit = { ...init };
delete safeInit.signal;
// Forward the fetch details to the content script
window.postMessage({
type: 'ARCHGW_FETCH',
url: urlString,
init: safeInit
}, '*', [port2]);
// Return a stream response that the content script will fulfill
return new Response(new ReadableStream({
start(controller) {
port1.onmessage = ({ data }) => {
if (data.done) {
controller.close();
port1.close();
} else {
controller.enqueue(new Uint8Array(data.chunk));
}
};
},
cancel() {
port1.close();
}
}), {
headers: { 'Content-Type': 'text/event-stream' }
});
}
// Otherwise, pass through to the original fetch
return origFetch(input, init);
};
console.log(`${TAG} fetch override installed`);
})();

View file

@ -1,12 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class', // ✅ Add this line
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./public/index.html",
],
theme: {
extend: {},
},
plugins: [],
}