tamfis-code 0.2.3__py3-none-any.whl

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,117 @@
1
+ """Offline/local chat: talk to a directly-configured LLM provider with no
2
+ TamfisGPT backend, account, or network round-trip to tamgpt6 at all.
3
+
4
+ This is a deliberately separate, second model-routing path from the Remote
5
+ Workspace backend's Tier IV orchestration -- see providers.py's module
6
+ docstring. It exists for genuine offline/local use (Ollama needs no API key
7
+ and runs fully on-device); it does not replicate the backend's model
8
+ health/fallback/quota-tracking, and it never exposes mutating tools (see
9
+ local_tools.py). Real conversational value only: read-only repo inspection
10
+ via tool-calling, no file writes, no shell execution, no approval gate
11
+ (nothing here can mutate anything, so none is needed).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import Any, AsyncIterator, Dict, List, Optional
17
+
18
+ from rich.console import Console
19
+
20
+ from .local_tools import READ_ONLY_TOOL_SCHEMAS, LocalReadOnlyTools
21
+ from .providers import ProviderManager, ProviderType
22
+
23
+ MAX_TOOL_ROUNDS = 5
24
+
25
+ _PROVIDER_ALIASES = {
26
+ "hf": ProviderType.HF, "huggingface": ProviderType.HF,
27
+ "nvidia": ProviderType.NVIDIA, "nvidia_nim": ProviderType.NVIDIA,
28
+ "or": ProviderType.OPENROUTER, "openrouter": ProviderType.OPENROUTER,
29
+ "ollama": ProviderType.OLLAMA,
30
+ "auto": ProviderType.AUTO,
31
+ }
32
+
33
+
34
+ def resolve_provider_type(name: Optional[str]) -> ProviderType:
35
+ key = (name or "auto").strip().lower()
36
+ if key not in _PROVIDER_ALIASES:
37
+ raise ValueError(f"Unknown local provider: {name!r}. Choose one of: {', '.join(sorted(_PROVIDER_ALIASES))}")
38
+ return _PROVIDER_ALIASES[key]
39
+
40
+
41
+ async def run_local_turn(
42
+ manager: ProviderManager,
43
+ provider: ProviderType,
44
+ messages: List[Dict[str, Any]],
45
+ model: Optional[str],
46
+ console: Console,
47
+ *,
48
+ use_tools: bool = True,
49
+ ) -> str:
50
+ """Run one user turn to completion, resolving any read-only tool calls
51
+ the model requests along the way. Returns the final assistant text.
52
+
53
+ Non-streaming by design: accumulating partial tool_call fragments across
54
+ a streamed response is real complexity this conservative first pass
55
+ intentionally skips (see module docstring's scope note) -- a user still
56
+ sees the final answer, just not token-by-token for turns with tool use.
57
+ """
58
+ tools_client = LocalReadOnlyTools() if use_tools else None
59
+ working_messages = list(messages)
60
+
61
+ for _ in range(MAX_TOOL_ROUNDS):
62
+ client = manager.get_client(provider)
63
+ if not client:
64
+ raise RuntimeError(f"Provider {provider.value} is not available (no client / no valid credentials).")
65
+ config = manager.PROVIDERS[provider if provider != ProviderType.AUTO else manager._select_best_provider()]
66
+ resolved_model = model or config.default_model
67
+
68
+ kwargs: Dict[str, Any] = {}
69
+ if tools_client is not None:
70
+ kwargs["tools"] = READ_ONLY_TOOL_SCHEMAS
71
+ kwargs["tool_choice"] = "auto"
72
+
73
+ response = await client.chat.completions.create(
74
+ model=resolved_model, messages=working_messages, stream=False,
75
+ temperature=0.2, max_tokens=4096, **kwargs,
76
+ )
77
+ if not response.choices:
78
+ return ""
79
+ choice = response.choices[0]
80
+ message = choice.message
81
+ tool_calls = getattr(message, "tool_calls", None)
82
+
83
+ if not tool_calls:
84
+ return message.content or ""
85
+
86
+ working_messages.append({
87
+ "role": "assistant", "content": message.content or "",
88
+ "tool_calls": [
89
+ {"id": tc.id, "type": "function",
90
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
91
+ for tc in tool_calls
92
+ ],
93
+ })
94
+ for tc in tool_calls:
95
+ console.print(f"[dim]· {tc.function.name}({tc.function.arguments})[/dim]")
96
+ try:
97
+ arguments = json.loads(tc.function.arguments or "{}")
98
+ result = await tools_client.call(tc.function.name, arguments)
99
+ content = json.dumps(result, default=str)
100
+ except Exception as exc:
101
+ content = json.dumps({"error": str(exc)})
102
+ working_messages.append({"role": "tool", "tool_call_id": tc.id, "content": content})
103
+
104
+ return "(Stopped after exhausting the local tool-call round limit -- try a narrower question.)"
105
+
106
+
107
+ async def stream_local_turn(
108
+ manager: ProviderManager,
109
+ provider: ProviderType,
110
+ messages: List[Dict[str, Any]],
111
+ model: Optional[str],
112
+ ) -> AsyncIterator[str]:
113
+ """Plain streaming, no tools -- used when the caller already knows this
114
+ turn needs no repo inspection (kept separate from run_local_turn so the
115
+ common single-shot/no-tools case still gets real token streaming)."""
116
+ async for chunk in manager.chat_completion(provider, messages, model=model, stream=True):
117
+ yield chunk
@@ -0,0 +1,93 @@
1
+ """Read-only local filesystem tools for tamfis-code's offline/local chat mode.
2
+
3
+ Local mode has no server-side approval gate, safety classifier, or mutation-
4
+ ledger/audit trail (that infrastructure lives in tamgpt6's Remote Workspace
5
+ backend for good reason). Without it, offering anything that mutates state --
6
+ writing files, running shell commands -- would be a real safety regression,
7
+ not a feature. This module therefore wraps only the read-only subset of
8
+ MCPServer's native tools (tamfis_code/mcp.py): read_file, list_directory,
9
+ search_code, get_git_info. write_file/execute_command/browser are
10
+ intentionally never exposed here.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Dict, List
15
+
16
+ from .mcp import MCPServer
17
+
18
+ READ_ONLY_TOOL_SCHEMAS: List[Dict[str, Any]] = [
19
+ {
20
+ "type": "function",
21
+ "function": {
22
+ "name": "read_file",
23
+ "description": "Read the contents of a local file (read-only).",
24
+ "parameters": {
25
+ "type": "object",
26
+ "properties": {"path": {"type": "string", "description": "File path"}},
27
+ "required": ["path"],
28
+ },
29
+ },
30
+ },
31
+ {
32
+ "type": "function",
33
+ "function": {
34
+ "name": "list_directory",
35
+ "description": "List the contents of a local directory (read-only).",
36
+ "parameters": {
37
+ "type": "object",
38
+ "properties": {"path": {"type": "string", "description": "Directory path", "default": "."}},
39
+ },
40
+ },
41
+ },
42
+ {
43
+ "type": "function",
44
+ "function": {
45
+ "name": "search_code",
46
+ "description": "Search local file contents using ripgrep (read-only).",
47
+ "parameters": {
48
+ "type": "object",
49
+ "properties": {
50
+ "query": {"type": "string", "description": "Search pattern"},
51
+ "path": {"type": "string", "description": "Search directory", "default": "."},
52
+ "file_pattern": {"type": "string", "description": "Optional glob filter, e.g. '*.py'"},
53
+ },
54
+ "required": ["query"],
55
+ },
56
+ },
57
+ },
58
+ {
59
+ "type": "function",
60
+ "function": {
61
+ "name": "get_git_info",
62
+ "description": "Read local git repository metadata: branch, latest commit, dirty status (read-only).",
63
+ "parameters": {
64
+ "type": "object",
65
+ "properties": {"path": {"type": "string", "description": "Repository path", "default": "."}},
66
+ },
67
+ },
68
+ },
69
+ ]
70
+
71
+ _READ_ONLY_NAMES = {schema["function"]["name"] for schema in READ_ONLY_TOOL_SCHEMAS}
72
+
73
+
74
+ class LocalReadOnlyTools:
75
+ """Dispatches only read_file/list_directory/search_code/get_git_info.
76
+
77
+ Any other tool name (in particular write_file/execute_command/browser,
78
+ which MCPServer also implements) is refused -- this is the enforcement
79
+ point for local mode's read-only boundary, not just documentation of it.
80
+ """
81
+
82
+ def __init__(self) -> None:
83
+ self._server = MCPServer()
84
+
85
+ async def call(self, name: str, arguments: Dict[str, Any]) -> Any:
86
+ if name not in _READ_ONLY_NAMES:
87
+ raise ValueError(
88
+ f"'{name}' is not available in local/offline mode -- only read-only tools "
89
+ f"({', '.join(sorted(_READ_ONLY_NAMES))}) are exposed without a server-side "
90
+ "approval gate and audit trail."
91
+ )
92
+ handler = getattr(self._server, f"_{name}")
93
+ return await handler(**arguments)