codex-chat-bot 0.1.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.
@@ -0,0 +1,19 @@
1
+ """Single-session chat helpers for OpenAI-compatible Responses API clients."""
2
+
3
+ from .config import ChatConfig
4
+ from .errors import ChatBotError, MissingAPIKeyError, MissingBaseURLError, ResponseTextError
5
+ from .session import ChatResponse, ChatSession, Message
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ __all__ = [
10
+ "ChatBotError",
11
+ "ChatConfig",
12
+ "ChatResponse",
13
+ "ChatSession",
14
+ "Message",
15
+ "MissingAPIKeyError",
16
+ "MissingBaseURLError",
17
+ "ResponseTextError",
18
+ "__version__",
19
+ ]
codex_chat_bot/cli.py ADDED
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import getpass
5
+ import sys
6
+ from collections.abc import Sequence
7
+ from dataclasses import replace
8
+ from pathlib import Path
9
+
10
+ from . import __version__
11
+ from .config import ChatConfig
12
+ from .errors import ChatBotError
13
+ from .session import ChatSession
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(
18
+ prog="codex-chat",
19
+ description="A simple single-session chat CLI for OpenAI-compatible endpoints.",
20
+ )
21
+ parser.add_argument(
22
+ "message",
23
+ nargs="*",
24
+ help="Run one chat turn and print the answer. Omit to start interactive mode.",
25
+ )
26
+ parser.add_argument("--api-key", help="API key. Defaults to CODEX_CHAT_API_KEY.")
27
+ parser.add_argument("--base-url", help="API base URL. Defaults to CODEX_CHAT_BASE_URL.")
28
+ parser.add_argument("--model", help="Model name. Defaults to CODEX_CHAT_MODEL or gpt-5.5.")
29
+ parser.add_argument(
30
+ "--system",
31
+ dest="system_rules",
32
+ action="append",
33
+ help="System instruction for this session. Repeat the option to add multiple instructions.",
34
+ )
35
+ parser.add_argument(
36
+ "--system-rule",
37
+ action="append",
38
+ dest="system_rules",
39
+ help="System rule for this session. Repeat the option to add multiple rules.",
40
+ )
41
+ parser.add_argument(
42
+ "--system-rules-file",
43
+ help="Read system rules from a UTF-8 text file, one non-empty line per rule.",
44
+ )
45
+ parser.add_argument("--temperature", type=float, help="Optional model temperature.")
46
+ parser.add_argument("--max-output-tokens", type=int, help="Optional response token limit.")
47
+ parser.add_argument(
48
+ "--max-history-messages",
49
+ type=int,
50
+ help="Keep only the latest N non-system messages in memory.",
51
+ )
52
+ parser.add_argument("--import-history", help="Load chat history from a JSON file before chatting.")
53
+ parser.add_argument("--bind-history", help="Bind chat history to a JSON file and save every update.")
54
+ parser.add_argument(
55
+ "--export-history",
56
+ nargs="?",
57
+ help=argparse.SUPPRESS,
58
+ )
59
+ parser.add_argument("--version", action="store_true", help="Print package version and exit.")
60
+ return parser
61
+
62
+
63
+ def main(argv: Sequence[str] | None = None) -> int:
64
+ parser = build_parser()
65
+ args = parser.parse_args(argv)
66
+
67
+ if args.version:
68
+ print(__version__)
69
+ return 0
70
+ if args.export_history:
71
+ parser.error("--export-history was renamed to --bind-history")
72
+
73
+ try:
74
+ system_rules = _collect_system_rules(args.system_rules, args.system_rules_file)
75
+ config = ChatConfig.from_env(
76
+ api_key=args.api_key,
77
+ base_url=args.base_url,
78
+ model=args.model,
79
+ system_rules=system_rules,
80
+ temperature=args.temperature,
81
+ max_output_tokens=args.max_output_tokens,
82
+ max_history_messages=args.max_history_messages,
83
+ )
84
+ config = _prompt_for_missing_required_config(config)
85
+ session = ChatSession(config)
86
+ if args.bind_history:
87
+ session.bind_history(args.bind_history)
88
+ elif args.import_history:
89
+ session.load_history(args.import_history)
90
+ except (ChatBotError, OSError, ValueError) as exc:
91
+ print(f"error: {exc}", file=sys.stderr)
92
+ return 1
93
+
94
+ return _run_session(session, args.message)
95
+
96
+
97
+ def _run_session(session: ChatSession, message_parts: list[str]) -> int:
98
+ try:
99
+ if message_parts:
100
+ return _run_one_shot(session, " ".join(message_parts))
101
+ return _run_interactive(session)
102
+ except KeyboardInterrupt:
103
+ print()
104
+ return 130
105
+
106
+
107
+ def _run_one_shot(session: ChatSession, message: str) -> int:
108
+ try:
109
+ print(session.ask(message))
110
+ return 0
111
+ except ChatBotError as exc:
112
+ print(f"error: {exc}", file=sys.stderr)
113
+ return 1
114
+
115
+
116
+ def _run_interactive(session: ChatSession) -> int:
117
+ print("Codex Chat Bot")
118
+ print("Commands: /reset clears this session, /import PATH loads history, /export PATH saves history, /exit quits.")
119
+
120
+ while True:
121
+ try:
122
+ user_text = input("you> ").strip()
123
+ except (EOFError, KeyboardInterrupt):
124
+ print()
125
+ return 0
126
+
127
+ if not user_text:
128
+ continue
129
+ if user_text in {"/exit", "/quit"}:
130
+ return 0
131
+ if user_text == "/reset":
132
+ session.reset()
133
+ print("session reset")
134
+ continue
135
+ if user_text.startswith("/import "):
136
+ _run_import_command(session, user_text.removeprefix("/import ").strip())
137
+ continue
138
+ if user_text.startswith("/export "):
139
+ _run_export_command(session, user_text.removeprefix("/export ").strip())
140
+ continue
141
+
142
+ try:
143
+ answer = session.ask(user_text)
144
+ except ChatBotError as exc:
145
+ print(f"error: {exc}", file=sys.stderr)
146
+ continue
147
+
148
+ print(f"assistant> {answer}")
149
+
150
+
151
+ def _run_import_command(session: ChatSession, path: str) -> None:
152
+ if not path:
153
+ print("usage: /import PATH")
154
+ return
155
+
156
+ try:
157
+ session.load_history(path)
158
+ except (OSError, ValueError) as exc:
159
+ print(f"error: {exc}", file=sys.stderr)
160
+ return
161
+
162
+ print(f"history imported from {path}")
163
+
164
+
165
+ def _run_export_command(session: ChatSession, path: str) -> None:
166
+ if not path:
167
+ print("usage: /export PATH")
168
+ return
169
+
170
+ try:
171
+ session.save_history(path)
172
+ except OSError as exc:
173
+ print(f"error: {exc}", file=sys.stderr)
174
+ return
175
+
176
+ print(f"history exported to {path}")
177
+
178
+
179
+ def _collect_system_rules(rules: list[str] | None, rules_file: str | None) -> list[str] | None:
180
+ collected: list[str] = []
181
+ if rules_file:
182
+ collected.extend(line.strip() for line in Path(rules_file).read_text(encoding="utf-8").splitlines())
183
+ if rules:
184
+ collected.extend(rules)
185
+
186
+ clean_rules = [rule.strip() for rule in collected if rule.strip()]
187
+ return clean_rules or None
188
+
189
+
190
+ def _prompt_for_missing_required_config(config: ChatConfig) -> ChatConfig:
191
+ api_key = _clean_optional_text(config.api_key)
192
+ base_url = _clean_optional_text(config.base_url)
193
+ if api_key and base_url:
194
+ return replace(config, api_key=api_key, base_url=base_url)
195
+
196
+ if not _is_interactive_input():
197
+ return replace(config, api_key=api_key, base_url=base_url)
198
+
199
+ prompted_api_key = api_key
200
+ prompted_base_url = base_url
201
+
202
+ try:
203
+ if not prompted_api_key:
204
+ print("No API key found in --api-key or CODEX_CHAT_API_KEY.", file=sys.stderr)
205
+ prompted_api_key = getpass.getpass("API key: ")
206
+ if not prompted_base_url:
207
+ print("No API base URL found in --base-url or CODEX_CHAT_BASE_URL.", file=sys.stderr)
208
+ prompted_base_url = input("Base URL: ")
209
+ except (EOFError, KeyboardInterrupt):
210
+ print(file=sys.stderr)
211
+ return replace(config, api_key=_clean_optional_text(prompted_api_key), base_url=_clean_optional_text(prompted_base_url))
212
+
213
+ return replace(
214
+ config,
215
+ api_key=_clean_optional_text(prompted_api_key),
216
+ base_url=_clean_optional_text(prompted_base_url),
217
+ )
218
+
219
+
220
+ def _clean_optional_text(value: str | None) -> str | None:
221
+ if value is None:
222
+ return None
223
+ value = value.strip()
224
+ return value or None
225
+
226
+
227
+ def _is_interactive_input() -> bool:
228
+ return sys.stdin.isatty()
229
+
230
+
231
+ if __name__ == "__main__":
232
+ raise SystemExit(main())
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+
8
+ DEFAULT_MODEL = "gpt-5.5"
9
+ DEFAULT_SYSTEM_RULES = ("You are a helpful assistant.",)
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ChatConfig:
14
+ """Runtime configuration for a chat session."""
15
+
16
+ api_key: str | None = None
17
+ base_url: str | None = None
18
+ model: str = DEFAULT_MODEL
19
+ system_rules: tuple[str, ...] = DEFAULT_SYSTEM_RULES
20
+ temperature: float | None = None
21
+ max_output_tokens: int | None = None
22
+ max_history_messages: int | None = None
23
+ timeout: float | None = None
24
+ organization: str | None = None
25
+ project: str | None = None
26
+
27
+ def __post_init__(self) -> None:
28
+ object.__setattr__(self, "system_rules", _normalize_system_rules(self.system_rules))
29
+
30
+ @classmethod
31
+ def from_env(cls, **overrides: object) -> "ChatConfig":
32
+ values: dict[str, object] = {
33
+ "api_key": _first_env("CODEX_CHAT_API_KEY"),
34
+ "base_url": _first_env("CODEX_CHAT_BASE_URL"),
35
+ "model": _first_env("CODEX_CHAT_MODEL") or DEFAULT_MODEL,
36
+ "temperature": _optional_float(_first_env("CODEX_CHAT_TEMPERATURE")),
37
+ "max_output_tokens": _optional_int(_first_env("CODEX_CHAT_MAX_OUTPUT_TOKENS")),
38
+ "max_history_messages": _optional_int(_first_env("CODEX_CHAT_MAX_HISTORY_MESSAGES")),
39
+ "timeout": _optional_float(_first_env("CODEX_CHAT_TIMEOUT")),
40
+ "organization": _first_env("CODEX_CHAT_ORG_ID"),
41
+ "project": _first_env("CODEX_CHAT_PROJECT_ID"),
42
+ }
43
+
44
+ for key, value in overrides.items():
45
+ if value is not None:
46
+ values[key] = value
47
+
48
+ return cls(**values)
49
+
50
+
51
+ def _first_env(*names: str) -> str | None:
52
+ for name in names:
53
+ value = os.getenv(name)
54
+ if value:
55
+ return value
56
+ return None
57
+
58
+
59
+ def _optional_int(value: str | None) -> int | None:
60
+ if value is None:
61
+ return None
62
+ return int(value)
63
+
64
+
65
+ def _optional_float(value: str | None) -> float | None:
66
+ if value is None:
67
+ return None
68
+ return float(value)
69
+
70
+
71
+ def _normalize_system_rules(value: object) -> tuple[str, ...]:
72
+ if value is None:
73
+ return ()
74
+ if isinstance(value, str):
75
+ return tuple(line.strip() for line in value.splitlines() if line.strip())
76
+ if isinstance(value, Sequence):
77
+ return tuple(str(item).strip() for item in value if str(item).strip())
78
+ raise TypeError("system_rules must be a string, a sequence of strings, or None.")
@@ -0,0 +1,14 @@
1
+ class ChatBotError(Exception):
2
+ """Base exception for codex-chat-bot."""
3
+
4
+
5
+ class MissingAPIKeyError(ChatBotError):
6
+ """Raised when no API key is available."""
7
+
8
+
9
+ class MissingBaseURLError(ChatBotError):
10
+ """Raised when no API base URL is available."""
11
+
12
+
13
+ class ResponseTextError(ChatBotError):
14
+ """Raised when response text cannot be extracted from an API response."""
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Mapping, Sequence
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from .config import ChatConfig
10
+ from .errors import MissingAPIKeyError, MissingBaseURLError, ResponseTextError
11
+
12
+
13
+ Role = Literal["system", "developer", "user", "assistant"]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Message:
18
+ role: Role
19
+ content: str
20
+
21
+ def to_api(self) -> dict[str, str]:
22
+ return {"role": self.role, "content": self.content}
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ChatResponse:
27
+ text: str
28
+ raw: Any
29
+ messages: tuple[Message, ...]
30
+
31
+
32
+ class ChatSession:
33
+ """Single-session chat state kept entirely in local memory."""
34
+
35
+ def __init__(self, config: ChatConfig | None = None, client: Any | None = None) -> None:
36
+ self.config = config or ChatConfig.from_env()
37
+ self._client = client or self._build_client()
38
+ self._messages: list[Message] = []
39
+ self._history_path: Path | None = None
40
+ self.reset()
41
+
42
+ @property
43
+ def messages(self) -> tuple[Message, ...]:
44
+ return tuple(self._messages)
45
+
46
+ def export_history_json(self) -> str:
47
+ payload = {"messages": [message.to_api() for message in self._messages]}
48
+ return json.dumps(payload, ensure_ascii=False, indent=4)
49
+
50
+ def save_history(self, path: str | Path) -> None:
51
+ Path(path).write_text(self.export_history_json() + "\n", encoding="utf-8")
52
+
53
+ def bind_history(self, path: str | Path) -> None:
54
+ history_path = Path(path)
55
+ previous_history_path = self._history_path
56
+ self._history_path = None
57
+ if history_path.exists():
58
+ try:
59
+ self.load_history(history_path)
60
+ except Exception:
61
+ self._history_path = previous_history_path
62
+ raise
63
+
64
+ self._history_path = history_path
65
+ self._save_bound_history()
66
+
67
+ def load_history_json(self, data: str) -> None:
68
+ try:
69
+ payload = json.loads(data)
70
+ except json.JSONDecodeError as exc:
71
+ raise ValueError("chat history must be valid JSON") from exc
72
+
73
+ messages = _messages_from_history_payload(payload)
74
+ self._messages = list(messages)
75
+ self._trim_history()
76
+ self._save_bound_history()
77
+
78
+ def load_history(self, path: str | Path) -> None:
79
+ self.load_history_json(Path(path).read_text(encoding="utf-8"))
80
+
81
+ def reset(self, system_rules: Sequence[str] | None = None) -> None:
82
+ self._messages.clear()
83
+ system_content = self._build_system_content(system_rules)
84
+ if system_content:
85
+ self._messages.append(Message(role="system", content=system_content))
86
+ self._save_bound_history()
87
+
88
+ def ask(self, message: str, **extra_request_args: Any) -> str:
89
+ return self.send(message, **extra_request_args).text
90
+
91
+ def send(self, message: str, **extra_request_args: Any) -> ChatResponse:
92
+ message = message.strip()
93
+ if not message:
94
+ raise ValueError("message cannot be empty")
95
+
96
+ self._messages.append(Message(role="user", content=message))
97
+ try:
98
+ response = self._client.responses.create(**self._request_payload(extra_request_args))
99
+ except Exception:
100
+ self._messages.pop()
101
+ self._save_bound_history()
102
+ raise
103
+
104
+ text = _extract_response_text(response)
105
+ self._messages.append(Message(role="assistant", content=text))
106
+ self._trim_history()
107
+ self._save_bound_history()
108
+ return ChatResponse(text=text, raw=response, messages=self.messages)
109
+
110
+ def _request_payload(self, extra_request_args: Mapping[str, Any]) -> dict[str, Any]:
111
+ payload: dict[str, Any] = {
112
+ "model": self.config.model,
113
+ "input": [message.to_api() for message in self._messages],
114
+ }
115
+ if self.config.temperature is not None:
116
+ payload["temperature"] = self.config.temperature
117
+ if self.config.max_output_tokens is not None:
118
+ payload["max_output_tokens"] = self.config.max_output_tokens
119
+
120
+ payload.update(extra_request_args)
121
+ return payload
122
+
123
+ def _build_system_content(self, system_rules: Sequence[str] | None) -> str:
124
+ rules = self.config.system_rules if system_rules is None else system_rules
125
+
126
+ clean_rules = [str(rule).strip() for rule in rules if str(rule).strip()]
127
+ return "\n".join(clean_rules)
128
+
129
+ def _trim_history(self) -> None:
130
+ max_messages = self.config.max_history_messages
131
+ if max_messages is None or max_messages <= 0:
132
+ return
133
+
134
+ prefix: list[Message] = []
135
+ rest = self._messages
136
+ while rest and rest[0].role in {"system", "developer"}:
137
+ prefix.append(rest[0])
138
+ rest = rest[1:]
139
+
140
+ if len(rest) > max_messages:
141
+ rest = rest[-max_messages:]
142
+ self._messages = prefix + rest
143
+
144
+ def _save_bound_history(self) -> None:
145
+ if self._history_path is not None:
146
+ self.save_history(self._history_path)
147
+
148
+ def _build_client(self) -> Any:
149
+ if not self.config.api_key:
150
+ raise MissingAPIKeyError(
151
+ "missing API key; set CODEX_CHAT_API_KEY, pass --api-key, "
152
+ "or pass ChatConfig(api_key=...)."
153
+ )
154
+ if not self.config.base_url:
155
+ raise MissingBaseURLError(
156
+ "missing API base URL; set CODEX_CHAT_BASE_URL, pass --base-url, "
157
+ "or pass ChatConfig(base_url=...)."
158
+ )
159
+
160
+ try:
161
+ from openai import OpenAI
162
+ except ImportError as exc:
163
+ raise RuntimeError("openai package is required; run `pip install -e .`.") from exc
164
+
165
+ kwargs: dict[str, Any] = {"api_key": self.config.api_key, "base_url": self.config.base_url}
166
+ for name in ("timeout", "organization", "project"):
167
+ value = getattr(self.config, name)
168
+ if value is not None:
169
+ kwargs[name] = value
170
+
171
+ return OpenAI(**kwargs)
172
+
173
+
174
+ def _extract_response_text(response: Any) -> str:
175
+ output_text = _get_value(response, "output_text")
176
+ if isinstance(output_text, str):
177
+ return output_text
178
+
179
+ chunks: list[str] = []
180
+ for item in _get_value(response, "output", default=[]) or []:
181
+ for part in _get_value(item, "content", default=[]) or []:
182
+ text = _get_value(part, "text")
183
+ if isinstance(text, str):
184
+ chunks.append(text)
185
+
186
+ if chunks:
187
+ return "".join(chunks)
188
+
189
+ raise ResponseTextError("could not extract text from the model response")
190
+
191
+
192
+ def _messages_from_history_payload(payload: Any) -> tuple[Message, ...]:
193
+ if isinstance(payload, list):
194
+ raw_messages = payload
195
+ elif isinstance(payload, Mapping) and isinstance(payload.get("messages"), list):
196
+ raw_messages = payload["messages"]
197
+ else:
198
+ raise ValueError("chat history JSON must be an object with a messages array")
199
+
200
+ messages: list[Message] = []
201
+ for index, item in enumerate(raw_messages):
202
+ if not isinstance(item, Mapping):
203
+ raise ValueError(f"chat history message {index} must be an object")
204
+
205
+ role = item.get("role")
206
+ content = item.get("content")
207
+ if role not in {"system", "developer", "user", "assistant"}:
208
+ raise ValueError(f"chat history message {index} has an invalid role")
209
+ if not isinstance(content, str):
210
+ raise ValueError(f"chat history message {index} content must be a string")
211
+
212
+ messages.append(Message(role=role, content=content))
213
+
214
+ return tuple(messages)
215
+
216
+
217
+ def _get_value(obj: Any, key: str, default: Any = None) -> Any:
218
+ if isinstance(obj, Mapping):
219
+ return obj.get(key, default)
220
+ return getattr(obj, key, default)
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-chat-bot
3
+ Version: 0.1.0
4
+ Summary: A small single-session chat client for OpenAI-compatible Responses API endpoints.
5
+ Author-email: GGN_2015 <neko@jlulug.org>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: openai<3,>=1.99.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8.0; extra == 'dev'
10
+ Description-Content-Type: text/markdown
11
+
12
+ # codex-chat-bot
13
+
14
+ A small Python chat project with:
15
+
16
+ - a Python programming interface
17
+ - a command-line interface
18
+ - single-session memory
19
+ - configurable API key, base URL, model, and system rules
20
+
21
+ The project uses OpenAI SDK-compatible Responses API endpoints.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ python -m pip install codex-chat-bot
27
+ ```
28
+
29
+ ## Environment Variables
30
+
31
+ Only `CODEX_CHAT_*` environment variables are read:
32
+
33
+ ```bash
34
+ export CODEX_CHAT_API_KEY="your API key"
35
+ export CODEX_CHAT_BASE_URL="https://api.openai.com/v1"
36
+ export CODEX_CHAT_MODEL="gpt-5.5" # optional
37
+ ```
38
+
39
+ ## Command-Line Usage
40
+
41
+ Run a single chat turn:
42
+
43
+ ```bash
44
+ codex-chat "Write a Python hello world example."
45
+ ```
46
+
47
+ Start interactive chat:
48
+
49
+ ```bash
50
+ codex-chat
51
+ ```
52
+
53
+ If `codex-chat` is not on your PATH, run the module directly:
54
+
55
+ ```bash
56
+ python -m codex_chat_bot.cli
57
+ python -m codex_chat_bot.cli "Hello"
58
+ ```
59
+
60
+ Interactive commands:
61
+
62
+ - `/reset` clears the current session memory.
63
+ - `/import PATH` loads chat history from a JSON file.
64
+ - `/export PATH` saves chat history to a JSON file.
65
+ - `/exit` or `/quit` exits the chat.
66
+
67
+ Common options:
68
+
69
+ ```bash
70
+ codex-chat --api-key "your API key" --base-url "https://api.openai.com/v1" "Hello"
71
+ codex-chat --model gpt-5.5 --system "You are a concise programming assistant."
72
+ codex-chat --system-rule "Answer in English." --system-rule "Keep answers short." "Explain pytest."
73
+ codex-chat --system-rules-file ./rules.txt "Review this idea."
74
+ codex-chat --bind-history ./history.json "Continue our chat."
75
+ codex-chat --base-url "https://api.openai.com/v1" "Explain pytest."
76
+ ```
77
+
78
+ If `CODEX_CHAT_API_KEY` or `CODEX_CHAT_BASE_URL` is not set and the value was
79
+ not passed on the command line, interactive CLI startup prompts for the missing
80
+ value.
81
+
82
+ `--bind-history` loads the JSON file if it already exists, creates it if it does
83
+ not, and writes every new message to that file as the chat changes.
84
+
85
+ ## Python API
86
+
87
+ ```python
88
+ from codex_chat_bot import ChatConfig, ChatSession
89
+
90
+ session = ChatSession(ChatConfig.from_env())
91
+
92
+ print(session.ask("Remember that my project is named codex-chat-bot."))
93
+ print(session.ask("What is my project called?"))
94
+
95
+ session.bind_history("history.json")
96
+ session.ask("This message is saved automatically.")
97
+ ```
98
+
99
+ You can also pass configuration explicitly:
100
+
101
+ ```python
102
+ from codex_chat_bot import ChatConfig, ChatSession
103
+
104
+ config = ChatConfig(
105
+ api_key="your API key",
106
+ base_url="https://api.openai.com/v1",
107
+ model="gpt-5.5",
108
+ system_rules=(
109
+ "You are a patient Python programming assistant.",
110
+ "Answer in English.",
111
+ "Keep code examples runnable.",
112
+ ),
113
+ )
114
+
115
+ session = ChatSession(config)
116
+ answer = session.ask("Write a function that reads a JSON file.")
117
+ print(answer)
118
+ ```
119
+
120
+ `system_rules` are added to the session's initial system message and stay active
121
+ until the session is reset.
122
+
123
+ Chat history JSON uses a top-level `messages` array:
124
+
125
+ ```json
126
+ {
127
+ "messages": [
128
+ { "role": "system", "content": "You are a helpful assistant." },
129
+ { "role": "user", "content": "Hello" },
130
+ { "role": "assistant", "content": "Hi!" }
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ## Tests
136
+
137
+ ```bash
138
+ python -m pytest
139
+ ```
@@ -0,0 +1,9 @@
1
+ codex_chat_bot/__init__.py,sha256=WvIBwz0w0e3d7TYJzxF6RqFrVwaghlHGzj7JP1ZzIv8,487
2
+ codex_chat_bot/cli.py,sha256=XUOK_tgcaQfaTgTrMtKk8R3KMO49qi09kzs0iLpgJEo,7546
3
+ codex_chat_bot/config.py,sha256=D-IewKcpzG8l5fxRdI1-FiuB-_M_MOhAPy61q7ZGFQY,2548
4
+ codex_chat_bot/errors.py,sha256=G0PsXacnxetZM_3B8N0Xa-FSRIg1ipgxNvCwG8E0mz0,379
5
+ codex_chat_bot/session.py,sha256=4rMH40f5O5bmXX5ZZebLqkqz4GhUMmqMXHUycEmjQHs,7808
6
+ codex_chat_bot-0.1.0.dist-info/METADATA,sha256=0CPgFvNOzYo3NgTLAKoI62umnQNQGqPM6qMTY6hL-rE,3494
7
+ codex_chat_bot-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ codex_chat_bot-0.1.0.dist-info/entry_points.txt,sha256=dsb0UKu37uXDthxyU8SLnXqY-6y3a50QzLQRwCkz8SU,55
9
+ codex_chat_bot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codex-chat = codex_chat_bot.cli:main