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.
@@ -7,6 +7,8 @@ __pycache__/
7
7
  .ruff_cache/
8
8
  .tox/
9
9
  .nox/
10
+ *.json
11
+ tmp/
10
12
 
11
13
  # Virtual environments
12
14
  .venv/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-chat-bot
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A small single-session chat client for OpenAI-compatible Responses API endpoints.
5
5
  Author-email: GGN_2015 <neko@jlulug.org>
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-chat-bot"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "A small single-session chat client for OpenAI-compatible Responses API endpoints."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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._client.responses.create(**self._request_payload(extra_request_args))
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