diff --git a/novita/agent-harness/cli_anything/novita/core/__init__.py b/novita/agent-harness/cli_anything/novita/core/__init__.py index 6b320d3f8..53351e78b 100644 --- a/novita/agent-harness/cli_anything/novita/core/__init__.py +++ b/novita/agent-harness/cli_anything/novita/core/__init__.py @@ -1,5 +1,6 @@ """Novita CLI core modules.""" +from cli_anything.novita.core.session import ChatSession from cli_anything.novita.utils.novita_backend import ( chat_completion, chat_completion_stream, @@ -8,6 +9,7 @@ from cli_anything.novita.utils.novita_backend import ( ) __all__ = [ + "ChatSession", "chat_completion", "chat_completion_stream", "run_full_workflow", diff --git a/novita/agent-harness/cli_anything/novita/core/session.py b/novita/agent-harness/cli_anything/novita/core/session.py new file mode 100644 index 000000000..d82eac8c5 --- /dev/null +++ b/novita/agent-harness/cli_anything/novita/core/session.py @@ -0,0 +1,65 @@ +"""Lightweight session for chat history management.""" + +from __future__ import annotations + +import json +import os +from datetime import datetime +from pathlib import Path + + +class ChatSession: + """Lightweight session for chat history management.""" + + def __init__(self, session_file: str = None): + self.session_file = session_file or str( + Path.home() / ".cli-anything-novita" / "session.json" + ) + self.messages = [] + self.history = [] + self.max_history = 50 + self.modified = False + if os.path.exists(self.session_file): + try: + with open(self.session_file, "r") as f: + data = json.load(f) + self.messages = data.get("messages", []) + except (json.JSONDecodeError, IOError): + self.messages = [] + + def add_user_message(self, content: str): + self.messages.append({"role": "user", "content": content}) + self.modified = True + self._save() + + def add_assistant_message(self, content: str): + self.messages.append({"role": "assistant", "content": content}) + self.modified = True + self._save() + + def get_messages(self): + return self.messages.copy() + + def clear(self): + self.messages = [] + self.modified = True + self._save() + + def status(self): + return { + "message_count": len(self.messages), + "modified": self.modified, + "session_file": self.session_file, + } + + def _save(self): + os.makedirs(os.path.dirname(self.session_file), exist_ok=True) + with open(self.session_file, "w") as f: + json.dump({"messages": self.messages}, f, indent=2) + + def save_history(self, command: str, result: dict): + self.history.append( + {"command": command, "result": result, "timestamp": str(datetime.now())} + ) + if len(self.history) > self.max_history: + self.history = self.history[-self.max_history :] diff --git a/novita/agent-harness/cli_anything/novita/novita_cli.py b/novita/agent-harness/cli_anything/novita/novita_cli.py index f2be4cbfa..809dfa82c 100644 --- a/novita/agent-harness/cli_anything/novita/novita_cli.py +++ b/novita/agent-harness/cli_anything/novita/novita_cli.py @@ -14,13 +14,12 @@ from __future__ import annotations import sys import os import json -from datetime import datetime import click from pathlib import Path -from typing import Optional sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from cli_anything.novita.core.session import ChatSession from cli_anything.novita.utils.novita_backend import ( get_api_key, load_config, @@ -46,63 +45,6 @@ def get_session(): return _session -class ChatSession: - """Lightweight session for chat history management.""" - - def __init__(self, session_file: str = None): - self.session_file = session_file or str( - Path.home() / ".cli-anything-novita" / "session.json" - ) - self.messages = [] - self.history = [] - self.max_history = 50 - self.modified = False - if os.path.exists(self.session_file): - try: - with open(self.session_file, "r") as f: - data = json.load(f) - self.messages = data.get("messages", []) - except (json.JSONDecodeError, IOError): - self.messages = [] - - def add_user_message(self, content: str): - self.messages.append({"role": "user", "content": content}) - self.modified = True - self._save() - - def add_assistant_message(self, content: str): - self.messages.append({"role": "assistant", "content": content}) - self.modified = True - self._save() - - def get_messages(self): - return self.messages.copy() - - def clear(self): - self.messages = [] - self.modified = True - self._save() - - def status(self): - return { - "message_count": len(self.messages), - "modified": self.modified, - "session_file": self.session_file, - } - - def _save(self): - os.makedirs(os.path.dirname(self.session_file), exist_ok=True) - with open(self.session_file, "w") as f: - json.dump({"messages": self.messages}, f, indent=2) - - def save_history(self, command: str, result: dict): - self.history.append( - {"command": command, "result": result, "timestamp": str(datetime.now())} - ) - if len(self.history) > self.max_history: - self.history = self.history[-self.max_history :] - - def output(data, message: str = ""): if _json_output: click.echo(json.dumps(data, indent=2, default=str)) @@ -170,6 +112,9 @@ def cli(ctx, use_json, api_key_opt, model_opt): """Novita CLI — OpenAI-compatible AI API client.""" global _json_output _json_output = use_json + ctx.ensure_object(dict) + ctx.obj["api_key"] = api_key_opt + ctx.obj["model"] = model_opt if ctx.invoked_subcommand is None: ctx.invoke(repl) @@ -186,11 +131,13 @@ def cli(ctx, use_json, api_key_opt, model_opt): ) @click.option("--temperature", type=float, default=None, help="Temperature (0.0-1.0)") @click.option("--max-tokens", type=int, default=None, help="Maximum tokens to generate") +@click.pass_context @handle_error -def chat(prompt, model_opt=None, temperature=None, max_tokens=None): +def chat(ctx, prompt, model_opt=None, temperature=None, max_tokens=None): """Chat with the Novita API.""" - api_key = get_api_key(api_key_opt) - model = model_opt or "deepseek/deepseek-v3.2" + parent_key = ctx.obj.get("api_key") if ctx.obj else None + api_key = get_api_key(parent_key) + model = model_opt or (ctx.obj.get("model") if ctx.obj else None) or "deepseek/deepseek-v3.2" # Build messages messages = [] @@ -239,11 +186,13 @@ def chat(prompt, model_opt=None, temperature=None, max_tokens=None): ) @click.option("--temperature", type=float, default=None, help="Temperature (0.0-1.0)") @click.option("--max-tokens", type=int, default=None, help="Maximum tokens to generate") +@click.pass_context @handle_error -def stream(prompt, model_opt=None, temperature=None, max_tokens=None): +def stream(ctx, prompt, model_opt=None, temperature=None, max_tokens=None): """Stream chat completion.""" - api_key = get_api_key(api_key_opt) - model = model_opt or "deepseek/deepseek-v3.2" + parent_key = ctx.obj.get("api_key") if ctx.obj else None + api_key = get_api_key(parent_key) + model = model_opt or (ctx.obj.get("model") if ctx.obj else None) or "deepseek/deepseek-v3.2" # Build messages messages = [] @@ -279,30 +228,36 @@ def stream(prompt, model_opt=None, temperature=None, max_tokens=None): output({"content": full_response}, "✓ Stream completed") -@cli.command() +@cli.group() +def session(): + """Session management commands.""" + pass + + +@session.command("status") @handle_error def session_status(): """Show session status.""" - session = get_session() - output(session.status(), "Session status") + s = get_session() + output(s.status(), "Session status") -@cli.command() +@session.command("clear") @handle_error def session_clear(): """Clear session history.""" - session = get_session() - session.clear() + s = get_session() + s.clear() output({"cleared": True}, "Session cleared") -@cli.command() +@session.command("history") @click.option("--limit", "-n", type=int, default=20, help="Maximum entries to show") @handle_error def session_history(limit): """Show command history.""" - session = get_session() - history = session.history[-limit:] + s = get_session() + history = s.history[-limit:] output(history, f"History ({len(history)} entries)") diff --git a/novita/agent-harness/cli_anything/novita/tests/test_core.py b/novita/agent-harness/cli_anything/novita/tests/test_core.py index ff6d89e29..dc7a32938 100644 --- a/novita/agent-harness/cli_anything/novita/tests/test_core.py +++ b/novita/agent-harness/cli_anything/novita/tests/test_core.py @@ -1,6 +1,7 @@ """Unit tests for Novita backend - no API key required (mock HTTP).""" import json +import requests from unittest.mock import patch, MagicMock from cli_anything.novita.utils.novita_backend import ( diff --git a/novita/agent-harness/cli_anything/novita/utils/__init__.py b/novita/agent-harness/cli_anything/novita/utils/__init__.py new file mode 100644 index 000000000..ef7a9d4b4 --- /dev/null +++ b/novita/agent-harness/cli_anything/novita/utils/__init__.py @@ -0,0 +1 @@ +"""Novita utility modules."""