happycolab 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- happycolab-1.0.0/PKG-INFO +126 -0
- happycolab-1.0.0/README.md +115 -0
- happycolab-1.0.0/happycolab/__init__.py +17 -0
- happycolab-1.0.0/happycolab/agent.py +365 -0
- happycolab-1.0.0/happycolab/cli.py +63 -0
- happycolab-1.0.0/happycolab/client.py +110 -0
- happycolab-1.0.0/happycolab/config.py +71 -0
- happycolab-1.0.0/happycolab/tools/__init__.py +25 -0
- happycolab-1.0.0/happycolab/tools/id_card_tools.py +63 -0
- happycolab-1.0.0/happycolab/tools/system_tools.py +53 -0
- happycolab-1.0.0/happycolab.egg-info/PKG-INFO +126 -0
- happycolab-1.0.0/happycolab.egg-info/SOURCES.txt +17 -0
- happycolab-1.0.0/happycolab.egg-info/dependency_links.txt +1 -0
- happycolab-1.0.0/happycolab.egg-info/entry_points.txt +3 -0
- happycolab-1.0.0/happycolab.egg-info/requires.txt +3 -0
- happycolab-1.0.0/happycolab.egg-info/top_level.txt +1 -0
- happycolab-1.0.0/pyproject.toml +23 -0
- happycolab-1.0.0/setup.cfg +4 -0
- happycolab-1.0.0/setup.py +3 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: happycolab
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: HappyColab: High-speed LLM bridge between Google Colab GPU and local machines with native tool execution
|
|
5
|
+
Author: Aswin Lal S Y
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: openai>=1.30.0
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
|
|
12
|
+
# 🌟 HappyColab (Python Library)
|
|
13
|
+
|
|
14
|
+
**HappyColab** is a high-speed Python client and AI agent framework that lets you run Large Language Models in **Google Colab** (with free GPU acceleration) and call them from any other machine with **native Tool & Function Calling**, real-time streaming, error recovery guides, and interactive chat.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📁 Repository Structure
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
HappyColab/
|
|
22
|
+
├── happycolab/ # 📦 Library Source Code
|
|
23
|
+
│ ├── __init__.py # Exports HappyAgent, HappyClient, tool
|
|
24
|
+
│ ├── agent.py # Agent with auto-schema tool decorator & loop
|
|
25
|
+
│ ├── client.py # Low-level OpenAI-compatible API connector
|
|
26
|
+
│ ├── config.py # Error recovery, guided troubleshooting & configs
|
|
27
|
+
│ ├── cli.py # CLI command runner (happycolab, happy-chat)
|
|
28
|
+
│ └── tools/ # Built-in tool suite
|
|
29
|
+
│ ├── system_tools.py # Open Google, open URL, calculate, local time
|
|
30
|
+
│ └── id_card_tools.py # ID card verification & clearance checks
|
|
31
|
+
│
|
|
32
|
+
├── examples/ # 💡 Ready-to-Run Examples
|
|
33
|
+
│ ├── 01_simple_chat.py # Basic single-turn Q&A
|
|
34
|
+
│ ├── 02_streaming.py # Real-time token streaming
|
|
35
|
+
│ ├── 03_custom_tool.py # Adding a custom tool
|
|
36
|
+
│ ├── 04_full_assistant.py # Interactive assistant with all default tools
|
|
37
|
+
│ └── 05_custom_function_calling.py # All 3 ways to register function calling
|
|
38
|
+
│
|
|
39
|
+
├── colab_server/ # 🚀 Laptop 1 (Colab Server Files)
|
|
40
|
+
│ ├── colab_server_code.py # Copy-pasteable script for Google Colab
|
|
41
|
+
│ └── colab_server_notebook.ipynb # Ready-to-upload Colab notebook
|
|
42
|
+
│
|
|
43
|
+
├── .gitignore
|
|
44
|
+
├── pyproject.toml
|
|
45
|
+
├── setup.py
|
|
46
|
+
└── requirements.txt
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ⚡ Quick Start
|
|
52
|
+
|
|
53
|
+
### 1. Launch Server (Laptop 1 - Google Colab)
|
|
54
|
+
Open [`colab_server/colab_server_notebook.ipynb`](colab_server/colab_server_notebook.ipynb) in Colab with T4 GPU. Copy the public Cloudflare URL (e.g. `https://xxxx.trycloudflare.com`).
|
|
55
|
+
|
|
56
|
+
### 2. Interactive Chat (Laptop 2)
|
|
57
|
+
The library is installed in your Python environment. You can immediately launch the interactive AI chat:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
happycolab
|
|
61
|
+
```
|
|
62
|
+
*(Or alias: `happy-chat`)*
|
|
63
|
+
|
|
64
|
+
If your URL is expired or unreachable, **HappyColab displays a step-by-step troubleshooting guide** and lets you paste your new URL directly into the prompt!
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🛠️ Adding Function Calling (3 Ways)
|
|
69
|
+
|
|
70
|
+
Users can add custom functions for the LLM to call in any of these 3 ways:
|
|
71
|
+
|
|
72
|
+
### Method 1: Using `@agent.tool` Decorator
|
|
73
|
+
Type hints and docstrings are automatically converted into OpenAI function calling schemas:
|
|
74
|
+
```python
|
|
75
|
+
from happycolab import HappyAgent
|
|
76
|
+
|
|
77
|
+
agent = HappyAgent(url="https://your-url.trycloudflare.com")
|
|
78
|
+
|
|
79
|
+
@agent.tool
|
|
80
|
+
def get_user_profile(user_id: str) -> dict:
|
|
81
|
+
"""Fetch employee department and clearance by User ID."""
|
|
82
|
+
return {"user_id": user_id, "name": "Jane Miller", "department": "AI Security"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Method 2: Using the Standalone `@tool` Decorator
|
|
86
|
+
```python
|
|
87
|
+
from happycolab import HappyAgent, tool
|
|
88
|
+
|
|
89
|
+
@tool
|
|
90
|
+
def calculate_tax(amount: float, tax_rate_percent: float = 10.0) -> dict:
|
|
91
|
+
"""Calculate tax and total cost for a transaction."""
|
|
92
|
+
tax = amount * (tax_rate_percent / 100.0)
|
|
93
|
+
return {"subtotal": amount, "tax": tax, "total": amount + tax}
|
|
94
|
+
|
|
95
|
+
agent = HappyAgent(url="https://your-url.trycloudflare.com")
|
|
96
|
+
agent.add_tool(calculate_tax)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Method 3: Registering Regular Python Functions (No Decorators!)
|
|
100
|
+
```python
|
|
101
|
+
def check_server(server_name: str) -> dict:
|
|
102
|
+
"""Check health and ping of an internal server."""
|
|
103
|
+
return {"server": server_name, "status": "ONLINE", "uptime": "99.9%"}
|
|
104
|
+
|
|
105
|
+
agent.add_tool(check_server)
|
|
106
|
+
# Or bulk register:
|
|
107
|
+
# agent.add_tools([func_a, func_b, func_c])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🔀 Flexible Model Switching
|
|
113
|
+
|
|
114
|
+
### In Terminal Chat:
|
|
115
|
+
While chatting inside `happycolab`, you can use slash commands:
|
|
116
|
+
- `/model llama3.2:3b` ➔ Switch model dynamically.
|
|
117
|
+
- `/models` ➔ Query all models installed on your Colab instance.
|
|
118
|
+
- `/url <new_url>` ➔ Change the tunnel URL on the fly.
|
|
119
|
+
- `/tools` ➔ View active tools.
|
|
120
|
+
- `/help` ➔ Show troubleshooting guide.
|
|
121
|
+
|
|
122
|
+
### In Python:
|
|
123
|
+
```python
|
|
124
|
+
agent.set_model("llama3.2:3b")
|
|
125
|
+
print(agent.list_models())
|
|
126
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# 🌟 HappyColab (Python Library)
|
|
2
|
+
|
|
3
|
+
**HappyColab** is a high-speed Python client and AI agent framework that lets you run Large Language Models in **Google Colab** (with free GPU acceleration) and call them from any other machine with **native Tool & Function Calling**, real-time streaming, error recovery guides, and interactive chat.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📁 Repository Structure
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
HappyColab/
|
|
11
|
+
├── happycolab/ # 📦 Library Source Code
|
|
12
|
+
│ ├── __init__.py # Exports HappyAgent, HappyClient, tool
|
|
13
|
+
│ ├── agent.py # Agent with auto-schema tool decorator & loop
|
|
14
|
+
│ ├── client.py # Low-level OpenAI-compatible API connector
|
|
15
|
+
│ ├── config.py # Error recovery, guided troubleshooting & configs
|
|
16
|
+
│ ├── cli.py # CLI command runner (happycolab, happy-chat)
|
|
17
|
+
│ └── tools/ # Built-in tool suite
|
|
18
|
+
│ ├── system_tools.py # Open Google, open URL, calculate, local time
|
|
19
|
+
│ └── id_card_tools.py # ID card verification & clearance checks
|
|
20
|
+
│
|
|
21
|
+
├── examples/ # 💡 Ready-to-Run Examples
|
|
22
|
+
│ ├── 01_simple_chat.py # Basic single-turn Q&A
|
|
23
|
+
│ ├── 02_streaming.py # Real-time token streaming
|
|
24
|
+
│ ├── 03_custom_tool.py # Adding a custom tool
|
|
25
|
+
│ ├── 04_full_assistant.py # Interactive assistant with all default tools
|
|
26
|
+
│ └── 05_custom_function_calling.py # All 3 ways to register function calling
|
|
27
|
+
│
|
|
28
|
+
├── colab_server/ # 🚀 Laptop 1 (Colab Server Files)
|
|
29
|
+
│ ├── colab_server_code.py # Copy-pasteable script for Google Colab
|
|
30
|
+
│ └── colab_server_notebook.ipynb # Ready-to-upload Colab notebook
|
|
31
|
+
│
|
|
32
|
+
├── .gitignore
|
|
33
|
+
├── pyproject.toml
|
|
34
|
+
├── setup.py
|
|
35
|
+
└── requirements.txt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## ⚡ Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Launch Server (Laptop 1 - Google Colab)
|
|
43
|
+
Open [`colab_server/colab_server_notebook.ipynb`](colab_server/colab_server_notebook.ipynb) in Colab with T4 GPU. Copy the public Cloudflare URL (e.g. `https://xxxx.trycloudflare.com`).
|
|
44
|
+
|
|
45
|
+
### 2. Interactive Chat (Laptop 2)
|
|
46
|
+
The library is installed in your Python environment. You can immediately launch the interactive AI chat:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
happycolab
|
|
50
|
+
```
|
|
51
|
+
*(Or alias: `happy-chat`)*
|
|
52
|
+
|
|
53
|
+
If your URL is expired or unreachable, **HappyColab displays a step-by-step troubleshooting guide** and lets you paste your new URL directly into the prompt!
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 🛠️ Adding Function Calling (3 Ways)
|
|
58
|
+
|
|
59
|
+
Users can add custom functions for the LLM to call in any of these 3 ways:
|
|
60
|
+
|
|
61
|
+
### Method 1: Using `@agent.tool` Decorator
|
|
62
|
+
Type hints and docstrings are automatically converted into OpenAI function calling schemas:
|
|
63
|
+
```python
|
|
64
|
+
from happycolab import HappyAgent
|
|
65
|
+
|
|
66
|
+
agent = HappyAgent(url="https://your-url.trycloudflare.com")
|
|
67
|
+
|
|
68
|
+
@agent.tool
|
|
69
|
+
def get_user_profile(user_id: str) -> dict:
|
|
70
|
+
"""Fetch employee department and clearance by User ID."""
|
|
71
|
+
return {"user_id": user_id, "name": "Jane Miller", "department": "AI Security"}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Method 2: Using the Standalone `@tool` Decorator
|
|
75
|
+
```python
|
|
76
|
+
from happycolab import HappyAgent, tool
|
|
77
|
+
|
|
78
|
+
@tool
|
|
79
|
+
def calculate_tax(amount: float, tax_rate_percent: float = 10.0) -> dict:
|
|
80
|
+
"""Calculate tax and total cost for a transaction."""
|
|
81
|
+
tax = amount * (tax_rate_percent / 100.0)
|
|
82
|
+
return {"subtotal": amount, "tax": tax, "total": amount + tax}
|
|
83
|
+
|
|
84
|
+
agent = HappyAgent(url="https://your-url.trycloudflare.com")
|
|
85
|
+
agent.add_tool(calculate_tax)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Method 3: Registering Regular Python Functions (No Decorators!)
|
|
89
|
+
```python
|
|
90
|
+
def check_server(server_name: str) -> dict:
|
|
91
|
+
"""Check health and ping of an internal server."""
|
|
92
|
+
return {"server": server_name, "status": "ONLINE", "uptime": "99.9%"}
|
|
93
|
+
|
|
94
|
+
agent.add_tool(check_server)
|
|
95
|
+
# Or bulk register:
|
|
96
|
+
# agent.add_tools([func_a, func_b, func_c])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🔀 Flexible Model Switching
|
|
102
|
+
|
|
103
|
+
### In Terminal Chat:
|
|
104
|
+
While chatting inside `happycolab`, you can use slash commands:
|
|
105
|
+
- `/model llama3.2:3b` ➔ Switch model dynamically.
|
|
106
|
+
- `/models` ➔ Query all models installed on your Colab instance.
|
|
107
|
+
- `/url <new_url>` ➔ Change the tunnel URL on the fly.
|
|
108
|
+
- `/tools` ➔ View active tools.
|
|
109
|
+
- `/help` ➔ Show troubleshooting guide.
|
|
110
|
+
|
|
111
|
+
### In Python:
|
|
112
|
+
```python
|
|
113
|
+
agent.set_model("llama3.2:3b")
|
|
114
|
+
print(agent.list_models())
|
|
115
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HappyColab: A Python library for running LLMs in Google Colab and executing tools locally on your client machine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import HappyClient, ColabClient
|
|
6
|
+
from .agent import HappyAgent, ColabAgent, tool
|
|
7
|
+
from .tools import ALL_DEFAULT_TOOLS
|
|
8
|
+
|
|
9
|
+
__version__ = "1.0.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"HappyClient",
|
|
12
|
+
"HappyAgent",
|
|
13
|
+
"ColabClient",
|
|
14
|
+
"ColabAgent",
|
|
15
|
+
"tool",
|
|
16
|
+
"ALL_DEFAULT_TOOLS",
|
|
17
|
+
]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ColabAgent: High-level Agent class with automatic tool schema generation,
|
|
3
|
+
execution dispatch, and conversational memory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
from typing import Callable, Dict, Any, List, Optional
|
|
9
|
+
from .client import HappyClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def python_type_to_json_type(py_type: Any) -> str:
|
|
13
|
+
"""Map python types to JSON schema types."""
|
|
14
|
+
if py_type in (int,):
|
|
15
|
+
return "integer"
|
|
16
|
+
if py_type in (float,):
|
|
17
|
+
return "number"
|
|
18
|
+
if py_type in (bool,):
|
|
19
|
+
return "boolean"
|
|
20
|
+
if py_type in (list, List):
|
|
21
|
+
return "array"
|
|
22
|
+
if py_type in (dict, Dict):
|
|
23
|
+
return "object"
|
|
24
|
+
return "string"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_tool_schema(func: Callable) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Automatically converts a Python function and its docstring/type hints
|
|
30
|
+
into an OpenAI-compatible function calling schema.
|
|
31
|
+
"""
|
|
32
|
+
sig = inspect.signature(func)
|
|
33
|
+
doc = inspect.getdoc(func) or f"Execute {func.__name__}"
|
|
34
|
+
|
|
35
|
+
properties = {}
|
|
36
|
+
required = []
|
|
37
|
+
|
|
38
|
+
for name, param in sig.parameters.items():
|
|
39
|
+
if name in ("self", "cls"):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
param_type = param.annotation if param.annotation != inspect.Parameter.empty else str
|
|
43
|
+
json_type = python_type_to_json_type(param_type)
|
|
44
|
+
|
|
45
|
+
properties[name] = {
|
|
46
|
+
"type": json_type,
|
|
47
|
+
"description": f"Parameter '{name}'",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if param.default == inspect.Parameter.empty:
|
|
51
|
+
required.append(name)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"type": "function",
|
|
55
|
+
"function": {
|
|
56
|
+
"name": func.__name__,
|
|
57
|
+
"description": doc,
|
|
58
|
+
"parameters": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": properties,
|
|
61
|
+
"required": required,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def tool(func: Callable) -> Callable:
|
|
68
|
+
"""
|
|
69
|
+
Standalone decorator to mark a Python function as a tool.
|
|
70
|
+
Usage:
|
|
71
|
+
@tool
|
|
72
|
+
def my_func(query: str) -> str:
|
|
73
|
+
'''Look up information.'''
|
|
74
|
+
return "data"
|
|
75
|
+
|
|
76
|
+
agent.add_tool(my_func)
|
|
77
|
+
"""
|
|
78
|
+
func.__is_happy_tool__ = True
|
|
79
|
+
func.__tool_schema__ = generate_tool_schema(func)
|
|
80
|
+
return func
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class HappyAgent:
|
|
84
|
+
"""
|
|
85
|
+
Intelligent Agent running against a Colab-hosted LLM with local tool execution.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
url: str,
|
|
91
|
+
model: str = "qwen2.5:7b",
|
|
92
|
+
system_prompt: Optional[str] = None,
|
|
93
|
+
timeout: float = 90.0,
|
|
94
|
+
):
|
|
95
|
+
self.client = HappyClient(url=url, model=model, timeout=timeout)
|
|
96
|
+
self.model = model
|
|
97
|
+
self.system_prompt = system_prompt or (
|
|
98
|
+
"You are a helpful and intelligent AI assistant. "
|
|
99
|
+
"You have access to tools that can be executed on the user's local machine. "
|
|
100
|
+
"When the user requests an action (such as opening a website or checking records), "
|
|
101
|
+
"use the relevant tool. Always provide friendly, direct answers."
|
|
102
|
+
)
|
|
103
|
+
self.tools_registry: Dict[str, Callable] = {}
|
|
104
|
+
self.tools_schema: List[Dict[str, Any]] = []
|
|
105
|
+
self.history: List[Dict[str, Any]] = [{"role": "system", "content": self.system_prompt}]
|
|
106
|
+
|
|
107
|
+
def tool(self, func: Callable) -> Callable:
|
|
108
|
+
"""
|
|
109
|
+
Decorator to register a Python function as a tool on this agent instance.
|
|
110
|
+
Example:
|
|
111
|
+
@agent.tool
|
|
112
|
+
def open_google() -> str:
|
|
113
|
+
'''Opens Google in the web browser.'''
|
|
114
|
+
import webbrowser
|
|
115
|
+
webbrowser.open("https://www.google.com")
|
|
116
|
+
return "Google opened successfully"
|
|
117
|
+
"""
|
|
118
|
+
self.add_tool(func)
|
|
119
|
+
return func
|
|
120
|
+
|
|
121
|
+
def add_tool(self, func: Callable, schema: Optional[Dict[str, Any]] = None):
|
|
122
|
+
"""
|
|
123
|
+
Register a single function as an executable tool.
|
|
124
|
+
If no custom schema is provided, auto-generates schema from docstring and type hints.
|
|
125
|
+
"""
|
|
126
|
+
self.tools_registry[func.__name__] = func
|
|
127
|
+
auto_schema = schema or getattr(func, "__tool_schema__", None) or generate_tool_schema(func)
|
|
128
|
+
|
|
129
|
+
# Replace existing schema if updating, or append
|
|
130
|
+
existing_idx = next((i for i, s in enumerate(self.tools_schema) if s.get("function", {}).get("name") == func.__name__), None)
|
|
131
|
+
if existing_idx is not None:
|
|
132
|
+
self.tools_schema[existing_idx] = auto_schema
|
|
133
|
+
else:
|
|
134
|
+
self.tools_schema.append(auto_schema)
|
|
135
|
+
|
|
136
|
+
def add_tools(self, funcs: List[Callable]):
|
|
137
|
+
"""
|
|
138
|
+
Bulk register multiple functions as tools.
|
|
139
|
+
Example:
|
|
140
|
+
agent.add_tools([fetch_weather, calculate_tax, open_dashboard])
|
|
141
|
+
"""
|
|
142
|
+
for f in funcs:
|
|
143
|
+
self.add_tool(f)
|
|
144
|
+
|
|
145
|
+
# Aliases
|
|
146
|
+
register_tool = add_tool
|
|
147
|
+
register_tools = add_tools
|
|
148
|
+
|
|
149
|
+
def load_default_tools(self):
|
|
150
|
+
"""Load all built-in tools (Google, URLs, Time, Calculator, ID Card lookup)."""
|
|
151
|
+
from .tools import ALL_DEFAULT_TOOLS
|
|
152
|
+
for t in ALL_DEFAULT_TOOLS:
|
|
153
|
+
if t.__name__ not in self.tools_registry:
|
|
154
|
+
self.add_tool(t)
|
|
155
|
+
|
|
156
|
+
def set_model(self, model: str):
|
|
157
|
+
"""Change the active model name."""
|
|
158
|
+
self.model = model.strip()
|
|
159
|
+
self.client.set_model(self.model)
|
|
160
|
+
|
|
161
|
+
def set_url(self, url: str):
|
|
162
|
+
"""Update the active Colab URL."""
|
|
163
|
+
self.client.set_url(url)
|
|
164
|
+
|
|
165
|
+
def list_models(self) -> List[str]:
|
|
166
|
+
"""List models available on the Colab server."""
|
|
167
|
+
return self.client.list_models()
|
|
168
|
+
|
|
169
|
+
def ping(self) -> bool:
|
|
170
|
+
"""Verify server connection."""
|
|
171
|
+
return self.client.ping()
|
|
172
|
+
|
|
173
|
+
def reset_history(self):
|
|
174
|
+
"""Clear conversation history."""
|
|
175
|
+
self.history = [{"role": "system", "content": self.system_prompt}]
|
|
176
|
+
|
|
177
|
+
def ask(self, user_query: str, verbose: bool = True) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Process a user input, execute tools if requested by the LLM, and return response.
|
|
180
|
+
"""
|
|
181
|
+
self.history.append({"role": "user", "content": user_query})
|
|
182
|
+
|
|
183
|
+
# Request completion from Colab LLM
|
|
184
|
+
if self.tools_schema:
|
|
185
|
+
response_msg = self.client.chat_with_tools(
|
|
186
|
+
messages=self.history,
|
|
187
|
+
tools=self.tools_schema,
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
raw_reply = self.client.chat(messages=self.history)
|
|
191
|
+
self.history.append({"role": "assistant", "content": raw_reply})
|
|
192
|
+
return raw_reply
|
|
193
|
+
|
|
194
|
+
# Handle tool execution loop
|
|
195
|
+
if response_msg.tool_calls:
|
|
196
|
+
self.history.append(response_msg)
|
|
197
|
+
|
|
198
|
+
for tool_call in response_msg.tool_calls:
|
|
199
|
+
func_name = tool_call.function.name
|
|
200
|
+
func_args_str = tool_call.function.arguments or "{}"
|
|
201
|
+
try:
|
|
202
|
+
func_args = json.loads(func_args_str)
|
|
203
|
+
except Exception:
|
|
204
|
+
func_args = {}
|
|
205
|
+
|
|
206
|
+
call_id = tool_call.id
|
|
207
|
+
|
|
208
|
+
if verbose:
|
|
209
|
+
print(f" ⚡ [Tool Executing] {func_name}({func_args})")
|
|
210
|
+
|
|
211
|
+
# Dispatch locally
|
|
212
|
+
if func_name in self.tools_registry:
|
|
213
|
+
try:
|
|
214
|
+
res = self.tools_registry[func_name](**func_args)
|
|
215
|
+
output_str = json.dumps(res) if not isinstance(res, str) else res
|
|
216
|
+
except Exception as err:
|
|
217
|
+
output_str = json.dumps({"error": str(err)})
|
|
218
|
+
else:
|
|
219
|
+
output_str = json.dumps({"error": f"Tool '{func_name}' is not registered."})
|
|
220
|
+
|
|
221
|
+
if verbose:
|
|
222
|
+
print(f" 👉 [Result] {output_str}")
|
|
223
|
+
|
|
224
|
+
self.history.append({
|
|
225
|
+
"role": "tool",
|
|
226
|
+
"tool_call_id": call_id,
|
|
227
|
+
"content": output_str,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
# Get final formulated reply with tool results
|
|
231
|
+
final_reply = self.client.chat(messages=self.history)
|
|
232
|
+
self.history.append({"role": "assistant", "content": final_reply})
|
|
233
|
+
return final_reply
|
|
234
|
+
else:
|
|
235
|
+
reply = response_msg.content or ""
|
|
236
|
+
self.history.append({"role": "assistant", "content": reply})
|
|
237
|
+
return reply
|
|
238
|
+
|
|
239
|
+
def interactive_chat(self):
|
|
240
|
+
"""Launch interactive terminal chat session with commands and error guidance."""
|
|
241
|
+
from .config import print_error_guide, save_config
|
|
242
|
+
|
|
243
|
+
print("\n" + "=" * 65)
|
|
244
|
+
print("🤖 HAPPYCOLAB AGENT - INTERACTIVE CHAT")
|
|
245
|
+
print(f" Model : {self.model}")
|
|
246
|
+
print(f" Endpoint : {self.client.url}")
|
|
247
|
+
print(f" Tools Active: {list(self.tools_registry.keys()) or 'None'}")
|
|
248
|
+
print("\n💡 Commands:")
|
|
249
|
+
print(" /model <name> -> Switch model (e.g. /model llama3.2:3b)")
|
|
250
|
+
print(" /models -> List models on Colab")
|
|
251
|
+
print(" /url <link> -> Change Colab Cloudflare URL")
|
|
252
|
+
print(" /tools -> Show active tools")
|
|
253
|
+
print(" /clear -> Reset conversation history")
|
|
254
|
+
print(" /help -> Show troubleshooting guide")
|
|
255
|
+
print(" exit / quit -> End chat")
|
|
256
|
+
print("=" * 65)
|
|
257
|
+
|
|
258
|
+
while True:
|
|
259
|
+
try:
|
|
260
|
+
user_msg = input("\nYou: ").strip()
|
|
261
|
+
if not user_msg:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Exit command
|
|
265
|
+
if user_msg.lower() in ("exit", "quit", "q"):
|
|
266
|
+
print("Goodbye!")
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
# Help command
|
|
270
|
+
if user_msg.lower() == "/help":
|
|
271
|
+
print_error_guide(self.client.url, "User requested /help guide.")
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Clear history
|
|
275
|
+
if user_msg.lower() == "/clear":
|
|
276
|
+
self.reset_history()
|
|
277
|
+
print("🧹 Conversation history cleared.")
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# Show active tools
|
|
281
|
+
if user_msg.lower() == "/tools":
|
|
282
|
+
print(f"\nRegistered Tools ({len(self.tools_registry)}):")
|
|
283
|
+
for t_name, t_func in self.tools_registry.items():
|
|
284
|
+
print(f" • {t_name}: {inspect.getdoc(t_func) or 'No docstring'}")
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# List models on Colab
|
|
288
|
+
if user_msg.lower() == "/models":
|
|
289
|
+
print("Fetching models from Colab...")
|
|
290
|
+
avail_models = self.list_models()
|
|
291
|
+
if avail_models:
|
|
292
|
+
print("Installed Models on Colab:")
|
|
293
|
+
for m in avail_models:
|
|
294
|
+
marker = " (Active)" if m == self.model else ""
|
|
295
|
+
print(f" • {m}{marker}")
|
|
296
|
+
else:
|
|
297
|
+
print(f"Current model is: {self.model}. (Could not query models list or only 1 loaded).")
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
# Change model command
|
|
301
|
+
if user_msg.lower().startswith("/model"):
|
|
302
|
+
parts = user_msg.split(maxsplit=1)
|
|
303
|
+
if len(parts) > 1:
|
|
304
|
+
new_model = parts[1].strip()
|
|
305
|
+
self.set_model(new_model)
|
|
306
|
+
save_config(model=new_model)
|
|
307
|
+
print(f"✅ Model switched to: '{new_model}'")
|
|
308
|
+
else:
|
|
309
|
+
print(f"Current model: '{self.model}'")
|
|
310
|
+
new_model = input("Enter new model name (e.g. qwen2.5:7b, llama3.2:3b, mistral:7b): ").strip()
|
|
311
|
+
if new_model:
|
|
312
|
+
self.set_model(new_model)
|
|
313
|
+
save_config(model=new_model)
|
|
314
|
+
print(f"✅ Model switched to: '{new_model}'")
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# Change URL command
|
|
318
|
+
if user_msg.lower().startswith("/url"):
|
|
319
|
+
parts = user_msg.split(maxsplit=1)
|
|
320
|
+
if len(parts) > 1:
|
|
321
|
+
new_url = parts[1].strip()
|
|
322
|
+
else:
|
|
323
|
+
new_url = input("Enter new Colab Cloudflare URL: ").strip()
|
|
324
|
+
|
|
325
|
+
if new_url:
|
|
326
|
+
self.set_url(new_url)
|
|
327
|
+
ok, msg = self.client.ping_detailed()
|
|
328
|
+
if ok:
|
|
329
|
+
save_config(url=new_url)
|
|
330
|
+
print(f"✅ URL updated & verified: {new_url}")
|
|
331
|
+
else:
|
|
332
|
+
print_error_guide(new_url, msg)
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
# Normal conversational ask
|
|
336
|
+
response = self.ask(user_msg, verbose=True)
|
|
337
|
+
print(f"\n🤖 HappyColab LLM:\n{response}")
|
|
338
|
+
|
|
339
|
+
except KeyboardInterrupt:
|
|
340
|
+
print("\nSession paused.")
|
|
341
|
+
break
|
|
342
|
+
except Exception as e:
|
|
343
|
+
err_msg = str(e)
|
|
344
|
+
print(f"\n❌ Error encountered: {err_msg}")
|
|
345
|
+
# Print guided walkthrough
|
|
346
|
+
print_error_guide(self.client.url, err_msg)
|
|
347
|
+
|
|
348
|
+
# Offer immediate prompt to paste a new URL or model
|
|
349
|
+
prompt_fix = input("Paste your new Colab URL here (or press Enter to retry / skip): ").strip()
|
|
350
|
+
if prompt_fix:
|
|
351
|
+
if prompt_fix.startswith("http"):
|
|
352
|
+
self.set_url(prompt_fix)
|
|
353
|
+
ok, msg = self.client.ping_detailed()
|
|
354
|
+
if ok:
|
|
355
|
+
save_config(url=prompt_fix)
|
|
356
|
+
print(f"🎉 Successfully reconnected to: {prompt_fix}")
|
|
357
|
+
else:
|
|
358
|
+
print(f"⚠️ Still unable to connect: {msg}")
|
|
359
|
+
else:
|
|
360
|
+
print("Invalid URL format. Expected 'https://...trycloudflare.com'")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Backward compatibility alias
|
|
364
|
+
ColabAgent = HappyAgent
|
|
365
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface entry point for happycolab and happy-chat.
|
|
3
|
+
Provides guided error recovery, interactive link pasting, and model switching.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from .agent import HappyAgent
|
|
9
|
+
from .config import load_config, save_config, print_error_guide
|
|
10
|
+
|
|
11
|
+
DEFAULT_FALLBACK_URL = "https://polished-hang-corrections-skip.trycloudflare.com"
|
|
12
|
+
DEFAULT_FALLBACK_MODEL = "qwen2.5:7b"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
cfg = load_config()
|
|
17
|
+
saved_url = cfg.get("url", DEFAULT_FALLBACK_URL)
|
|
18
|
+
saved_model = cfg.get("model", DEFAULT_FALLBACK_MODEL)
|
|
19
|
+
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description="HappyColab: High-speed LLM client with local tool execution backed by Google Colab"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--url", default=saved_url, help=f"Cloudflare URL from Colab (default: {saved_url})")
|
|
24
|
+
parser.add_argument("--model", default=saved_model, help=f"Model name (default: {saved_model})")
|
|
25
|
+
parser.add_argument("--no-tools", action="store_true", help="Disable default local tools")
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
active_url = args.url
|
|
29
|
+
active_model = args.model
|
|
30
|
+
|
|
31
|
+
agent = HappyAgent(url=active_url, model=active_model)
|
|
32
|
+
|
|
33
|
+
# Verify connection
|
|
34
|
+
print(f"\n🔍 Connecting to HappyColab LLM at: {active_url} (Model: {active_model}) ...")
|
|
35
|
+
connected, reason = agent.client.ping_detailed()
|
|
36
|
+
|
|
37
|
+
while not connected:
|
|
38
|
+
# Show guided troubleshooting steps
|
|
39
|
+
print_error_guide(active_url, reason)
|
|
40
|
+
|
|
41
|
+
user_input = input("👉 Paste your active Colab Cloudflare URL (or press Enter to exit): ").strip()
|
|
42
|
+
if not user_input:
|
|
43
|
+
print("\nExiting. Start your Colab notebook and run happycolab again anytime.")
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
|
|
46
|
+
active_url = user_input
|
|
47
|
+
agent.set_url(active_url)
|
|
48
|
+
print(f"\nRetrying connection to: {active_url} ...")
|
|
49
|
+
connected, reason = agent.client.ping_detailed()
|
|
50
|
+
|
|
51
|
+
if connected:
|
|
52
|
+
save_config(url=active_url)
|
|
53
|
+
print(" Connected successfully!")
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if not args.no_tools:
|
|
57
|
+
agent.load_default_tools()
|
|
58
|
+
|
|
59
|
+
agent.interactive_chat()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core API client for connecting to LLMs hosted on Google Colab or remote endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Any, Generator, Optional
|
|
6
|
+
from openai import OpenAI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HappyClient:
|
|
10
|
+
"""
|
|
11
|
+
Connects to an OpenAI-compatible LLM server running in Google Colab via Cloudflare/ngrok.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, url: str, model: str = "qwen2.5:7b", timeout: float = 90.0):
|
|
15
|
+
self.timeout = timeout
|
|
16
|
+
self.model = model
|
|
17
|
+
self.set_url(url)
|
|
18
|
+
|
|
19
|
+
def set_url(self, url: str):
|
|
20
|
+
"""Update Colab tunnel URL and reconfigure OpenAI client."""
|
|
21
|
+
base_url = url.strip().rstrip("/")
|
|
22
|
+
if not base_url.endswith("/v1"):
|
|
23
|
+
base_url = f"{base_url}/v1"
|
|
24
|
+
self.url = base_url
|
|
25
|
+
self.client = OpenAI(base_url=self.url, api_key="ollama", timeout=self.timeout)
|
|
26
|
+
|
|
27
|
+
def set_model(self, model: str):
|
|
28
|
+
"""Update model name (e.g. 'qwen2.5:7b', 'llama3.2:3b', 'mistral:7b')."""
|
|
29
|
+
self.model = model.strip()
|
|
30
|
+
|
|
31
|
+
def ping(self) -> bool:
|
|
32
|
+
"""Check if Colab server is reachable and active."""
|
|
33
|
+
ok, _ = self.ping_detailed()
|
|
34
|
+
return ok
|
|
35
|
+
|
|
36
|
+
def ping_detailed(self) -> tuple:
|
|
37
|
+
"""
|
|
38
|
+
Check connection and return (success: bool, message: str).
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
models = self.client.models.list()
|
|
42
|
+
return True, f"Connected! Found {len(models.data)} model(s) available."
|
|
43
|
+
except Exception as e:
|
|
44
|
+
err_str = str(e)
|
|
45
|
+
if "403" in err_str:
|
|
46
|
+
return False, "HTTP 403 Forbidden: Colab blocked the connection. Ensure OLLAMA_ORIGINS='*' and --http-host-header are configured in Colab."
|
|
47
|
+
if "Connection refused" in err_str or "Failed to connect" in err_str or "ConnectError" in err_str:
|
|
48
|
+
return False, "Connection Refused: Colab cell might not be running or Cloudflare tunnel is offline."
|
|
49
|
+
return False, f"Connection Failed: {err_str}"
|
|
50
|
+
|
|
51
|
+
def list_models(self) -> List[str]:
|
|
52
|
+
"""List model names available on the Colab server."""
|
|
53
|
+
try:
|
|
54
|
+
res = self.client.models.list()
|
|
55
|
+
return [m.id for m in res.data]
|
|
56
|
+
except Exception:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
def chat(
|
|
60
|
+
self,
|
|
61
|
+
messages: List[Dict[str, Any]],
|
|
62
|
+
temperature: float = 0.7,
|
|
63
|
+
max_tokens: Optional[int] = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Standard chat completion."""
|
|
66
|
+
res = self.client.chat.completions.create(
|
|
67
|
+
model=self.model,
|
|
68
|
+
messages=messages,
|
|
69
|
+
temperature=temperature,
|
|
70
|
+
max_tokens=max_tokens,
|
|
71
|
+
)
|
|
72
|
+
return res.choices[0].message.content or ""
|
|
73
|
+
|
|
74
|
+
def stream_chat(
|
|
75
|
+
self,
|
|
76
|
+
messages: List[Dict[str, Any]],
|
|
77
|
+
temperature: float = 0.7,
|
|
78
|
+
) -> Generator[str, None, None]:
|
|
79
|
+
"""Stream response tokens in real-time."""
|
|
80
|
+
stream = self.client.chat.completions.create(
|
|
81
|
+
model=self.model,
|
|
82
|
+
messages=messages,
|
|
83
|
+
temperature=temperature,
|
|
84
|
+
stream=True,
|
|
85
|
+
)
|
|
86
|
+
for chunk in stream:
|
|
87
|
+
content = chunk.choices[0].delta.content
|
|
88
|
+
if content:
|
|
89
|
+
yield content
|
|
90
|
+
|
|
91
|
+
def chat_with_tools(
|
|
92
|
+
self,
|
|
93
|
+
messages: List[Dict[str, Any]],
|
|
94
|
+
tools: List[Dict[str, Any]],
|
|
95
|
+
tool_choice: str = "auto",
|
|
96
|
+
temperature: float = 0.2,
|
|
97
|
+
):
|
|
98
|
+
"""Invoke completion with tool definitions."""
|
|
99
|
+
res = self.client.chat.completions.create(
|
|
100
|
+
model=self.model,
|
|
101
|
+
messages=messages,
|
|
102
|
+
tools=tools,
|
|
103
|
+
tool_choice=tool_choice,
|
|
104
|
+
temperature=temperature,
|
|
105
|
+
)
|
|
106
|
+
return res.choices[0].message
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Backward compatibility alias
|
|
110
|
+
ColabClient = HappyClient
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration and troubleshooting helper for HappyColab.
|
|
3
|
+
Manages persistent URLs, model preferences, and helpful error guidance.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
|
|
10
|
+
CONFIG_FILE_NAME = ".happycolab.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_config_path() -> str:
|
|
14
|
+
"""Returns local project config path, or user home directory path."""
|
|
15
|
+
cwd_path = os.path.join(os.getcwd(), CONFIG_FILE_NAME)
|
|
16
|
+
if os.path.exists(cwd_path):
|
|
17
|
+
return cwd_path
|
|
18
|
+
home_dir = os.path.expanduser("~")
|
|
19
|
+
return os.path.join(home_dir, CONFIG_FILE_NAME)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_config() -> Dict[str, Any]:
|
|
23
|
+
"""Loads saved config if exists."""
|
|
24
|
+
path = get_config_path()
|
|
25
|
+
if os.path.exists(path):
|
|
26
|
+
try:
|
|
27
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
28
|
+
return json.load(f)
|
|
29
|
+
except Exception:
|
|
30
|
+
return {}
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_config(url: Optional[str] = None, model: Optional[str] = None):
|
|
35
|
+
"""Saves preferences to config file."""
|
|
36
|
+
cfg = load_config()
|
|
37
|
+
if url:
|
|
38
|
+
cfg["url"] = url
|
|
39
|
+
if model:
|
|
40
|
+
cfg["model"] = model
|
|
41
|
+
path = get_config_path()
|
|
42
|
+
try:
|
|
43
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
44
|
+
json.dump(cfg, f, indent=2)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_error_guide(url: str, reason: str = ""):
|
|
50
|
+
"""
|
|
51
|
+
Displays a friendly, clear troubleshooting guide when connection fails.
|
|
52
|
+
"""
|
|
53
|
+
print("\n" + "═" * 70)
|
|
54
|
+
print("⚠️ COULD NOT CONNECT TO GOOGLE COLAB LLM")
|
|
55
|
+
print("═" * 70)
|
|
56
|
+
if reason:
|
|
57
|
+
print(f"👉 Error Details: {reason}")
|
|
58
|
+
print(f"👉 Target URL : {url}")
|
|
59
|
+
print("\n📋 STEP-BY-STEP TROUBLESHOOTING GUIDE:")
|
|
60
|
+
print(" 1. Check Colab on Laptop 1:")
|
|
61
|
+
print(" Make sure your Google Colab tab is open and the notebook cell is actively running.")
|
|
62
|
+
print(" 2. Cloudflare URLs are temporary:")
|
|
63
|
+
print(" Every time you restart Colab or restart the tunnel, a NEW .trycloudflare.com")
|
|
64
|
+
print(" URL is created. The previous link expires immediately.")
|
|
65
|
+
print(" 3. Check for 403 Forbidden:")
|
|
66
|
+
print(" If you see a 403 error, ensure your Colab cell has:")
|
|
67
|
+
print(" os.environ['OLLAMA_ORIGINS'] = '*'")
|
|
68
|
+
print(" and cloudflared is run with: --http-host-header localhost:11434")
|
|
69
|
+
print(" 4. Check GPU & Model:")
|
|
70
|
+
print(" In Colab, go to Runtime -> Change runtime type -> Ensure T4 GPU is selected.")
|
|
71
|
+
print("═" * 70 + "\n")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in tools bundle for colab-llm.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .system_tools import open_google, open_url, get_system_time, calculate
|
|
6
|
+
from .id_card_tools import lookup_id_card, verify_security_clearance
|
|
7
|
+
|
|
8
|
+
ALL_DEFAULT_TOOLS = [
|
|
9
|
+
open_google,
|
|
10
|
+
open_url,
|
|
11
|
+
get_system_time,
|
|
12
|
+
calculate,
|
|
13
|
+
lookup_id_card,
|
|
14
|
+
verify_security_clearance,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"open_google",
|
|
19
|
+
"open_url",
|
|
20
|
+
"get_system_time",
|
|
21
|
+
"calculate",
|
|
22
|
+
"lookup_id_card",
|
|
23
|
+
"verify_security_clearance",
|
|
24
|
+
"ALL_DEFAULT_TOOLS",
|
|
25
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ID Card and Badge verification tools.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
MOCK_ID_DATABASE: Dict[str, Dict[str, Any]] = {
|
|
8
|
+
"ID-9021": {
|
|
9
|
+
"status": "active",
|
|
10
|
+
"name": "Sarah Connor",
|
|
11
|
+
"role": "Lead Systems Engineer",
|
|
12
|
+
"department": "Cybernetics Division",
|
|
13
|
+
"clearance_level": 4,
|
|
14
|
+
"expiry_date": "2028-12-31",
|
|
15
|
+
},
|
|
16
|
+
"ID-4420": {
|
|
17
|
+
"status": "suspended",
|
|
18
|
+
"name": "Alex Mercer",
|
|
19
|
+
"role": "Research Analyst",
|
|
20
|
+
"department": "Biotech Labs",
|
|
21
|
+
"clearance_level": 2,
|
|
22
|
+
"expiry_date": "2025-06-15",
|
|
23
|
+
},
|
|
24
|
+
"ID-1001": {
|
|
25
|
+
"status": "active",
|
|
26
|
+
"name": "John Doe",
|
|
27
|
+
"role": "Security Specialist",
|
|
28
|
+
"department": "Operations",
|
|
29
|
+
"clearance_level": 3,
|
|
30
|
+
"expiry_date": "2027-01-01",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def lookup_id_card(card_id: str) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
Looks up employee or member profile and status using an ID card number (e.g. 'ID-9021', 'ID-4420').
|
|
38
|
+
"""
|
|
39
|
+
cleaned_id = card_id.strip().upper()
|
|
40
|
+
if cleaned_id in MOCK_ID_DATABASE:
|
|
41
|
+
return {"found": True, "record": MOCK_ID_DATABASE[cleaned_id]}
|
|
42
|
+
return {"found": False, "message": f"No record exists for card ID: {card_id}"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def verify_security_clearance(card_id: str, required_level: int) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Checks if a given ID card has sufficient security clearance level (1 to 5).
|
|
48
|
+
"""
|
|
49
|
+
cleaned_id = card_id.strip().upper()
|
|
50
|
+
if cleaned_id not in MOCK_ID_DATABASE:
|
|
51
|
+
return {"authorized": False, "reason": "ID card not found"}
|
|
52
|
+
|
|
53
|
+
record = MOCK_ID_DATABASE[cleaned_id]
|
|
54
|
+
if record["status"] != "active":
|
|
55
|
+
return {"authorized": False, "reason": f"Card status is {record['status']}"}
|
|
56
|
+
|
|
57
|
+
authorized = record["clearance_level"] >= required_level
|
|
58
|
+
return {
|
|
59
|
+
"authorized": authorized,
|
|
60
|
+
"name": record["name"],
|
|
61
|
+
"clearance_level": record["clearance_level"],
|
|
62
|
+
"required_level": required_level,
|
|
63
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standard built-in system tools (Web browsing, search, time, calculator).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import webbrowser
|
|
6
|
+
import datetime
|
|
7
|
+
import urllib.parse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def open_google(search_query: str = "") -> str:
|
|
11
|
+
"""
|
|
12
|
+
Opens Google in the default web browser. If a search query is provided, searches for it.
|
|
13
|
+
"""
|
|
14
|
+
if search_query:
|
|
15
|
+
query_encoded = urllib.parse.quote(search_query)
|
|
16
|
+
target = f"https://www.google.com/search?q={query_encoded}"
|
|
17
|
+
webbrowser.open(target)
|
|
18
|
+
return f"Opened Google search for '{search_query}' in your web browser."
|
|
19
|
+
else:
|
|
20
|
+
webbrowser.open("https://www.google.com")
|
|
21
|
+
return "Opened Google in your web browser."
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def open_url(url: str) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Opens any website URL in the user's default browser (e.g. 'https://github.com').
|
|
27
|
+
"""
|
|
28
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
|
29
|
+
url = "https://" + url
|
|
30
|
+
webbrowser.open(url)
|
|
31
|
+
return f"Opened website: {url}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_system_time() -> str:
|
|
35
|
+
"""
|
|
36
|
+
Returns the current local date, time, and timezone on this machine.
|
|
37
|
+
"""
|
|
38
|
+
now = datetime.datetime.now()
|
|
39
|
+
return f"Current local time: {now.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def calculate(expression: str) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Evaluates basic mathematical expressions like '25 * 4 + 10' safely.
|
|
45
|
+
"""
|
|
46
|
+
allowed_chars = set("0123456789+-*/(). %")
|
|
47
|
+
if not all(c in allowed_chars for c in expression):
|
|
48
|
+
return "Error: Expression contains unsupported characters."
|
|
49
|
+
try:
|
|
50
|
+
result = eval(expression, {"__builtins__": None}, {})
|
|
51
|
+
return f"Result: {result}"
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return f"Calculation error: {e}"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: happycolab
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: HappyColab: High-speed LLM bridge between Google Colab GPU and local machines with native tool execution
|
|
5
|
+
Author: Aswin Lal S Y
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: openai>=1.30.0
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
|
|
12
|
+
# 🌟 HappyColab (Python Library)
|
|
13
|
+
|
|
14
|
+
**HappyColab** is a high-speed Python client and AI agent framework that lets you run Large Language Models in **Google Colab** (with free GPU acceleration) and call them from any other machine with **native Tool & Function Calling**, real-time streaming, error recovery guides, and interactive chat.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📁 Repository Structure
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
HappyColab/
|
|
22
|
+
├── happycolab/ # 📦 Library Source Code
|
|
23
|
+
│ ├── __init__.py # Exports HappyAgent, HappyClient, tool
|
|
24
|
+
│ ├── agent.py # Agent with auto-schema tool decorator & loop
|
|
25
|
+
│ ├── client.py # Low-level OpenAI-compatible API connector
|
|
26
|
+
│ ├── config.py # Error recovery, guided troubleshooting & configs
|
|
27
|
+
│ ├── cli.py # CLI command runner (happycolab, happy-chat)
|
|
28
|
+
│ └── tools/ # Built-in tool suite
|
|
29
|
+
│ ├── system_tools.py # Open Google, open URL, calculate, local time
|
|
30
|
+
│ └── id_card_tools.py # ID card verification & clearance checks
|
|
31
|
+
│
|
|
32
|
+
├── examples/ # 💡 Ready-to-Run Examples
|
|
33
|
+
│ ├── 01_simple_chat.py # Basic single-turn Q&A
|
|
34
|
+
│ ├── 02_streaming.py # Real-time token streaming
|
|
35
|
+
│ ├── 03_custom_tool.py # Adding a custom tool
|
|
36
|
+
│ ├── 04_full_assistant.py # Interactive assistant with all default tools
|
|
37
|
+
│ └── 05_custom_function_calling.py # All 3 ways to register function calling
|
|
38
|
+
│
|
|
39
|
+
├── colab_server/ # 🚀 Laptop 1 (Colab Server Files)
|
|
40
|
+
│ ├── colab_server_code.py # Copy-pasteable script for Google Colab
|
|
41
|
+
│ └── colab_server_notebook.ipynb # Ready-to-upload Colab notebook
|
|
42
|
+
│
|
|
43
|
+
├── .gitignore
|
|
44
|
+
├── pyproject.toml
|
|
45
|
+
├── setup.py
|
|
46
|
+
└── requirements.txt
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ⚡ Quick Start
|
|
52
|
+
|
|
53
|
+
### 1. Launch Server (Laptop 1 - Google Colab)
|
|
54
|
+
Open [`colab_server/colab_server_notebook.ipynb`](colab_server/colab_server_notebook.ipynb) in Colab with T4 GPU. Copy the public Cloudflare URL (e.g. `https://xxxx.trycloudflare.com`).
|
|
55
|
+
|
|
56
|
+
### 2. Interactive Chat (Laptop 2)
|
|
57
|
+
The library is installed in your Python environment. You can immediately launch the interactive AI chat:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
happycolab
|
|
61
|
+
```
|
|
62
|
+
*(Or alias: `happy-chat`)*
|
|
63
|
+
|
|
64
|
+
If your URL is expired or unreachable, **HappyColab displays a step-by-step troubleshooting guide** and lets you paste your new URL directly into the prompt!
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🛠️ Adding Function Calling (3 Ways)
|
|
69
|
+
|
|
70
|
+
Users can add custom functions for the LLM to call in any of these 3 ways:
|
|
71
|
+
|
|
72
|
+
### Method 1: Using `@agent.tool` Decorator
|
|
73
|
+
Type hints and docstrings are automatically converted into OpenAI function calling schemas:
|
|
74
|
+
```python
|
|
75
|
+
from happycolab import HappyAgent
|
|
76
|
+
|
|
77
|
+
agent = HappyAgent(url="https://your-url.trycloudflare.com")
|
|
78
|
+
|
|
79
|
+
@agent.tool
|
|
80
|
+
def get_user_profile(user_id: str) -> dict:
|
|
81
|
+
"""Fetch employee department and clearance by User ID."""
|
|
82
|
+
return {"user_id": user_id, "name": "Jane Miller", "department": "AI Security"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Method 2: Using the Standalone `@tool` Decorator
|
|
86
|
+
```python
|
|
87
|
+
from happycolab import HappyAgent, tool
|
|
88
|
+
|
|
89
|
+
@tool
|
|
90
|
+
def calculate_tax(amount: float, tax_rate_percent: float = 10.0) -> dict:
|
|
91
|
+
"""Calculate tax and total cost for a transaction."""
|
|
92
|
+
tax = amount * (tax_rate_percent / 100.0)
|
|
93
|
+
return {"subtotal": amount, "tax": tax, "total": amount + tax}
|
|
94
|
+
|
|
95
|
+
agent = HappyAgent(url="https://your-url.trycloudflare.com")
|
|
96
|
+
agent.add_tool(calculate_tax)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Method 3: Registering Regular Python Functions (No Decorators!)
|
|
100
|
+
```python
|
|
101
|
+
def check_server(server_name: str) -> dict:
|
|
102
|
+
"""Check health and ping of an internal server."""
|
|
103
|
+
return {"server": server_name, "status": "ONLINE", "uptime": "99.9%"}
|
|
104
|
+
|
|
105
|
+
agent.add_tool(check_server)
|
|
106
|
+
# Or bulk register:
|
|
107
|
+
# agent.add_tools([func_a, func_b, func_c])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🔀 Flexible Model Switching
|
|
113
|
+
|
|
114
|
+
### In Terminal Chat:
|
|
115
|
+
While chatting inside `happycolab`, you can use slash commands:
|
|
116
|
+
- `/model llama3.2:3b` ➔ Switch model dynamically.
|
|
117
|
+
- `/models` ➔ Query all models installed on your Colab instance.
|
|
118
|
+
- `/url <new_url>` ➔ Change the tunnel URL on the fly.
|
|
119
|
+
- `/tools` ➔ View active tools.
|
|
120
|
+
- `/help` ➔ Show troubleshooting guide.
|
|
121
|
+
|
|
122
|
+
### In Python:
|
|
123
|
+
```python
|
|
124
|
+
agent.set_model("llama3.2:3b")
|
|
125
|
+
print(agent.list_models())
|
|
126
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
happycolab/__init__.py
|
|
5
|
+
happycolab/agent.py
|
|
6
|
+
happycolab/cli.py
|
|
7
|
+
happycolab/client.py
|
|
8
|
+
happycolab/config.py
|
|
9
|
+
happycolab.egg-info/PKG-INFO
|
|
10
|
+
happycolab.egg-info/SOURCES.txt
|
|
11
|
+
happycolab.egg-info/dependency_links.txt
|
|
12
|
+
happycolab.egg-info/entry_points.txt
|
|
13
|
+
happycolab.egg-info/requires.txt
|
|
14
|
+
happycolab.egg-info/top_level.txt
|
|
15
|
+
happycolab/tools/__init__.py
|
|
16
|
+
happycolab/tools/id_card_tools.py
|
|
17
|
+
happycolab/tools/system_tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
happycolab
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "happycolab"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "HappyColab: High-speed LLM bridge between Google Colab GPU and local machines with native tool execution"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [{ name = "Aswin Lal S Y" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"openai>=1.30.0",
|
|
14
|
+
"requests>=2.31.0",
|
|
15
|
+
"pydantic>=2.0.0"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
happycolab = "happycolab.cli:main"
|
|
20
|
+
happy-chat = "happycolab.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
include = ["happycolab*"]
|