virtuai-cli 0.8.7__tar.gz → 0.8.8__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.
Files changed (23) hide show
  1. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/PKG-INFO +3 -1
  2. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/pyproject.toml +9 -1
  3. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/ask.py +4 -0
  4. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/tui.py +4 -0
  5. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/runner.py +87 -0
  6. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli.egg-info/PKG-INFO +3 -1
  7. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli.egg-info/requires.txt +3 -0
  8. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/README.md +0 -0
  9. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/setup.cfg +0 -0
  10. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/__init__.py +0 -0
  11. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/__init__.py +0 -0
  12. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/command.py +0 -0
  13. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/history.py +0 -0
  14. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/sse.py +0 -0
  15. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/chat/widgets.py +0 -0
  16. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/config.py +0 -0
  17. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/executor.py +0 -0
  18. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/main.py +0 -0
  19. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli/security.py +0 -0
  20. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
  21. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
  22. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
  23. {virtuai_cli-0.8.7 → virtuai_cli-0.8.8}/src/virtuai_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 0.8.7
3
+ Version: 0.8.8
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -20,6 +20,8 @@ Requires-Dist: keyring>=25.0
20
20
  Requires-Dist: typer>=0.12
21
21
  Requires-Dist: rich>=13.0
22
22
  Requires-Dist: textual>=0.86
23
+ Provides-Extra: local
24
+ Requires-Dist: langchain-ollama>=0.2; extra == "local"
23
25
 
24
26
  # VirtuAI CLI
25
27
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "virtuai-cli"
7
- version = "0.8.7"
7
+ version = "0.8.8"
8
8
  description = "Run VirtuAI deep agents on your local machine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -30,6 +30,14 @@ dependencies = [
30
30
  "textual>=0.86",
31
31
  ]
32
32
 
33
+ # Optional: local model inference. Pulls langchain-ollama (+ langchain-core) so
34
+ # the CLI can answer the server's model_generate frames from a locally-running
35
+ # Ollama. Install with: pip install 'virtuai-cli[local]'
36
+ [project.optional-dependencies]
37
+ local = [
38
+ "langchain-ollama>=0.2",
39
+ ]
40
+
33
41
  [project.urls]
34
42
  Homepage = "https://imvituai.com"
35
43
 
@@ -134,6 +134,10 @@ async def _run_ask(
134
134
  err.print(f"[dim]✓ {first[0][:80] if first else ''}[/dim]")
135
135
  else:
136
136
  err.print("[dim]✓ done[/dim]")
137
+ elif etype == "notice" and mode == "stream":
138
+ msg = event.get("message", "")
139
+ color = {"warn": "yellow", "error": "red"}.get(event.get("level"), "dim")
140
+ err.print(f"[{color}]⚠ {msg}[/{color}]")
137
141
  elif etype == "error":
138
142
  msg = event.get("error") or event.get("message") or "unknown"
139
143
  err.print(f"[red]error: {msg}[/red]")
@@ -871,6 +871,10 @@ class ChatApp(App):
871
871
  "\n\n*[response truncated — try raising the agent's max_tokens]*"
872
872
  )
873
873
  turn._truncated_shown = True
874
+ elif etype == "notice":
875
+ msg = event.get("message") or ""
876
+ if msg:
877
+ await turn.append_token(f"\n\n*⚠ {msg}*\n\n")
874
878
  elif etype == "error":
875
879
  msg = event.get("error") or event.get("message") or "unknown error"
876
880
  await turn.append_token(f"\n\n*[error: {msg}]*")
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import platform
9
10
  import sys
10
11
  import time
@@ -13,6 +14,9 @@ from typing import Callable, Optional
13
14
 
14
15
  _AUDIT_DIR = Path.home() / ".virtuai"
15
16
 
17
+ # Local Ollama endpoint used to answer the server's model_generate frames.
18
+ OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
19
+
16
20
  import websockets
17
21
  from rich.console import Console
18
22
 
@@ -101,6 +105,84 @@ def _default_on_status(level: str, msg: str) -> None:
101
105
  console.print(f"[{style}]{msg}[/{style}]" if style else msg)
102
106
 
103
107
 
108
+ async def _probe_ollama() -> dict:
109
+ """Return {"reachable": bool, "models": [name, ...]} for the local Ollama.
110
+
111
+ Reported in the ready frame so the server knows up-front whether it can
112
+ proxy a model turn here or should fall back to a cloud model.
113
+ """
114
+ try:
115
+ import httpx
116
+ except Exception:
117
+ return {"reachable": False, "models": []}
118
+ try:
119
+ async with httpx.AsyncClient(timeout=2.0) as client:
120
+ resp = await client.get(f"{OLLAMA_BASE_URL.rstrip('/')}/api/tags")
121
+ resp.raise_for_status()
122
+ data = resp.json()
123
+ models = [m.get("name", "") for m in data.get("models", []) if m.get("name")]
124
+ return {"reachable": True, "models": models}
125
+ except Exception:
126
+ return {"reachable": False, "models": []}
127
+
128
+
129
+ async def _handle_model_generate(frame: dict, ws, on_status: StatusCallback) -> None:
130
+ """Answer a server model_generate frame from the local Ollama.
131
+
132
+ Streams text deltas as model_token frames, then a terminal model_result
133
+ carrying tool_calls + usage. On any failure replies model_error so the
134
+ server can fall back to a cloud model.
135
+ """
136
+ fid = frame.get("id")
137
+
138
+ async def _error(reason: str) -> None:
139
+ await ws.send(json.dumps({
140
+ "v": 1, "id": fid, "type": "model_error", "error": reason,
141
+ }))
142
+
143
+ try:
144
+ from langchain_ollama import ChatOllama
145
+ from langchain_core.messages import messages_from_dict
146
+ except Exception:
147
+ await _error(
148
+ "local model support not installed — run: pip install 'virtuai-cli[local]'"
149
+ )
150
+ return
151
+
152
+ model = frame.get("model") or ""
153
+ on_status("info", f"local model: {model}")
154
+ try:
155
+ messages = messages_from_dict(frame.get("messages", []))
156
+ tools = frame.get("tools") or []
157
+ kwargs = {"model": model, "base_url": OLLAMA_BASE_URL}
158
+ if frame.get("temperature") is not None:
159
+ kwargs["temperature"] = frame["temperature"]
160
+ if frame.get("num_predict") is not None:
161
+ kwargs["num_predict"] = frame["num_predict"]
162
+ llm = ChatOllama(**kwargs)
163
+ if tools:
164
+ llm = llm.bind_tools(tools)
165
+
166
+ gathered = None
167
+ async for chunk in llm.astream(messages):
168
+ gathered = chunk if gathered is None else gathered + chunk
169
+ text = chunk.content if isinstance(chunk.content, str) else ""
170
+ if text:
171
+ await ws.send(json.dumps({
172
+ "v": 1, "id": fid, "type": "model_token", "content": text,
173
+ }))
174
+
175
+ tool_calls = list(getattr(gathered, "tool_calls", []) or []) if gathered else []
176
+ usage = dict(getattr(gathered, "usage_metadata", None) or {}) if gathered else {}
177
+ await ws.send(json.dumps({
178
+ "v": 1, "id": fid, "type": "model_result",
179
+ "tool_calls": tool_calls, "usage": usage,
180
+ }))
181
+ except Exception as exc:
182
+ logger.warning("model_generate failed: %s", exc)
183
+ await _error(str(exc))
184
+
185
+
104
186
  async def _handle_frame(
105
187
  frame: dict,
106
188
  workdir: Path,
@@ -124,11 +206,16 @@ async def _handle_frame(
124
206
  "workdir": str(workdir),
125
207
  "local_skills": _read_local_skills(workdir),
126
208
  "local_memory": _read_local_memory(workdir),
209
+ "ollama": await _probe_ollama(),
127
210
  }
128
211
  await ws.send(json.dumps(reply))
129
212
  on_status("info", f"Connected. Workdir: {workdir}")
130
213
  return
131
214
 
215
+ if ftype == "model_generate":
216
+ await _handle_model_generate(frame, ws, on_status)
217
+ return
218
+
132
219
  if ftype == "ping":
133
220
  await ws.send(json.dumps({"v": 1, "id": fid, "type": "pong", "ts": time.time()}))
134
221
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 0.8.7
3
+ Version: 0.8.8
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -20,6 +20,8 @@ Requires-Dist: keyring>=25.0
20
20
  Requires-Dist: typer>=0.12
21
21
  Requires-Dist: rich>=13.0
22
22
  Requires-Dist: textual>=0.86
23
+ Provides-Extra: local
24
+ Requires-Dist: langchain-ollama>=0.2; extra == "local"
23
25
 
24
26
  # VirtuAI CLI
25
27
 
@@ -6,3 +6,6 @@ keyring>=25.0
6
6
  typer>=0.12
7
7
  rich>=13.0
8
8
  textual>=0.86
9
+
10
+ [local]
11
+ langchain-ollama>=0.2
File without changes
File without changes