oshell 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.
- oshell/__init__.py +3 -0
- oshell/agent.py +146 -0
- oshell/cli.py +836 -0
- oshell/config.py +91 -0
- oshell/history.py +57 -0
- oshell/media.py +188 -0
- oshell/media_agent.py +55 -0
- oshell/personas.py +90 -0
- oshell/providers.py +275 -0
- oshell/retrieval.py +180 -0
- oshell/storyboard.py +163 -0
- oshell/tools.py +150 -0
- oshell-0.1.1.dist-info/METADATA +286 -0
- oshell-0.1.1.dist-info/RECORD +17 -0
- oshell-0.1.1.dist-info/WHEEL +4 -0
- oshell-0.1.1.dist-info/entry_points.txt +3 -0
- oshell-0.1.1.dist-info/licenses/LICENSE +21 -0
oshell/__init__.py
ADDED
oshell/agent.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The oshell coding agent.
|
|
2
|
+
|
|
3
|
+
A small but capable ReAct-style loop: the model is asked to reply with a
|
|
4
|
+
single JSON object describing one action at a time. We execute the action,
|
|
5
|
+
feed the result back as an observation, and repeat until the model calls
|
|
6
|
+
``finish`` (or we hit the step limit).
|
|
7
|
+
|
|
8
|
+
This JSON protocol works with *any* chat model — it does not rely on
|
|
9
|
+
provider-specific function/tool calling, so the same agent runs on local
|
|
10
|
+
Ollama models and on OpenAI alike.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from typing import Iterator, List
|
|
18
|
+
|
|
19
|
+
from .providers import Message, Provider
|
|
20
|
+
from .tools import TOOL_REFERENCE, Toolbox
|
|
21
|
+
|
|
22
|
+
MAX_STEPS = 25
|
|
23
|
+
|
|
24
|
+
SYSTEM_PROMPT = f"""\
|
|
25
|
+
You are oshell Agent, an autonomous coding assistant working inside a user's \
|
|
26
|
+
project directory. You accomplish the user's task by taking ONE action at a \
|
|
27
|
+
time using the available tools.
|
|
28
|
+
|
|
29
|
+
{TOOL_REFERENCE}
|
|
30
|
+
|
|
31
|
+
RESPONSE FORMAT — respond with a SINGLE JSON object and nothing else:
|
|
32
|
+
{{"thought": "brief reasoning about the next step",
|
|
33
|
+
"action": "<one tool name above>",
|
|
34
|
+
"args": {{ ...arguments for that tool... }}}}
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- Output ONLY the JSON object. No markdown, no code fences, no extra prose.
|
|
38
|
+
- Take one action per response. Wait for the observation before continuing.
|
|
39
|
+
- Explore with list_dir / read_file before editing unfamiliar files.
|
|
40
|
+
- When writing a file, include its COMPLETE intended contents in "content".
|
|
41
|
+
- When the task is done, use the "finish" action with a short summary.
|
|
42
|
+
- Keep commands cross-platform where possible.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _extract_json(text: str) -> dict:
|
|
47
|
+
"""Best-effort parse of a JSON object from a model response."""
|
|
48
|
+
text = text.strip()
|
|
49
|
+
# Strip code fences if the model added them anyway.
|
|
50
|
+
if text.startswith("```"):
|
|
51
|
+
text = re.sub(r"^```[a-zA-Z]*\n?", "", text)
|
|
52
|
+
text = re.sub(r"\n?```$", "", text).strip()
|
|
53
|
+
try:
|
|
54
|
+
return json.loads(text)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
pass
|
|
57
|
+
# Fall back to the first balanced {...} block.
|
|
58
|
+
start = text.find("{")
|
|
59
|
+
if start == -1:
|
|
60
|
+
raise ValueError("No JSON object found in model response.")
|
|
61
|
+
depth = 0
|
|
62
|
+
for i in range(start, len(text)):
|
|
63
|
+
if text[i] == "{":
|
|
64
|
+
depth += 1
|
|
65
|
+
elif text[i] == "}":
|
|
66
|
+
depth -= 1
|
|
67
|
+
if depth == 0:
|
|
68
|
+
return json.loads(text[start : i + 1])
|
|
69
|
+
raise ValueError("Unbalanced JSON in model response.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AgentEvent:
|
|
73
|
+
"""A step in the agent's run, yielded for live display."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, kind: str, **data):
|
|
76
|
+
self.kind = kind # "thought" | "action" | "observation" | "finish" | "error"
|
|
77
|
+
self.data = data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CodingAgent:
|
|
81
|
+
"""Drives the think-act-observe loop."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, provider: Provider, toolbox: Toolbox, max_steps: int = MAX_STEPS):
|
|
84
|
+
self.provider = provider
|
|
85
|
+
self.toolbox = toolbox
|
|
86
|
+
self.max_steps = max_steps
|
|
87
|
+
# Conversation state persists across tasks so the agent can take
|
|
88
|
+
# follow-up instructions in interactive mode while remembering what
|
|
89
|
+
# it already did.
|
|
90
|
+
self.messages: List[Message] = [
|
|
91
|
+
{"role": "system", "content": SYSTEM_PROMPT}
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def _complete(self, messages: List[Message]) -> str:
|
|
95
|
+
"""Collect a full (non-streamed) completion from the provider."""
|
|
96
|
+
return "".join(self.provider.stream_chat(messages))
|
|
97
|
+
|
|
98
|
+
def run(self, task: str) -> Iterator[AgentEvent]:
|
|
99
|
+
"""Run the agent on ``task``, yielding events as it progresses.
|
|
100
|
+
|
|
101
|
+
Conversation history is retained between calls, so calling ``run``
|
|
102
|
+
again with a follow-up continues the same session.
|
|
103
|
+
"""
|
|
104
|
+
self.messages.append({"role": "user", "content": f"Task: {task}"})
|
|
105
|
+
|
|
106
|
+
for _ in range(self.max_steps):
|
|
107
|
+
raw = self._complete(self.messages)
|
|
108
|
+
try:
|
|
109
|
+
step = _extract_json(raw)
|
|
110
|
+
except ValueError as exc:
|
|
111
|
+
yield AgentEvent("error", message=f"{exc}\nModel said: {raw[:300]}")
|
|
112
|
+
# Nudge the model back to the protocol.
|
|
113
|
+
self.messages.append({"role": "assistant", "content": raw})
|
|
114
|
+
self.messages.append(
|
|
115
|
+
{
|
|
116
|
+
"role": "user",
|
|
117
|
+
"content": "Your last message was not valid JSON. "
|
|
118
|
+
"Respond with exactly one JSON object as instructed.",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
thought = step.get("thought", "")
|
|
124
|
+
action = step.get("action", "")
|
|
125
|
+
args = step.get("args", {}) or {}
|
|
126
|
+
|
|
127
|
+
if thought:
|
|
128
|
+
yield AgentEvent("thought", text=thought)
|
|
129
|
+
|
|
130
|
+
self.messages.append({"role": "assistant", "content": json.dumps(step)})
|
|
131
|
+
|
|
132
|
+
if action == "finish":
|
|
133
|
+
yield AgentEvent("finish", summary=args.get("summary", "Done."))
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
yield AgentEvent("action", action=action, args=args)
|
|
137
|
+
result = self.toolbox.dispatch(action, args)
|
|
138
|
+
yield AgentEvent("observation", ok=result.ok, output=result.output)
|
|
139
|
+
|
|
140
|
+
self.messages.append(
|
|
141
|
+
{"role": "user", "content": f"Observation: {result.render()}"}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
yield AgentEvent(
|
|
145
|
+
"error", message=f"Reached the step limit ({self.max_steps}). Stopping."
|
|
146
|
+
)
|