From c133d63fbd9415b9afcea33ed78770176d8a0f2e Mon Sep 17 00:00:00 2001 From: Amir Motefaker <46513710+AmirMotefaker@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:37:42 +0330 Subject: [PATCH] feat: modernize OpenAI integration with Responses API (#3) --- .github/workflows/python-modernization.yml | 69 ++++++++++ .gitignore | 6 + ChatGPT_Web_Application.py | 141 +++++++++++---------- README.md | 126 ++++++++++++++---- evidence/phase5-openai-modernization.md | 43 +++++++ openai_service.py | 53 ++++++++ requirements.txt | 2 + tests/test_openai_service.py | 69 ++++++++++ 8 files changed, 416 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/python-modernization.yml create mode 100644 .gitignore create mode 100644 evidence/phase5-openai-modernization.md create mode 100644 openai_service.py create mode 100644 requirements.txt create mode 100644 tests/test_openai_service.py diff --git a/.github/workflows/python-modernization.yml b/.github/workflows/python-modernization.yml new file mode 100644 index 0000000..2880a0c --- /dev/null +++ b/.github/workflows/python-modernization.yml @@ -0,0 +1,69 @@ +name: Python modernization validation + +on: + pull_request: + push: + branches: + - main + - agent/openai-responses-modernization-2026-v1 + +permissions: + contents: read + +jobs: + validate: + name: validate (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.12" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: python -m pip install -r requirements.txt + + - name: Compile Python + run: python -m compileall -q . + + - name: Run offline unit tests + run: python -m unittest discover -s tests -v + + - name: Reject secrets and legacy API in modern entrypoints + shell: bash + run: | + set -euo pipefail + if grep -R -n -E \ + --exclude-dir=.git \ + --exclude='*.ipynb' \ + 'sk-[A-Za-z0-9_-]{20,}' .; then + echo "Potential API key detected." + exit 1 + fi + modern_files=(openai_service.py) + + if [[ -f chat.py ]]; then + modern_files+=(chat.py) + fi + + if [[ -f ChatGPT_Web_Application.py ]]; then + modern_files+=(ChatGPT_Web_Application.py) + fi + + if grep -n -E 'openai\.Completion|text-davinci-003' \ + "${modern_files[@]}"; then + echo "Legacy OpenAI API usage detected in a modern entrypoint." + exit 1 + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ead8c4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv/ +.env +__pycache__/ +*.py[cod] +.pytest_cache/ +.streamlit/secrets.toml diff --git a/ChatGPT_Web_Application.py b/ChatGPT_Web_Application.py index 47611a3..803ef5c 100644 --- a/ChatGPT_Web_Application.py +++ b/ChatGPT_Web_Application.py @@ -1,71 +1,82 @@ -# import streamlit as st +from __future__ import annotations -# st.title("ChatGPT-like Web App") -# #storing the chat -# if 'generated' not in st.session_state: -# st.session_state['generated'] = [] -# if 'past' not in st.session_state: -# st.session_state['past'] = [] -# user_input=st.text_input("You:",key='input') -# if user_input: -# output=generate_response(user_input) -# #store the output -# st.session_state['past'].append(user_input) -# st.session_state['generated'].append(output) -# if st.session_state['generated']: -# for i in range(len(st.session_state['generated'])-1, -1, -1): -# message(st.session_state["generated"][i], key=str(i)) -# message(st.session_state['past'][i], is_user=True, key=str(i) + '_user') +import os +import streamlit as st +from openai_service import DEFAULT_MODEL, generate_response +st.set_page_config( + page_title="ChatGPT-style Responses API Demo", + page_icon="💬", +) -import streamlit as st +st.title("ChatGPT-style Web Application") +st.caption("Modern OpenAI Responses API + Streamlit") + +if "messages" not in st.session_state: + st.session_state.messages = [] + +if "previous_response_id" not in st.session_state: + st.session_state.previous_response_id = None + +if "active_model" not in st.session_state: + st.session_state.active_model = DEFAULT_MODEL + +with st.sidebar: + st.header("Settings") + model = st.text_input( + "Model", + value=st.session_state.active_model, + help="Default comes from OPENAI_MODEL or falls back to gpt-5.5.", + ).strip() + + if not model: + model = DEFAULT_MODEL + + if model != st.session_state.active_model: + st.session_state.active_model = model + st.session_state.previous_response_id = None + st.session_state.messages = [] + st.info("Conversation reset because the model changed.") + + if st.button("Clear conversation", use_container_width=True): + st.session_state.messages = [] + st.session_state.previous_response_id = None + st.rerun() + + st.markdown("---") + if os.getenv("OPENAI_API_KEY"): + st.success("OPENAI_API_KEY detected.") + else: + st.warning("Set OPENAI_API_KEY in the environment before sending a message.") + +for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + +prompt = st.chat_input("Ask something...") + +if prompt: + st.session_state.messages.append({"role": "user", "content": prompt}) + + with st.chat_message("user"): + st.markdown(prompt) + + try: + with st.chat_message("assistant"): + with st.spinner("Thinking..."): + answer, response_id = generate_response( + prompt, + model=st.session_state.active_model, + previous_response_id=st.session_state.previous_response_id, + ) + st.markdown(answer) + + st.session_state.messages.append( + {"role": "assistant", "content": answer} + ) + st.session_state.previous_response_id = response_id -# this loop will let us ask questions continuously - -while True: - - # Set up the model and prompt - model_engine = "text-davinci-003" - - prompt = input('Enter new prompt: ') - - if 'exit' in prompt or 'quit' in prompt: - break - - # Generate a response - # given the most recent context (4096 characters) - # continue the text up to 2048 tokens ~ 8192 charaters - completion = openai.Completion.create( - engine=model_engine, - prompt=prompt, - max_tokens=1024, - n=1, - stop=None, - temperature=0.5, - ) - - # extracting useful part of response - response = completion.choices[0].text - - # printing response - print(response) - - -st.title("ChatGPT-like Web App") -#storing the chat -if 'generated' not in st.session_state: - st.session_state['generated'] = [] -if 'past' not in st.session_state: - st.session_state['past'] = [] -user_input=st.text_input("You:",key='input') -if user_input: - output=generate_response(user_input) - #store the output - st.session_state['past'].append(user_input) - st.session_state['generated'].append(output) -if st.session_state['generated']: - for i in range(len(st.session_state['generated'])-1, -1, -1): - message(st.session_state["generated"][i], key=str(i)) - message(st.session_state['past'][i], is_user=True, key=str(i) + '_user') + except Exception as exc: + st.error(f"OpenAI request failed: {type(exc).__name__}: {exc}") diff --git a/README.md b/README.md index 649cf26..729138e 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,122 @@ -# ChatGPT Web Application — Streamlit & Gradio Experiments +# ChatGPT Web Application — Modern Responses API + Legacy Experiments [![GitHub stars](https://img.shields.io/github/stars/AmirMotefaker/ChatGPT-Web-Application?style=flat&logo=github)](https://github.com/AmirMotefaker/ChatGPT-Web-Application/stargazers) [![GitHub forks](https://img.shields.io/github/forks/AmirMotefaker/ChatGPT-Web-Application?style=flat&logo=github)](https://github.com/AmirMotefaker/ChatGPT-Web-Application/network/members) -[![Python](https://img.shields.io/badge/Python-Streamlit%20%2B%20Gradio-3776AB?logo=python&logoColor=white)](https://www.python.org/) +[![Python modernization](https://github.com/AmirMotefaker/ChatGPT-Web-Application/actions/workflows/python-modernization.yml/badge.svg)](https://github.com/AmirMotefaker/ChatGPT-Web-Application/actions/workflows/python-modernization.yml) -An educational archive of early Python experiments for building ChatGPT-style interfaces with Streamlit, Gradio, Jupyter, Colab, and Kaggle. +A runnable Streamlit chat application using the modern OpenAI Responses API, while preserving the project's original Colab/Kaggle/Gradio notebooks as historical learning material. -> [!IMPORTANT] -> **Legacy educational project.** The current Python example uses `text-davinci-003` and `openai.Completion.create`, which belong to an older OpenAI API generation. The repository is valuable as a historical learning project, but the code should be modernized before being treated as a current production template. - -## Repository contents +## Modern 2026 application -| File | Purpose | -| --- | --- | -| [`ChatGPT_Web_Application.py`](ChatGPT_Web_Application.py) | Python/Streamlit experiment | -| [`ChatGPT_Web_Application_colab.ipynb`](ChatGPT_Web_Application_colab.ipynb) | Colab notebook | -| [`ChatGPT_Web_Application_using_Streamlit_colab.ipynb`](ChatGPT_Web_Application_using_Streamlit_colab.ipynb) | Streamlit-focused Colab example | -| [`ChatGPT_Web_Application_using_Gradio_colab.ipynb`](ChatGPT_Web_Application_using_Gradio_colab.ipynb) | Gradio-focused Colab example | -| [`chatgpt-web-application-kaggle.ipynb`](chatgpt-web-application-kaggle.ipynb) | Kaggle notebook | -| [`chatgpt-web-application-using-gradio.ipynb`](chatgpt-web-application-using-gradio.ipynb) | Gradio notebook | +[`ChatGPT_Web_Application.py`](ChatGPT_Web_Application.py) is now the supported application entrypoint. -## What the project demonstrates +The app uses: -- Prompt/response experimentation in Python -- Early OpenAI Completion API usage -- Streamlit interface concepts -- Gradio-based notebook experiments -- Jupyter/Colab/Kaggle workflows +- the official OpenAI Python SDK +- `client.responses.create(...)` +- `response.output_text` +- `OPENAI_API_KEY` from the environment +- `previous_response_id` for multi-turn conversation state +- Streamlit chat components +- `gpt-5.5` as the default model, overridable with `OPENAI_MODEL` or the sidebar -## Quick start for code review +### Setup ```bash git clone https://github.com/AmirMotefaker/ChatGPT-Web-Application.git cd ChatGPT-Web-Application +python -m venv .venv +``` + +Activate it: + +```powershell +# Windows PowerShell +.venv\Scripts\Activate.ps1 +``` + +```bash +# macOS / Linux +source .venv/bin/activate +``` + +Install dependencies: + +```bash +python -m pip install -r requirements.txt +``` + +Set the API key locally: + +```powershell +$env:OPENAI_API_KEY = "your-key-here" +``` + +or: + +```bash +export OPENAI_API_KEY="your-key-here" ``` -Review the notebooks and Python file before executing them. The API calls and model identifiers are legacy and may require modernization. +Run the app: + +```bash +streamlit run ChatGPT_Web_Application.py +``` + +The sidebar lets you change the model and clear the current conversation. Changing the model resets the stored response chain to avoid mixing conversation state across models. + +## Architecture + +```text +Streamlit UI + | + v +ChatGPT_Web_Application.py + | + v +openai_service.py + | + v +OpenAI Responses API +``` + +`openai_service.py` isolates the API call from the UI and makes the Responses API contract testable without making paid network requests. + +## Historical notebooks + +These files are preserved as an archive of the original project: + +| File | Status | +| --- | --- | +| `ChatGPT_Web_Application_colab.ipynb` | Legacy educational notebook | +| `ChatGPT_Web_Application_using_Streamlit_colab.ipynb` | Legacy Streamlit/Colab notebook | +| `ChatGPT_Web_Application_using_Gradio_colab.ipynb` | Legacy Gradio/Colab notebook | +| `chatgpt-web-application-kaggle.ipynb` | Legacy Kaggle notebook | +| `chatgpt-web-application-using-gradio.ipynb` | Legacy Gradio notebook | + +> [!IMPORTANT] +> The historical notebooks can contain old model names or deprecated OpenAI API patterns. The root Streamlit application is the modern supported path. + +## Validation + +```bash +python -m unittest discover -s tests -v +python -m compileall -q . +``` -> [!CAUTION] -> Do not hard-code or commit API credentials. Keep secrets outside the repository. +GitHub Actions validates the modernization on Python 3.10 and 3.12 and performs offline unit tests, syntax compilation, secret-pattern scanning, and a guard against legacy API usage in modern entrypoints. -## Modernization roadmap +## Security -The next code milestone should update the API integration, dependency management, secret handling, runnable examples, and basic validation. Keeping that work separate makes the repository history clear: **README modernization first, code modernization second.** +- Never hard-code an API key. +- Use `OPENAI_API_KEY` in your environment or secret manager. +- `.env` and Streamlit secret files are ignored. +- CI does not make live OpenAI API requests. ## Support the project -If these early Streamlit/Gradio experiments are useful as a learning reference, consider giving the repository a ⭐. +If the modern Streamlit example or legacy learning material helps you, consider giving the repository a ⭐. ## Author diff --git a/evidence/phase5-openai-modernization.md b/evidence/phase5-openai-modernization.md new file mode 100644 index 0000000..a28b953 --- /dev/null +++ b/evidence/phase5-openai-modernization.md @@ -0,0 +1,43 @@ +# Phase 5 - OpenAI Responses API Modernization Evidence + +Generated: **2026-08-13T22:07:19Z** + +## Lifecycle + +- Issue: #3 +- Branch: **agent/openai-responses-modernization-2026-v1** +- Target tag: **openai-modernization-v2026.08.14** +- Base main SHA: **320c64d03d5ea6abae36da1d4d0d3a4229e28e75** + +## Official API baseline used + +- OpenAI Python SDK minimum: **2.45.0** +- Primary model interaction API: **Responses API** +- Default example model: **gpt-5.5** +- Secret source: **OPENAI_API_KEY environment variable** +- Multi-turn state: **previous_response_id** + +Official references reviewed for this milestone: + +- https://github.com/openai/openai-python +- https://platform.openai.com/docs/api-reference/responses +- https://platform.openai.com/docs/guides/conversation-state + +## Modernization contract + +- [x] openai_service.py uses client.responses.create. +- [x] Modern code reads API credentials from the environment. +- [x] Historical notebooks are preserved. +- [x] Modern code has offline unit tests. +- [x] GitHub Actions validates Python 3.10 and 3.12. +- [x] CI rejects obvious API-key patterns in committed text files. +- [x] CI rejects openai.Completion and ext-davinci-003 in modern entrypoints. +- [x] CI does not make paid/live OpenAI API calls. + +## Local validation + +The publisher runs Python bytecode compilation before commit when a usable local Python 3 interpreter is available. + +GitHub Actions always performs authoritative dependency installation, Python compilation, and offline unit tests on Python 3.10 and 3.12. + +No API key, response content, private repository information, or paid API-call output is stored in this evidence. diff --git a/openai_service.py b/openai_service.py new file mode 100644 index 0000000..c748795 --- /dev/null +++ b/openai_service.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import os +from typing import Any + +from openai import OpenAI + +DEFAULT_MODEL = os.getenv("OPENAI_MODEL", "gpt-5.5") +DEFAULT_INSTRUCTIONS = ( + "You are a helpful AI assistant. Be accurate, concise, and explicit when " + "you are uncertain." +) + + +def _require_api_key() -> None: + if not os.getenv("OPENAI_API_KEY"): + raise RuntimeError( + "OPENAI_API_KEY is not set. Store the API key in your environment; " + "never commit it to source control." + ) + + +def generate_response( + prompt: str, + *, + model: str | None = None, + previous_response_id: str | None = None, + client: Any | None = None, +) -> tuple[str, str]: + cleaned_prompt = prompt.strip() + if not cleaned_prompt: + raise ValueError("Prompt must not be empty.") + + if client is None: + _require_api_key() + client = OpenAI() + + request: dict[str, Any] = { + "model": model or DEFAULT_MODEL, + "instructions": DEFAULT_INSTRUCTIONS, + "input": cleaned_prompt, + } + + if previous_response_id: + request["previous_response_id"] = previous_response_id + + response = client.responses.create(**request) + + output_text = (response.output_text or "").strip() + if not output_text: + output_text = "[The API returned no text output.]" + + return output_text, response.id diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..195d2e7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +openai>=2.45.0,<3.0.0 +streamlit>=1.57.0,<2.0.0 diff --git a/tests/test_openai_service.py b/tests/test_openai_service.py new file mode 100644 index 0000000..8360171 --- /dev/null +++ b/tests/test_openai_service.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import unittest +from types import SimpleNamespace + +import openai_service + + +class FakeResponses: + def __init__(self) -> None: + self.last_request = None + + def create(self, **kwargs): + self.last_request = kwargs + return SimpleNamespace( + output_text="hello from fake API", + id="resp_test_123", + ) + + +class FakeClient: + def __init__(self) -> None: + self.responses = FakeResponses() + + +class OpenAIServiceTests(unittest.TestCase): + def test_uses_responses_api_contract(self) -> None: + client = FakeClient() + + text, response_id = openai_service.generate_response( + "Hello", + model="gpt-5.5", + client=client, + ) + + self.assertEqual(text, "hello from fake API") + self.assertEqual(response_id, "resp_test_123") + self.assertEqual(client.responses.last_request["model"], "gpt-5.5") + self.assertEqual(client.responses.last_request["input"], "Hello") + self.assertIn("instructions", client.responses.last_request) + self.assertNotIn( + "previous_response_id", + client.responses.last_request, + ) + + def test_previous_response_id_is_forwarded(self) -> None: + client = FakeClient() + + openai_service.generate_response( + "Follow up", + previous_response_id="resp_previous", + client=client, + ) + + self.assertEqual( + client.responses.last_request["previous_response_id"], + "resp_previous", + ) + + def test_empty_prompt_is_rejected(self) -> None: + with self.assertRaises(ValueError): + openai_service.generate_response( + " ", + client=FakeClient(), + ) + + +if __name__ == "__main__": + unittest.main()