regcode 0.3.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
@@ -61,6 +61,7 @@ def chat(model, config_path, stream, full, no_color):
61
61
  status_callback=ui.handle_status,
62
62
  result_callback=ui.handle_result,
63
63
  context_callback=ui.set_context_progress,
64
+ cost_callback=ui.handle_cost,
64
65
  )
65
66
 
66
67
  # Attach agent to the Textual app
regcode/config.py CHANGED
@@ -44,7 +44,15 @@ default_system_prompt = dedent(
44
44
  logger = logging.getLogger(__name__)
45
45
 
46
46
  class AgentConfig(BaseModel):
47
- 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")
48
56
  context_window: int = 128000
49
57
  compaction_threshold: float = 0.8 # Compact when context > 80% of window
50
58
  enable_compaction: bool = True
@@ -81,11 +89,30 @@ class SandboxConfig(BaseModel):
81
89
  type_check: bool = True
82
90
  allowed_imports: list[str] = []
83
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
+ )
84
110
 
85
111
  class Config(BaseModel):
86
112
  model_config = ConfigDict(extra="allow")
87
113
 
88
114
  agent: AgentConfig = AgentConfig()
115
+ tracing: TracingConfig = TracingConfig()
89
116
  provider: ProviderConfig = ProviderConfig()
90
117
  system_prompt: str = default_system_prompt
91
118
  tools: dict = {}
@@ -96,19 +123,29 @@ class Config(BaseModel):
96
123
  @classmethod
97
124
  def load(cls, path: str | None = None) -> "Config":
98
125
  # get default home dir for user first
126
+ _path = None
99
127
  if path is None:
100
- logger.warning(
101
- "No config path provided, looking for ~/.regcode/config.yaml"
102
- )
103
- alt_config_path = Path.home() / ".regcode" / "config.yaml"
104
- if alt_config_path.exists():
105
- logger.warning(f"Loading from config at {alt_config_path}")
106
- _path = alt_config_path
107
- else:
108
- _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()
109
142
  else:
110
143
  _path = Path(path)
111
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
+ )
112
149
  return cls()
113
150
  with open(_path) as f:
114
151
  data = yaml.safe_load(f)
@@ -135,17 +172,6 @@ class Config(BaseModel):
135
172
  result = self.provider.model_dump()
136
173
  if extra_kwargs:
137
174
  result.update(extra_kwargs)
138
- # Agent-level config overrides provider defaults for known keys
139
- if self.agent.context_window is not None:
140
- result["context_window"] = self.agent.context_window
141
- if hasattr(self.agent, "compaction_threshold"):
142
- result["compaction_threshold"] = self.agent.compaction_threshold
143
- if hasattr(self.agent, "enable_compaction"):
144
- result["enable_compaction"] = self.agent.enable_compaction
145
- # Apply agent-level extra fields (e.g. model, max_tokens, temperature
146
- # from YAML agent section) as overrides over provider defaults
147
- if hasattr(self.agent, "model_extra") and self.agent.model_extra:
148
- result.update(self.agent.model_extra)
149
175
  return result
150
176
 
151
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 = []