diff --git a/Modelfile b/Modelfile new file mode 100644 index 0000000..e6e02a2 --- /dev/null +++ b/Modelfile @@ -0,0 +1,9 @@ +FROM kimi-k2.7-code:cloud + +SYSTEM """You are an expert data scientist and Python/Pandas assistant. +You help users understand and write Pandas code for real-life data analysis tasks. +When asked about code, provide clear, working examples with brief explanations. +Focus on practical, idiomatic Pandas usage.""" + +PARAMETER temperature 0.2 +PARAMETER num_ctx 16384 diff --git a/claude b/claude new file mode 100755 index 0000000..c00178a --- /dev/null +++ b/claude @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +Ollama-backed Claude-like assistant launcher. + +Usage: + ollama launch claude --model kimi-k2.7-code:cloud + ./claude --model kimi-k2.7-code:cloud +""" + +import sys +import argparse +import json +import os +import urllib.request +import urllib.error + +OLLAMA_BASE_URL = os.environ.get("OLLAMA_HOST", "http://localhost:11434") + +SYSTEM_PROMPT = """You are an expert data scientist and Python/Pandas assistant. +You help users understand and write Pandas code for real-life data analysis tasks. +When asked about code, provide clear, working examples with brief explanations. +Focus on practical, idiomatic Pandas usage.""" + + +def check_ollama_running(): + try: + req = urllib.request.urlopen(f"{OLLAMA_BASE_URL}/api/tags", timeout=3) + return req.status == 200 + except Exception: + return False + + +def list_local_models(): + try: + req = urllib.request.urlopen(f"{OLLAMA_BASE_URL}/api/tags", timeout=5) + data = json.loads(req.read()) + return [m["name"] for m in data.get("models", [])] + except Exception: + return [] + + +def stream_chat(model, messages): + payload = json.dumps({ + "model": model, + "messages": messages, + "stream": True, + }).encode() + + req = urllib.request.Request( + f"{OLLAMA_BASE_URL}/api/chat", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + + full_response = "" + try: + with urllib.request.urlopen(req) as resp: + for line in resp: + line = line.strip() + if not line: + continue + chunk = json.loads(line) + token = chunk.get("message", {}).get("content", "") + if token: + print(token, end="", flush=True) + full_response += token + if chunk.get("done"): + break + except urllib.error.URLError as e: + print(f"\nError communicating with Ollama: {e}", file=sys.stderr) + sys.exit(1) + + print() + return full_response + + +def pull_model(model): + print(f"Pulling model '{model}'... (this may take a while)") + payload = json.dumps({"name": model, "stream": True}).encode() + req = urllib.request.Request( + f"{OLLAMA_BASE_URL}/api/pull", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req) as resp: + for line in resp: + line = line.strip() + if not line: + continue + chunk = json.loads(line) + status = chunk.get("status", "") + if status: + print(f"\r{status}", end="", flush=True) + if chunk.get("status") == "success": + break + print("\nModel ready.") + except urllib.error.URLError as e: + print(f"\nFailed to pull model: {e}", file=sys.stderr) + sys.exit(1) + + +def repl(model): + print(f"Claude (via Ollama / {model})") + print("Type your message and press Enter. Use Ctrl+C or 'exit' to quit.\n") + + messages = [{"role": "system", "content": SYSTEM_PROMPT}] + + while True: + try: + user_input = input("You: ").strip() + except (KeyboardInterrupt, EOFError): + print("\nGoodbye.") + break + + if not user_input: + continue + if user_input.lower() in ("exit", "quit", "/exit", "/quit"): + print("Goodbye.") + break + + messages.append({"role": "user", "content": user_input}) + print("Assistant: ", end="", flush=True) + response = stream_chat(model, messages) + messages.append({"role": "assistant", "content": response}) + + +def main(): + parser = argparse.ArgumentParser( + prog="claude", + description="Launch a Claude-like assistant backed by a local Ollama model.", + ) + parser.add_argument( + "--model", + default="kimi-k2.7-code:cloud", + help="Ollama model to use (default: kimi-k2.7-code:cloud)", + ) + parser.add_argument( + "--list-models", + action="store_true", + help="List locally available Ollama models and exit.", + ) + parser.add_argument( + "--pull", + action="store_true", + help="Pull the model before starting (download if not present).", + ) + args = parser.parse_args() + + if not check_ollama_running(): + print( + "Ollama is not running or not installed.\n" + "Install it from https://ollama.com and run: ollama serve\n" + "Then re-run this command.", + file=sys.stderr, + ) + sys.exit(1) + + if args.list_models: + models = list_local_models() + if models: + print("Available models:") + for m in models: + print(f" {m}") + else: + print("No models found locally. Run with --pull to download one.") + return + + local_models = list_local_models() + model = args.model + + if model not in local_models: + if args.pull: + pull_model(model) + else: + print( + f"Model '{model}' is not available locally.\n" + f"Run with --pull to download it, or choose from: {local_models or ['(none)']}" + ) + sys.exit(1) + + repl(model) + + +if __name__ == "__main__": + main()