codex-chat-bot 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/.gitignore +2 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/PKG-INFO +1 -1
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/pyproject.toml +1 -1
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/src/codex_chat_bot/session.py +31 -3
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/tests/test_session.py +85 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/README.md +0 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/src/codex_chat_bot/__init__.py +0 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/src/codex_chat_bot/cli.py +0 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/src/codex_chat_bot/config.py +0 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/src/codex_chat_bot/errors.py +0 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/tests/test_cli.py +0 -0
- {codex_chat_bot-0.1.2 → codex_chat_bot-0.1.3}/tests/test_config.py +0 -0
|
@@ -15,6 +15,8 @@ SYSTEM_USERNAME = "system"
|
|
|
15
15
|
DEVELOPER_USERNAME = "developer"
|
|
16
16
|
DEFAULT_USERNAME = "user"
|
|
17
17
|
ASSISTANT_USERNAME = "assistant"
|
|
18
|
+
MAX_EMPTY_RESPONSE_ATTEMPTS = 5
|
|
19
|
+
MAX_REQUEST_HISTORY_MESSAGES = 30
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
@dataclass(frozen=True)
|
|
@@ -136,22 +138,33 @@ class ChatSession:
|
|
|
136
138
|
|
|
137
139
|
self._messages.append(Message(role="user", content=message, username=username))
|
|
138
140
|
try:
|
|
139
|
-
response = self.
|
|
141
|
+
response, text = self._create_response_until_text(extra_request_args)
|
|
140
142
|
except Exception:
|
|
141
143
|
self._messages.pop()
|
|
142
144
|
self._save_bound_history()
|
|
143
145
|
raise
|
|
144
146
|
|
|
145
|
-
text = _extract_response_text(response)
|
|
146
147
|
self._messages.append(Message(role="assistant", content=text, username=ASSISTANT_USERNAME))
|
|
147
148
|
self._trim_history()
|
|
148
149
|
self._save_bound_history()
|
|
149
150
|
return ChatResponse(text=text, raw=response, messages=self.messages)
|
|
150
151
|
|
|
152
|
+
def _create_response_until_text(self, extra_request_args: Mapping[str, Any]) -> tuple[Any, str]:
|
|
153
|
+
response: Any = None
|
|
154
|
+
text = ""
|
|
155
|
+
|
|
156
|
+
for _ in range(MAX_EMPTY_RESPONSE_ATTEMPTS):
|
|
157
|
+
response = self._client.responses.create(**self._request_payload(extra_request_args))
|
|
158
|
+
text = _extract_response_text(response)
|
|
159
|
+
if text.strip():
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
return response, text if text.strip() else ""
|
|
163
|
+
|
|
151
164
|
def _request_payload(self, extra_request_args: Mapping[str, Any]) -> dict[str, Any]:
|
|
152
165
|
payload: dict[str, Any] = {
|
|
153
166
|
"model": self.config.model,
|
|
154
|
-
"input": [message.to_api() for message in self._messages],
|
|
167
|
+
"input": [message.to_api() for message in _latest_messages(self._messages, MAX_REQUEST_HISTORY_MESSAGES)],
|
|
155
168
|
}
|
|
156
169
|
if self.config.temperature is not None:
|
|
157
170
|
payload["temperature"] = self.config.temperature
|
|
@@ -256,6 +269,21 @@ def _messages_from_history_payload(payload: Any) -> tuple[Message, ...]:
|
|
|
256
269
|
return tuple(messages)
|
|
257
270
|
|
|
258
271
|
|
|
272
|
+
def _latest_messages(messages: Sequence[Message], max_messages: int) -> tuple[Message, ...]:
|
|
273
|
+
if max_messages <= 0:
|
|
274
|
+
return tuple(messages)
|
|
275
|
+
|
|
276
|
+
prefix: list[Message] = []
|
|
277
|
+
rest = list(messages)
|
|
278
|
+
while rest and rest[0].role in {"system", "developer"}:
|
|
279
|
+
prefix.append(rest[0])
|
|
280
|
+
rest = rest[1:]
|
|
281
|
+
|
|
282
|
+
if len(rest) > max_messages:
|
|
283
|
+
rest = rest[-max_messages:]
|
|
284
|
+
return tuple(prefix + rest)
|
|
285
|
+
|
|
286
|
+
|
|
259
287
|
def normalize_username(username: str) -> str:
|
|
260
288
|
username = str(username).strip()
|
|
261
289
|
if not username:
|
|
@@ -24,12 +24,33 @@ class FakeClient:
|
|
|
24
24
|
self.responses = FakeResponses()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class ScriptedResponses:
|
|
28
|
+
def __init__(self, output_texts):
|
|
29
|
+
self.output_texts = list(output_texts)
|
|
30
|
+
self.calls = []
|
|
31
|
+
|
|
32
|
+
def create(self, **kwargs):
|
|
33
|
+
self.calls.append(kwargs)
|
|
34
|
+
return SimpleNamespace(output_text=self.output_texts.pop(0))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ScriptedClient:
|
|
38
|
+
def __init__(self, output_texts):
|
|
39
|
+
self.responses = ScriptedResponses(output_texts)
|
|
40
|
+
|
|
41
|
+
|
|
27
42
|
def make_session(**config_overrides):
|
|
28
43
|
config = ChatConfig(api_key="test-key", base_url="https://api.example/v1", model="test-model", **config_overrides)
|
|
29
44
|
client = FakeClient()
|
|
30
45
|
return ChatSession(config=config, client=client), client
|
|
31
46
|
|
|
32
47
|
|
|
48
|
+
def make_scripted_session(output_texts, **config_overrides):
|
|
49
|
+
config = ChatConfig(api_key="test-key", base_url="https://api.example/v1", model="test-model", **config_overrides)
|
|
50
|
+
client = ScriptedClient(output_texts)
|
|
51
|
+
return ChatSession(config=config, client=client), client
|
|
52
|
+
|
|
53
|
+
|
|
33
54
|
def test_session_sends_full_single_session_history():
|
|
34
55
|
session, client = make_session(system_rules=("Follow the test.",))
|
|
35
56
|
|
|
@@ -67,6 +88,50 @@ def test_session_sends_usernames_as_model_visible_user_context():
|
|
|
67
88
|
)
|
|
68
89
|
|
|
69
90
|
|
|
91
|
+
def test_session_retries_empty_model_text_until_non_empty():
|
|
92
|
+
session, client = make_scripted_session(["", " \n\t ", "answer: hello"], system_rules=("Follow the test.",))
|
|
93
|
+
|
|
94
|
+
response = session.send("hello")
|
|
95
|
+
|
|
96
|
+
assert response.text == "answer: hello"
|
|
97
|
+
assert response.raw.output_text == "answer: hello"
|
|
98
|
+
assert len(client.responses.calls) == 3
|
|
99
|
+
assert [call["input"] for call in client.responses.calls] == [
|
|
100
|
+
[
|
|
101
|
+
{"role": "system", "content": "Follow the test."},
|
|
102
|
+
{"role": "user", "content": "Message from user:\nhello"},
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
{"role": "system", "content": "Follow the test."},
|
|
106
|
+
{"role": "user", "content": "Message from user:\nhello"},
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
{"role": "system", "content": "Follow the test."},
|
|
110
|
+
{"role": "user", "content": "Message from user:\nhello"},
|
|
111
|
+
],
|
|
112
|
+
]
|
|
113
|
+
assert session.messages == (
|
|
114
|
+
Message(role="system", content="Follow the test.", username="system"),
|
|
115
|
+
Message(role="user", content="hello", username="user"),
|
|
116
|
+
Message(role="assistant", content="answer: hello", username="assistant"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_session_returns_empty_string_after_five_empty_model_texts():
|
|
121
|
+
session, client = make_scripted_session(["", " ", "\n", "\t", "\r\n"], system_rules=("Follow the test.",))
|
|
122
|
+
|
|
123
|
+
response = session.send("hello")
|
|
124
|
+
|
|
125
|
+
assert response.text == ""
|
|
126
|
+
assert response.raw.output_text == "\r\n"
|
|
127
|
+
assert len(client.responses.calls) == 5
|
|
128
|
+
assert session.messages == (
|
|
129
|
+
Message(role="system", content="Follow the test.", username="system"),
|
|
130
|
+
Message(role="user", content="hello", username="user"),
|
|
131
|
+
Message(role="assistant", content="", username="assistant"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
70
135
|
def test_session_adds_system_rules_to_system_message():
|
|
71
136
|
session, client = make_session(
|
|
72
137
|
system_rules=("Follow the test.", "Answer in English.", "Keep answers short."),
|
|
@@ -112,6 +177,26 @@ def test_history_limit_keeps_latest_non_system_messages():
|
|
|
112
177
|
)
|
|
113
178
|
|
|
114
179
|
|
|
180
|
+
def test_request_payload_keeps_only_latest_thirty_history_messages():
|
|
181
|
+
session, client = make_session(system_rules=("ignored",))
|
|
182
|
+
history_messages = [{"role": "system", "content": "Persisted system."}]
|
|
183
|
+
history_messages.extend(
|
|
184
|
+
{"role": "user" if index % 2 == 0 else "assistant", "content": f"message {index}"}
|
|
185
|
+
for index in range(40)
|
|
186
|
+
)
|
|
187
|
+
session.load_history_json(json.dumps({"messages": history_messages}))
|
|
188
|
+
|
|
189
|
+
session.ask("new")
|
|
190
|
+
|
|
191
|
+
request_input = client.responses.calls[0]["input"]
|
|
192
|
+
assert len(request_input) == 31
|
|
193
|
+
assert request_input[0] == {"role": "system", "content": "Persisted system."}
|
|
194
|
+
assert request_input[1] == {"role": "assistant", "content": "message 11"}
|
|
195
|
+
assert request_input[-2] == {"role": "assistant", "content": "message 39"}
|
|
196
|
+
assert request_input[-1] == {"role": "user", "content": "Message from user:\nnew"}
|
|
197
|
+
assert session.messages[1] == Message(role="user", content="message 0", username="user")
|
|
198
|
+
|
|
199
|
+
|
|
115
200
|
def test_session_exports_and_imports_history_json():
|
|
116
201
|
session, _ = make_session(system_rules=("Follow the test.",))
|
|
117
202
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|