copper-pilot-cli 0.1.1__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.
- copper_pilot_cli/__init__.py +37 -0
- copper_pilot_cli/__main__.py +6 -0
- copper_pilot_cli/_upstream/__init__.py +0 -0
- copper_pilot_cli/_upstream/dcode_0_1_69/PROVENANCE.json +46 -0
- copper_pilot_cli/_upstream/dcode_0_1_69/__init__.py +0 -0
- copper_pilot_cli/_upstream/dcode_0_1_69/diff_utils.py +222 -0
- copper_pilot_cli/_version.py +32 -0
- copper_pilot_cli/clipboard.py +107 -0
- copper_pilot_cli/copper_api.py +24 -0
- copper_pilot_cli/copper_app.py +1071 -0
- copper_pilot_cli/copper_auth.py +232 -0
- copper_pilot_cli/copper_config.py +62 -0
- copper_pilot_cli/copper_features.py +263 -0
- copper_pilot_cli/copper_graph.py +178 -0
- copper_pilot_cli/copper_hooks.py +49 -0
- copper_pilot_cli/copper_main.py +345 -0
- copper_pilot_cli/copper_preferences.py +85 -0
- copper_pilot_cli/copper_presentation.py +325 -0
- copper_pilot_cli/copper_protocol.py +421 -0
- copper_pilot_cli/copper_theme.py +34 -0
- copper_pilot_cli/copper_tools.py +23 -0
- copper_pilot_cli/copper_update.py +32 -0
- copper_pilot_cli/copper_widgets.py +1224 -0
- copper_pilot_cli/copper_workspace.py +151 -0
- copper_pilot_cli/deepagents_tools.py +556 -0
- copper_pilot_cli/diagnostics.py +55 -0
- copper_pilot_cli/langchain.py +215 -0
- copper_pilot_cli/media_utils.py +626 -0
- copper_pilot_cli/py.typed +0 -0
- copper_pilot_cli/sessions.py +1578 -0
- copper_pilot_cli/textual_patches.py +35 -0
- copper_pilot_cli-0.1.1.data/data/share/doc/copper-pilot-cli/NOTICE +13 -0
- copper_pilot_cli-0.1.1.data/data/share/doc/copper-pilot-cli/UPSTREAM.md +44 -0
- copper_pilot_cli-0.1.1.dist-info/METADATA +211 -0
- copper_pilot_cli-0.1.1.dist-info/RECORD +39 -0
- copper_pilot_cli-0.1.1.dist-info/WHEEL +4 -0
- copper_pilot_cli-0.1.1.dist-info/entry_points.txt +3 -0
- copper_pilot_cli-0.1.1.dist-info/licenses/LICENSE +22 -0
- copper_pilot_cli-0.1.1.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Rotating, content-redacted local diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from logging.handlers import RotatingFileHandler
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from copper_pilot_cli.copper_config import paths
|
|
11
|
+
|
|
12
|
+
_SECRET_PATTERNS = (
|
|
13
|
+
re.compile(r"cf_live_[A-Za-z0-9._~-]+"),
|
|
14
|
+
re.compile(r"(?i)(authorization[\"']?\s*[:=]\s*[\"']?bearer\s+)[^\s\"']+"),
|
|
15
|
+
re.compile(r"(?i)(flow_token[\"']?\s*[:=]\s*[\"']?)[^\s&\"']+"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def redact(value: object) -> str:
|
|
20
|
+
"""Remove known credentials and omit multiline user/tool content."""
|
|
21
|
+
text = str(value)
|
|
22
|
+
for pattern in _SECRET_PATTERNS:
|
|
23
|
+
text = pattern.sub(r"\1[REDACTED]" if pattern.groups else "[REDACTED]", text)
|
|
24
|
+
if "\n" in text:
|
|
25
|
+
first, *rest = text.splitlines()
|
|
26
|
+
text = f"{first} … [{len(rest)} lines omitted]"
|
|
27
|
+
return text[:2_000]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RedactingFormatter(logging.Formatter):
|
|
31
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
32
|
+
copied = logging.makeLogRecord(record.__dict__.copy())
|
|
33
|
+
copied.msg = redact(copied.msg)
|
|
34
|
+
copied.args = tuple(redact(item) for item in copied.args) if copied.args else ()
|
|
35
|
+
return super().format(copied)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def configure_diagnostics() -> None:
|
|
39
|
+
"""Configure a bounded local log without console or telemetry handlers."""
|
|
40
|
+
logger = logging.getLogger("copper_pilot_cli")
|
|
41
|
+
if any(getattr(handler, "_copper_diagnostics", False) for handler in logger.handlers):
|
|
42
|
+
return
|
|
43
|
+
directory = paths().logs
|
|
44
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
handler = RotatingFileHandler(
|
|
46
|
+
directory / "copper-pilot.log",
|
|
47
|
+
maxBytes=2 * 1024 * 1024,
|
|
48
|
+
backupCount=3,
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
cast(Any, handler)._copper_diagnostics = True
|
|
52
|
+
handler.setFormatter(RedactingFormatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
|
|
53
|
+
logger.addHandler(handler)
|
|
54
|
+
logger.setLevel(logging.INFO)
|
|
55
|
+
logger.propagate = False
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""LangChain and Deep Agents adapters for the hosted CopperPilot agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage, HumanMessage
|
|
13
|
+
from langchain_core.runnables import Runnable, RunnableConfig
|
|
14
|
+
|
|
15
|
+
from copper_pilot_cli.copper_auth import DeviceCredential, load_credential
|
|
16
|
+
from copper_pilot_cli.copper_protocol import (
|
|
17
|
+
ChatMessage,
|
|
18
|
+
ChatRequest,
|
|
19
|
+
CopperEvent,
|
|
20
|
+
CopperMode,
|
|
21
|
+
EventKind,
|
|
22
|
+
HostedChatClient,
|
|
23
|
+
PlanContext,
|
|
24
|
+
)
|
|
25
|
+
from copper_pilot_cli.copper_tools import LocalToolBroker
|
|
26
|
+
from copper_pilot_cli.copper_workspace import (
|
|
27
|
+
build_initial_file_tree,
|
|
28
|
+
canonical_workspace,
|
|
29
|
+
discover_workspace,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
AgentInput = Mapping[str, Any] | Sequence[BaseMessage]
|
|
33
|
+
AgentOutput = dict[str, Any]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _messages(value: AgentInput) -> list[BaseMessage]:
|
|
37
|
+
raw: Any = value.get("messages", []) if isinstance(value, Mapping) else value
|
|
38
|
+
messages: list[BaseMessage] = []
|
|
39
|
+
for item in raw:
|
|
40
|
+
if isinstance(item, BaseMessage):
|
|
41
|
+
messages.append(item)
|
|
42
|
+
elif isinstance(item, Mapping):
|
|
43
|
+
role = str(item.get("role") or item.get("type") or "")
|
|
44
|
+
content = str(item.get("content") or "")
|
|
45
|
+
message = (
|
|
46
|
+
HumanMessage(content=content)
|
|
47
|
+
if role in {"user", "human"}
|
|
48
|
+
else AIMessage(content=content)
|
|
49
|
+
)
|
|
50
|
+
messages.append(message)
|
|
51
|
+
else:
|
|
52
|
+
raise TypeError(f"Unsupported message value: {type(item).__name__}")
|
|
53
|
+
return messages
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _context(messages: Sequence[BaseMessage]) -> list[ChatMessage]:
|
|
57
|
+
result: list[ChatMessage] = []
|
|
58
|
+
for message in messages[:-1]:
|
|
59
|
+
role = "user" if message.type == "human" else "assistant"
|
|
60
|
+
if message.type not in {"human", "ai"}:
|
|
61
|
+
continue
|
|
62
|
+
content = (
|
|
63
|
+
message.content if isinstance(message.content, str) else json.dumps(message.content)
|
|
64
|
+
)
|
|
65
|
+
result.append(ChatMessage(role=role, content=content))
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CopperPilotAgent(Runnable[AgentInput, Any]):
|
|
70
|
+
"""A hosted-agent Runnable; it is deliberately not a BaseChatModel."""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
workspace: str | Path = ".",
|
|
75
|
+
*,
|
|
76
|
+
credential: DeviceCredential | None = None,
|
|
77
|
+
conversation_id: str | None = None,
|
|
78
|
+
mode: CopperMode = CopperMode.AGENT,
|
|
79
|
+
workflow: str | None = None,
|
|
80
|
+
skill_manifest_id: str | None = None,
|
|
81
|
+
plan_context: PlanContext | None = None,
|
|
82
|
+
runtime_context: dict[str, Any] | None = None,
|
|
83
|
+
tool_broker: LocalToolBroker | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.workspace = canonical_workspace(workspace)
|
|
86
|
+
self.context = discover_workspace(self.workspace)
|
|
87
|
+
self.credential = credential or load_credential()
|
|
88
|
+
if self.credential is None:
|
|
89
|
+
raise RuntimeError("CopperPilot login is required. Run `copper-pilot auth login`.")
|
|
90
|
+
self.conversation_id = conversation_id or str(uuid.uuid4())
|
|
91
|
+
self.mode = mode
|
|
92
|
+
self.workflow = workflow
|
|
93
|
+
self.skill_manifest_id = skill_manifest_id
|
|
94
|
+
self.plan_context = plan_context
|
|
95
|
+
self.runtime_context = runtime_context
|
|
96
|
+
self.tool_broker = tool_broker or LocalToolBroker(self.workspace)
|
|
97
|
+
self._client = HostedChatClient(
|
|
98
|
+
self.credential.base_url,
|
|
99
|
+
self.credential.api_key,
|
|
100
|
+
self.credential.fingerprint,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _request(self, messages: list[BaseMessage]) -> ChatRequest:
|
|
104
|
+
if not messages or messages[-1].type != "human":
|
|
105
|
+
raise ValueError("The final input message must be a human message.")
|
|
106
|
+
query = messages[-1].content
|
|
107
|
+
if not isinstance(query, str):
|
|
108
|
+
query = json.dumps(query)
|
|
109
|
+
return ChatRequest(
|
|
110
|
+
query=query,
|
|
111
|
+
context=_context(messages),
|
|
112
|
+
workspace_path=str(self.workspace),
|
|
113
|
+
conversation_id=self.conversation_id,
|
|
114
|
+
mode=self.mode,
|
|
115
|
+
agentic_mode=self.mode is CopperMode.AGENT or self.plan_context is not None,
|
|
116
|
+
workflow=self.workflow,
|
|
117
|
+
skill_manifest_id=self.skill_manifest_id,
|
|
118
|
+
plan_context=self.plan_context,
|
|
119
|
+
runtime_context=self.runtime_context,
|
|
120
|
+
schematic_path=self.context.schematic_path,
|
|
121
|
+
pcb_path=self.context.pcb_path,
|
|
122
|
+
project_data={"workspace_path": str(self.workspace)},
|
|
123
|
+
initial_file_tree=build_initial_file_tree(self.workspace) or None,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def astream_events_raw(
|
|
127
|
+
self,
|
|
128
|
+
input: AgentInput,
|
|
129
|
+
config: RunnableConfig | None = None,
|
|
130
|
+
) -> AsyncIterator[CopperEvent]:
|
|
131
|
+
del config
|
|
132
|
+
request = self._request(_messages(input))
|
|
133
|
+
self.tool_broker.begin_turn()
|
|
134
|
+
async for event in self._client.stream(request, self.tool_broker.execute):
|
|
135
|
+
yield event
|
|
136
|
+
|
|
137
|
+
async def ainvoke(
|
|
138
|
+
self,
|
|
139
|
+
input: AgentInput,
|
|
140
|
+
config: RunnableConfig | None = None,
|
|
141
|
+
**kwargs: Any,
|
|
142
|
+
) -> AgentOutput:
|
|
143
|
+
del kwargs
|
|
144
|
+
messages = _messages(input)
|
|
145
|
+
text: list[str] = []
|
|
146
|
+
events: list[dict[str, Any]] = []
|
|
147
|
+
async for event in self.astream_events_raw(messages, config):
|
|
148
|
+
if event.kind is EventKind.TEXT:
|
|
149
|
+
text.append(str(event.data))
|
|
150
|
+
events.append({"kind": event.kind.value, "data": event.data, "title": event.title})
|
|
151
|
+
assistant = AIMessage(content="".join(text))
|
|
152
|
+
return {"messages": [*messages, assistant], "events": events}
|
|
153
|
+
|
|
154
|
+
def invoke(
|
|
155
|
+
self,
|
|
156
|
+
input: AgentInput,
|
|
157
|
+
config: RunnableConfig | None = None,
|
|
158
|
+
**kwargs: Any,
|
|
159
|
+
) -> AgentOutput:
|
|
160
|
+
return asyncio.run(self.ainvoke(input, config, **kwargs))
|
|
161
|
+
|
|
162
|
+
async def astream(
|
|
163
|
+
self,
|
|
164
|
+
input: AgentInput,
|
|
165
|
+
config: RunnableConfig | None = None,
|
|
166
|
+
**kwargs: Any,
|
|
167
|
+
) -> AsyncIterator[AIMessageChunk]:
|
|
168
|
+
del kwargs
|
|
169
|
+
async for event in self.astream_events_raw(input, config):
|
|
170
|
+
if event.kind is EventKind.TEXT:
|
|
171
|
+
yield AIMessageChunk(content=str(event.data))
|
|
172
|
+
|
|
173
|
+
def stream(
|
|
174
|
+
self,
|
|
175
|
+
input: AgentInput,
|
|
176
|
+
config: RunnableConfig | None = None,
|
|
177
|
+
**kwargs: Any,
|
|
178
|
+
) -> Iterator[AIMessageChunk]:
|
|
179
|
+
async def collect() -> list[AIMessageChunk]:
|
|
180
|
+
return [chunk async for chunk in self.astream(input, config, **kwargs)]
|
|
181
|
+
|
|
182
|
+
yield from asyncio.run(collect())
|
|
183
|
+
|
|
184
|
+
async def acancel(self) -> None:
|
|
185
|
+
await self._client.cancel()
|
|
186
|
+
|
|
187
|
+
def as_langgraph(self) -> Any:
|
|
188
|
+
"""Compile the transport Runnable as a one-node LangGraph."""
|
|
189
|
+
from langgraph.graph import END, START, MessagesState, StateGraph
|
|
190
|
+
|
|
191
|
+
graph = StateGraph(MessagesState) # ty: ignore[invalid-argument-type]
|
|
192
|
+
|
|
193
|
+
async def run(state: MessagesState) -> dict[str, Any]:
|
|
194
|
+
result = await self.ainvoke({"messages": state["messages"]})
|
|
195
|
+
return {"messages": [result["messages"][-1]]}
|
|
196
|
+
|
|
197
|
+
graph.add_node("copper_pilot", run)
|
|
198
|
+
graph.add_edge(START, "copper_pilot")
|
|
199
|
+
graph.add_edge("copper_pilot", END)
|
|
200
|
+
return graph.compile()
|
|
201
|
+
|
|
202
|
+
def as_compiled_subagent(
|
|
203
|
+
self,
|
|
204
|
+
name: str = "copper-pilot",
|
|
205
|
+
description: str = "Delegate an electronics design task to CopperPilot.",
|
|
206
|
+
) -> Any:
|
|
207
|
+
"""Return an explicit Deep Agents compiled-subagent declaration."""
|
|
208
|
+
try:
|
|
209
|
+
from deepagents.middleware.subagents import CompiledSubAgent
|
|
210
|
+
except ModuleNotFoundError as exc:
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"The required `deepagents` runtime is unavailable; reinstall copper-pilot-cli."
|
|
213
|
+
) from exc
|
|
214
|
+
|
|
215
|
+
return CompiledSubAgent(name=name, description=description, runnable=self.as_langgraph())
|