regcode 0.2.0__py3-none-any.whl → 0.4.0__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.
regcode/__init__.py CHANGED
@@ -2,4 +2,4 @@ from regcode.config import Config
2
2
  from regcode.main import Agent, AgentStatus, ToolCall
3
3
 
4
4
  __all__ = ["Agent", "Config", "AgentStatus", "ToolCall"]
5
- __version__ = "0.1.0"
5
+ __version__ = "0.4.0"
regcode/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ from pathlib import Path
2
3
 
3
4
  import click
4
5
  import yaml
@@ -42,7 +43,10 @@ def cli(ctx):
42
43
  )
43
44
  def chat(model, config_path, stream, full, no_color):
44
45
  """Chat with the agent using a Textual TUI with sticky bottom bar."""
45
- config = regcode.Config.load(config_path)
46
+ # Pass None if the file doesn't exist so Config.load falls back
47
+ # to ~/.regcode/config.yaml (matching the TUI's expected behaviour).
48
+ _effective_path = config_path if Path(config_path).exists() else None
49
+ config = regcode.Config.load(_effective_path)
46
50
  if model:
47
51
  config.provider.model = model
48
52
  if full:
@@ -57,6 +61,7 @@ def chat(model, config_path, stream, full, no_color):
57
61
  status_callback=ui.handle_status,
58
62
  result_callback=ui.handle_result,
59
63
  context_callback=ui.set_context_progress,
64
+ cost_callback=ui.handle_cost,
60
65
  )
61
66
 
62
67
  # Attach agent to the Textual app
regcode/config.py CHANGED
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import os
2
3
  from pathlib import Path
3
4
  from textwrap import dedent
@@ -40,9 +41,18 @@ default_system_prompt = dedent(
40
41
  """
41
42
  )
42
43
 
44
+ logger = logging.getLogger(__name__)
43
45
 
44
46
  class AgentConfig(BaseModel):
45
- model_config = ConfigDict(extra="allow")
47
+ """Configuration specific to the agent's behavior (not provider params).
48
+
49
+ Model, temperature, max_tokens belong in ProviderConfig where
50
+ litellm.completion expects them. AgentConfig only holds compaction
51
+ and context-window tuning — passing model params through this object
52
+ caused conflicts with different providers.
53
+ """
54
+
55
+ model_config = ConfigDict(extra="forbid")
46
56
  context_window: int = 128000
47
57
  compaction_threshold: float = 0.8 # Compact when context > 80% of window
48
58
  enable_compaction: bool = True
@@ -79,11 +89,30 @@ class SandboxConfig(BaseModel):
79
89
  type_check: bool = True
80
90
  allowed_imports: list[str] = []
81
91
 
92
+ class TracingConfig(BaseModel):
93
+ """Configuration for tracing and logging."""
94
+
95
+ model_config = ConfigDict(extra="allow")
96
+ enable_tracing: bool = False
97
+ mlflow_url: str | None = None
98
+ mlflow_experiment_name: str | None = None
99
+ mlflow_async: bool = True
100
+ mlflow_requires_auth: bool = True
101
+ mlflow_username: str | None = None
102
+ mlflow_password: str | None = None
103
+
104
+ def tracing_enabled(self) -> bool:
105
+ """Check if tracing is fully configured and enabled."""
106
+ return (
107
+ self.enable_tracing
108
+ and self.mlflow_url is not None
109
+ )
82
110
 
83
111
  class Config(BaseModel):
84
112
  model_config = ConfigDict(extra="allow")
85
113
 
86
114
  agent: AgentConfig = AgentConfig()
115
+ tracing: TracingConfig = TracingConfig()
87
116
  provider: ProviderConfig = ProviderConfig()
88
117
  system_prompt: str = default_system_prompt
89
118
  tools: dict = {}
@@ -94,15 +123,29 @@ class Config(BaseModel):
94
123
  @classmethod
95
124
  def load(cls, path: str | None = None) -> "Config":
96
125
  # get default home dir for user first
126
+ _path = None
97
127
  if path is None:
98
- alt_config_path = Path.home() / ".regcode" / "config.yaml"
99
- if alt_config_path.exists():
100
- _path = alt_config_path
101
- else:
102
- _path = Path("config.yaml")
128
+ possible_paths = [
129
+ Path("./config.yaml"), Path.home() / ".regcode" / "config.yaml"
130
+ ]
131
+ for p in possible_paths:
132
+ if p.exists():
133
+ logger.info(f"Loading from config at {p}")
134
+ _path = p
135
+ break
136
+ if _path is None:
137
+ logger.warning(
138
+ "No config file found, using default config, "
139
+ "Use `regcode configure` to configure."
140
+ )
141
+ return cls()
103
142
  else:
104
143
  _path = Path(path)
105
144
  if not _path.exists():
145
+ logger.warning(
146
+ f"Config file {path} not found, using default config, "
147
+ "Use `regcode configure` to configure."
148
+ )
106
149
  return cls()
107
150
  with open(_path) as f:
108
151
  data = yaml.safe_load(f)
@@ -129,17 +172,6 @@ class Config(BaseModel):
129
172
  result = self.provider.model_dump()
130
173
  if extra_kwargs:
131
174
  result.update(extra_kwargs)
132
- # Agent-level config overrides provider defaults for known keys
133
- if self.agent.context_window is not None:
134
- result["context_window"] = self.agent.context_window
135
- if hasattr(self.agent, "compaction_threshold"):
136
- result["compaction_threshold"] = self.agent.compaction_threshold
137
- if hasattr(self.agent, "enable_compaction"):
138
- result["enable_compaction"] = self.agent.enable_compaction
139
- # Apply agent-level extra fields (e.g. model, max_tokens, temperature
140
- # from YAML agent section) as overrides over provider defaults
141
- if hasattr(self.agent, "model_extra") and self.agent.model_extra:
142
- result.update(self.agent.model_extra)
143
175
  return result
144
176
 
145
177
 
regcode/main.py CHANGED
@@ -19,7 +19,9 @@ from regcode.tools.base import ToolResult
19
19
  from regcode.tools.builtins import create_default_tools
20
20
  from regcode.tools.registry import ToolRegistry
21
21
  from regcode.tools.review_notes import AddReviewNoteTool, ReadReviewNotesTool
22
+ from regcode.tracing import setup_mlflow_tracing
22
23
 
24
+ setup_mlflow_tracing()
23
25
 
24
26
  class _SafeEncoder(json.JSONEncoder):
25
27
  def default(self, o):
@@ -33,6 +35,7 @@ class AgentStatus(Enum):
33
35
 
34
36
  TOOL_CALL = "tool_call"
35
37
  TOOL_RESULT = "tool_result"
38
+ COMPACTION_START = "compaction_start"
36
39
  IDLE = "idle"
37
40
  BUDGET_EXHAUSTED = "budget_exhausted"
38
41
 
@@ -63,6 +66,7 @@ class Agent:
63
66
  )
64
67
  | None = None,
65
68
  context_callback: Callable[[int, int], None] | None = None,
69
+ cost_callback: Callable[[float, int | None], None] | None = None,
66
70
  ) -> None:
67
71
  self.config = config or Config.load()
68
72
  self.system_prompt = self.config.system_prompt
@@ -71,10 +75,13 @@ class Agent:
71
75
  self._status_callback = status_callback
72
76
  self._result_callback = result_callback
73
77
  self._context_callback = context_callback
78
+ self._cost_callback = cost_callback
74
79
  self._allowed_dir = directory
75
80
  self._tool_budget = self.config.tool_budget
76
81
  self._tool_budget_remaining = self._tool_budget
77
82
  self._tool_budget_exhausted = False
83
+ self._cumulative_cost: float = 0.0
84
+ self._total_tokens: int = 0
78
85
 
79
86
  # Context window manager
80
87
  self.context_manager = ConversationManager()
@@ -212,6 +219,8 @@ class Agent:
212
219
  self.config.agent.enable_compaction
213
220
  and self.context_manager.should_compact()
214
221
  ):
222
+ if self._status_callback is not None:
223
+ self._status_callback(AgentStatus.COMPACTION_START, None)
215
224
  self.context_manager.compact()
216
225
  messages = self.context_manager.get_messages()
217
226
 
@@ -249,6 +258,9 @@ class Agent:
249
258
 
250
259
  response = litellm.completion(**completion_kwargs)
251
260
 
261
+ # Track cost and tokens from this response
262
+ self._track_response_cost(response)
263
+
252
264
  choice = response.choices[0] # type: ignore
253
265
  message_obj = choice.message
254
266
  response_message = {
@@ -510,6 +522,9 @@ class Agent:
510
522
 
511
523
  response = litellm.completion(**completion_kwargs)
512
524
 
525
+ # Track cost and tokens from this response
526
+ self._track_response_cost(response)
527
+
513
528
  # Stream the response — accumulate text and tool_calls
514
529
  # across ALL chunks, not just the last one.
515
530
  full_text = ""
@@ -750,6 +765,36 @@ class Agent:
750
765
  except Exception:
751
766
  pass
752
767
 
768
+ def _track_response_cost(self, response) -> None:
769
+ """Track cost and token usage from a litellm completion response."""
770
+ if self._cost_callback is None:
771
+ return
772
+ try:
773
+ # Try to get cost from _hidden_params first
774
+ cost = 0.0
775
+ if hasattr(response, "_hidden_params") and response._hidden_params:
776
+ cost = response._hidden_params.get("response_cost", 0.0) or 0.0
777
+ # Fallback: compute from usage if available
778
+ if cost == 0.0 and hasattr(response, "usage") and response.usage:
779
+ usage = response.usage
780
+ prompt_tokens = getattr(usage, "prompt_tokens", 0) or 0
781
+ completion_tokens = getattr(usage, "completion_tokens", 0) or 0
782
+ total_tokens = getattr(usage, "total_tokens", 0) or 0
783
+ self._total_tokens += total_tokens
784
+ # Use litellm's cost calculator for the current model
785
+ try:
786
+ cost = litellm.completion_cost(
787
+ model=self.config.provider.model,
788
+ prompt_tokens=prompt_tokens,
789
+ completion_tokens=completion_tokens,
790
+ )
791
+ except (KeyError, Exception):
792
+ cost = 0.0
793
+ self._cumulative_cost += cost
794
+ self._cost_callback(self._cumulative_cost, self._total_tokens)
795
+ except Exception:
796
+ pass
797
+
753
798
  def reset(self) -> None:
754
799
  """Clear conversation history."""
755
800
  self._history = []
regcode/tools/__init__.py CHANGED
@@ -4,7 +4,7 @@ from regcode.tools.base import BaseTool, ToolParam, ToolResult
4
4
  from regcode.tools.builtins import create_default_tools
5
5
  from regcode.tools.registry import ToolRegistry
6
6
 
7
- __version__ = "0.2.0"
7
+ __version__ = "0.3.0"
8
8
 
9
9
  __all__ = [
10
10
  "BaseTool",