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.
@@ -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,3 @@
1
+ [console_scripts]
2
+ happy-chat = happycolab.cli:main
3
+ happycolab = happycolab.cli:main
@@ -0,0 +1,3 @@
1
+ openai>=1.30.0
2
+ requests>=2.31.0
3
+ pydantic>=2.0.0
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()