From fcb4253a70fa6c16712fa2d073de96c7dfc35955 Mon Sep 17 00:00:00 2001 From: DYung26 Date: Sun, 5 Apr 2026 09:19:24 +0100 Subject: [PATCH] chore(tests): fix test directory references in workflow and resolve linting violations in test files --- .coverage | Bin 114688 -> 114688 bytes .github/workflows/python-tests.yml | 6 +- plotsense/visual_suggestion/suggestions.py | 2 - test_logs/pytest.log | 372 +++++++++++++-------- tests/unit/test_hidden_api_input.py | 42 +-- tests/unit/test_provider_architecture.py | 72 ++-- tests/unit/test_provider_selection.py | 44 +-- tests/unit/test_registry_loader.py | 128 +++---- 8 files changed, 382 insertions(+), 284 deletions(-) diff --git a/.coverage b/.coverage index db29c51b3d6d50515f7eb17a0492d61c8305e80c..0c114285e72c719a6020f85d24dbf4a2bb587288 100644 GIT binary patch delta 12106 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 W_gU~oNSonv5#4F+o!=~f| delta 11872 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 Vv*`1LHqi+cp0u7_bOBX7{{I89>s$Z; diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 3f181f1..0c68178 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -43,7 +43,7 @@ jobs: env: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} run: | - uv run pytest test/ -v --cov=plotsense --cov-report=xml --cov-report=term-missing + uv run pytest tests/ -v --cov=plotsense --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' @@ -77,7 +77,7 @@ jobs: - name: Run unit tests only (fast) run: | - uv run pytest test/ -v -m "not slow" --tb=short + uv run pytest tests/ -v -m "not slow" --tb=short lint: name: Code Quality @@ -105,4 +105,4 @@ jobs: - name: Check test code quality run: | - uv run flake8 test --count --max-line-length=150 --statistics --extend-ignore=F401,F811 + uv run flake8 tests --count --max-line-length=150 --statistics --extend-ignore=F401,F811 diff --git a/plotsense/visual_suggestion/suggestions.py b/plotsense/visual_suggestion/suggestions.py index 9b85e2f..31b28f0 100644 --- a/plotsense/visual_suggestion/suggestions.py +++ b/plotsense/visual_suggestion/suggestions.py @@ -22,9 +22,7 @@ class VisualizationRecommender: 'groq': [ ('llama-3.3-70b-versatile', 0.5), # (model_name, weight) ('llama-3.1-8b-instant', 0.5) - ], - } def __init__(self, diff --git a/test_logs/pytest.log b/test_logs/pytest.log index ef7f095..9403a78 100644 --- a/test_logs/pytest.log +++ b/test_logs/pytest.log @@ -1,42 +1,42 @@ DEBUG matplotlib.font_manager:font_manager.py:1471 findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=10.0. -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Italic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Bold.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 0.33499999999999996 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBol.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-BoldOblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 1.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansDisplay.ttf', name='DejaVu Sans Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Bold.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymReg.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBolIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf', name='STIXGeneral', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerifDisplay.ttf', name='DejaVu Serif Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf', name='STIXGeneral', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 1.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymBol.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-BoldOblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 0.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymBol.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymBol.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneral.ttf', name='STIXGeneral', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmb10.ttf', name='cmb10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymReg.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmex10.ttf', name='cmex10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFiveSymReg.ttf', name='STIXSizeFiveSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmr10.ttf', name='cmr10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymBol.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf', name='STIXGeneral', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmsy10.ttf', name='cmsy10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymReg.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmtt10.ttf', name='cmtt10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmss10.ttf', name='cmss10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Bold.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-BoldItalic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmmi10.ttf', name='cmmi10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymReg.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUni.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Italic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Bold.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 0.33499999999999996 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBol.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-BoldOblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 1.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansDisplay.ttf', name='DejaVu Sans Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Bold.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymReg.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBolIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf', name='STIXGeneral', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerifDisplay.ttf', name='DejaVu Serif Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf', name='STIXGeneral', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 1.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymBol.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-BoldOblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 0.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymBol.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymBol.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneral.ttf', name='STIXGeneral', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmb10.ttf', name='cmb10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymReg.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmex10.ttf', name='cmex10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFiveSymReg.ttf', name='STIXSizeFiveSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmr10.ttf', name='cmr10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymBol.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf', name='STIXGeneral', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmsy10.ttf', name='cmsy10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymReg.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmtt10.ttf', name='cmtt10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmss10.ttf', name='cmss10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Bold.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-BoldItalic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmmi10.ttf', name='cmmi10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymReg.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUni.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/liberation/LiberationMono-Italic.ttf', name='Liberation Mono', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/TTF/Roboto-Light.ttf', name='Roboto', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/TTF/JetBrainsMonoNL-SemiBoldItalic.ttf', name='JetBrains Mono NL', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24 @@ -154,83 +154,83 @@ DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry( DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/liberation/LiberationSans-Regular.ttf', name='Liberation Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/gsfonts/NimbusSansNarrow-Regular.otf', name='Nimbus Sans Narrow', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/gsfonts/NimbusRoman-Italic.otf', name='Nimbus Roman', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1515 findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=10.0 to DejaVu Sans ('/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf') with score of 0.050000. -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-7fb41753-25be-4e9a-9872-2f8db4ddd1b0', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': 'ping'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 5}} -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG matplotlib.font_manager:font_manager.py:1515 findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=10.0 to DejaVu Sans ('/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf') with score of 0.050000. +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-46553c92-79b6-4c6d-8d53-ff4df6f516fc', 'json_data': {'messages': [{'role': 'user', 'content': 'ping'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 5}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=5.0 socket_options=None -DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= -DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=5.0 -DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=5.0 +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:37 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14399'), (b'x-ratelimit-remaining-tokens', b'5959'), (b'x-ratelimit-reset-requests', b'6s'), (b'x-ratelimit-reset-tokens', b'410ms'), (b'x-request-id', b'req_01kncdw09vfjd959xmdwt813dw'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=EK17JVQFVXDxuKBJeuovie1y_TnZ.L2Y1aMSPzCAs_I-1775312437.556789-1.0.1.1-TYbX8HRseb6hhvR83nH.ve80RvFwIUVdw02wPFFjmUMfoviVBymNuVCBwa8URVlDwQrtTY1yg31QqceWJ_5VC8D.p3KN38ii9EP5jVFgGXY58T_CZo4XRlQULYk.mHFG; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:37 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f46eb9b4edec-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:48 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14399'), (b'x-ratelimit-remaining-tokens', b'5959'), (b'x-ratelimit-reset-requests', b'6s'), (b'x-ratelimit-reset-tokens', b'410ms'), (b'x-request-id', b'req_01kneb91p4fcestkpadz2nzq3e'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=mZ3QZHfY652t5iLiJutceaNy5JtS5zu9iOL4Sd6n9e8-1775376828.0928154-1.0.1.1-l1CNT0oq1YAnVVYKGtJx0rMuhnCucFMRVJBtmkploj1TNPLes70q9yu9CrTYKbWW0Auct4qeF.d3XKy5FXlqwg3w.9904PO2L6GUvFmthSG21Q6EN709C_xTKXxJUgxp; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:48 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e771877991b132e-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:37 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14399', 'x-ratelimit-remaining-tokens': '5959', 'x-ratelimit-reset-requests': '6s', 'x-ratelimit-reset-tokens': '410ms', 'x-request-id': 'req_01kncdw09vfjd959xmdwt813dw', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=EK17JVQFVXDxuKBJeuovie1y_TnZ.L2Y1aMSPzCAs_I-1775312437.556789-1.0.1.1-TYbX8HRseb6hhvR83nH.ve80RvFwIUVdw02wPFFjmUMfoviVBymNuVCBwa8URVlDwQrtTY1yg31QqceWJ_5VC8D.p3KN38ii9EP5jVFgGXY58T_CZo4XRlQULYk.mHFG; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:37 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f46eb9b4edec-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG openai._base_client:_base_client.py:486 Request options: {'method': 'post', 'url': '/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-7484c26c-82dd-4a3f-8113-2c285817e772', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': 'ping'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 5}} +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:48 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14399', 'x-ratelimit-remaining-tokens': '5959', 'x-ratelimit-reset-requests': '6s', 'x-ratelimit-reset-tokens': '410ms', 'x-request-id': 'req_01kneb91p4fcestkpadz2nzq3e', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=mZ3QZHfY652t5iLiJutceaNy5JtS5zu9iOL4Sd6n9e8-1775376828.0928154-1.0.1.1-l1CNT0oq1YAnVVYKGtJx0rMuhnCucFMRVJBtmkploj1TNPLes70q9yu9CrTYKbWW0Auct4qeF.d3XKy5FXlqwg3w.9904PO2L6GUvFmthSG21Q6EN709C_xTKXxJUgxp; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:48 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e771877991b132e-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG openai._base_client:_base_client.py:486 Request options: {'method': 'post', 'url': '/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-6ebd99bf-7fb2-413c-b591-17eec3f3373a', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': 'ping'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 5}} DEBUG openai._base_client:_base_client.py:1001 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=5.0 socket_options=None -DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= -DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=5.0 -DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=5.0 +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:38 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14398'), (b'x-ratelimit-remaining-tokens', b'5959'), (b'x-ratelimit-reset-requests', b'12s'), (b'x-ratelimit-reset-tokens', b'410ms'), (b'x-request-id', b'req_01kncdw169fjd9vdxjzcyrcga4'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=_RwMLeADD3S9MAAfhoKQ2GMevCb8kfwYKcSEnT7LsLg-1775312438.4663794-1.0.1.1-yW3.MtPBXG1iwF7y.vIyuwfXsszeohlurQNVkvyx8mNcDosle7rSUR375rPrF9ccDSVlBmW_5mYW4H26IsomUmG1.Bud1abcXHCAQtD8gncqWd6YzQ3T0KIuBoh6BIoJ; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:38 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f4746fb088ef-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:49 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14398'), (b'x-ratelimit-remaining-tokens', b'5959'), (b'x-ratelimit-reset-requests', b'12s'), (b'x-ratelimit-reset-tokens', b'410ms'), (b'x-request-id', b'req_01kneb92v4fcernp2r36rnh240'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=ZiFjW8YW2ratqqIqja874RGOCkrg5MyphMcJBQYhaEk-1775376829.278896-1.0.1.1-LF5p739thSpMWf28YTe8I1oNRh5Qjh0xElCY101JArH6DQJX0zthJ5BUkbNarSvmphYyq62jtv_1ceqh6CikGwUo8g6ZCAHHZgZU59qYCK6RzsOP0m9Iw_fr5As76JwC; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:49 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e77187efa36b29c-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG openai._base_client:_base_client.py:1039 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:38 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14398', 'x-ratelimit-remaining-tokens': '5959', 'x-ratelimit-reset-requests': '12s', 'x-ratelimit-reset-tokens': '410ms', 'x-request-id': 'req_01kncdw169fjd9vdxjzcyrcga4', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=_RwMLeADD3S9MAAfhoKQ2GMevCb8kfwYKcSEnT7LsLg-1775312438.4663794-1.0.1.1-yW3.MtPBXG1iwF7y.vIyuwfXsszeohlurQNVkvyx8mNcDosle7rSUR375rPrF9ccDSVlBmW_5mYW4H26IsomUmG1.Bud1abcXHCAQtD8gncqWd6YzQ3T0KIuBoh6BIoJ; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:38 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f4746fb088ef-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG openai._base_client:_base_client.py:1047 request_id: req_01kncdw169fjd9vdxjzcyrcga4 +DEBUG openai._base_client:_base_client.py:1039 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:49 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14398', 'x-ratelimit-remaining-tokens': '5959', 'x-ratelimit-reset-requests': '12s', 'x-ratelimit-reset-tokens': '410ms', 'x-request-id': 'req_01kneb92v4fcernp2r36rnh240', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=ZiFjW8YW2ratqqIqja874RGOCkrg5MyphMcJBQYhaEk-1775376829.278896-1.0.1.1-LF5p739thSpMWf28YTe8I1oNRh5Qjh0xElCY101JArH6DQJX0zthJ5BUkbNarSvmphYyq62jtv_1ceqh6CikGwUo8g6ZCAHHZgZU59qYCK6RzsOP0m9Iw_fr5As76JwC; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:49 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e77187efa36b29c-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG openai._base_client:_base_client.py:1047 request_id: req_01kneb92v4fcernp2r36rnh240 DEBUG matplotlib.font_manager:font_manager.py:1471 findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=12.0. -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Italic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Bold.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 0.33499999999999996 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBol.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-BoldOblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 1.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansDisplay.ttf', name='DejaVu Sans Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Bold.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymReg.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBolIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf', name='STIXGeneral', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerifDisplay.ttf', name='DejaVu Serif Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf', name='STIXGeneral', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 1.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymBol.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-BoldOblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 0.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymBol.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymBol.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneral.ttf', name='STIXGeneral', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmb10.ttf', name='cmb10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymReg.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmex10.ttf', name='cmex10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFiveSymReg.ttf', name='STIXSizeFiveSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmr10.ttf', name='cmr10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymBol.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf', name='STIXGeneral', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmsy10.ttf', name='cmsy10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymReg.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmtt10.ttf', name='cmtt10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmss10.ttf', name='cmss10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Bold.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-BoldItalic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmmi10.ttf', name='cmmi10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymReg.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUni.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Italic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Bold.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 0.33499999999999996 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBol.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-BoldOblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 1.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansDisplay.ttf', name='DejaVu Sans Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Bold.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymReg.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUniBolIta.ttf', name='STIXNonUnicode', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf', name='STIXGeneral', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerifDisplay.ttf', name='DejaVu Serif Display', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf', name='STIXGeneral', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf', name='DejaVu Sans', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 1.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymBol.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-BoldOblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf', name='DejaVu Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 0.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymBol.ttf', name='STIXSizeThreeSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymBol.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneral.ttf', name='STIXGeneral', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmb10.ttf', name='cmb10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono.ttf', name='DejaVu Sans Mono', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymReg.ttf', name='STIXSizeTwoSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmex10.ttf', name='cmex10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFiveSymReg.ttf', name='STIXSizeFiveSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmr10.ttf', name='cmr10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymBol.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf', name='STIXGeneral', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmsy10.ttf', name='cmsy10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizOneSymReg.ttf', name='STIXSizeOneSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmtt10.ttf', name='cmtt10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmss10.ttf', name='cmss10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Bold.ttf', name='DejaVu Serif', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')) = 10.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSerif-BoldItalic.ttf', name='DejaVu Serif', style='italic', variant='normal', weight=700, stretch='normal', size='scalable')) = 11.335 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/cmmi10.ttf', name='cmmi10', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXSizFourSymReg.ttf', name='STIXSizeFourSym', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf', name='DejaVu Sans Mono', style='oblique', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 +DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/STIXNonUni.ttf', name='STIXNonUnicode', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/liberation/LiberationMono-Italic.ttf', name='Liberation Mono', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/TTF/Roboto-Light.ttf', name='Roboto', style='normal', variant='normal', weight=300, stretch='normal', size='scalable')) = 10.145 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/TTF/JetBrainsMonoNL-SemiBoldItalic.ttf', name='JetBrains Mono NL', style='italic', variant='normal', weight=600, stretch='normal', size='scalable')) = 11.24 @@ -348,118 +348,218 @@ DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry( DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/liberation/LiberationSans-Regular.ttf', name='Liberation Sans', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')) = 10.05 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/gsfonts/NimbusSansNarrow-Regular.otf', name='Nimbus Sans Narrow', style='normal', variant='normal', weight=400, stretch='condensed', size='scalable')) = 10.25 DEBUG matplotlib.font_manager:font_manager.py:1483 findfont: score(FontEntry(fname='/usr/share/fonts/gsfonts/NimbusRoman-Italic.otf', name='Nimbus Roman', style='italic', variant='normal', weight=400, stretch='normal', size='scalable')) = 11.05 -DEBUG matplotlib.font_manager:font_manager.py:1515 findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=12.0 to DejaVu Sans ('/home/dyung/.local/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf') with score of 0.050000. -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-c8330a54-1d05-40f7-ae4f-a76ea0890f17', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n Explanation Generation Requirements:\n - Provide a comprehensive analysis of the data visualization\n - Use a structured format with these sections:\n 1. Overview\n 2. Key Features\n 3. Insights and Patterns\n 4. Conclusion\n - Be specific and data-driven\n - Highlight key statistical and visual elements\n\n Specific Prompt: Explain what this plot shows\n\n Formatting Instructions:\n - Use markdown-style headers\n - Include bullet points for clarity\n - Provide quantitative insights\n - Explain the significance of visual elements\n '}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.7}} -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG matplotlib.font_manager:font_manager.py:1515 findfont: Matching sans\-serif:style=normal:variant=normal:weight=normal:stretch=normal:size=12.0 to DejaVu Sans ('/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf') with score of 0.050000. +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-97d8a877-b406-48b0-8a62-aa8e37543597', 'json_data': {'messages': [{'role': 'user', 'content': '\n Explanation Generation Requirements:\n - Provide a comprehensive analysis of the data visualization\n - Use a structured format with these sections:\n 1. Overview\n 2. Key Features\n 3. Insights and Patterns\n 4. Conclusion\n - Be specific and data-driven\n - Highlight key statistical and visual elements\n\n Specific Prompt: Explain what this plot shows\n\n Formatting Instructions:\n - Use markdown-style headers\n - Include bullet points for clarity\n - Provide quantitative insights\n - Explain the significance of visual elements\n '}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.7}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:42 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14397'), (b'x-ratelimit-remaining-tokens', b'5363'), (b'x-ratelimit-reset-requests', b'18s'), (b'x-ratelimit-reset-tokens', b'6.37s'), (b'x-request-id', b'req_01kncdw297f8zr9h62wwhbavcm'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f47b685cedec-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:51 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14397'), (b'x-ratelimit-remaining-tokens', b'4850'), (b'x-ratelimit-reset-requests', b'18s'), (b'x-ratelimit-reset-tokens', b'11.5s'), (b'x-request-id', b'req_01kneb940rfjys2fssmsarbfcc'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e7718868830132e-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:42 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14397', 'x-ratelimit-remaining-tokens': '5363', 'x-ratelimit-reset-requests': '18s', 'x-ratelimit-reset-tokens': '6.37s', 'x-request-id': 'req_01kncdw297f8zr9h62wwhbavcm', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f47b685cedec-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-353918ae-f863-402a-bbcd-9aade33d1bde', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n Explanation Generation Requirements:\n - Provide a comprehensive analysis of the data visualization\n - Use a structured format with these sections:\n 1. Overview\n 2. Key Features\n 3. Insights and Patterns\n 4. Conclusion\n - Be specific and data-driven\n - Highlight key statistical and visual elements\n\n Specific Prompt: What does this plot visualize?\n\n Formatting Instructions:\n - Use markdown-style headers\n - Include bullet points for clarity\n - Provide quantitative insights\n - Explain the significance of visual elements\n '}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.7}} -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:51 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14397', 'x-ratelimit-remaining-tokens': '4850', 'x-ratelimit-reset-requests': '18s', 'x-ratelimit-reset-tokens': '11.5s', 'x-request-id': 'req_01kneb940rfjys2fssmsarbfcc', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e7718868830132e-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-ab5aa91d-3389-4db1-9d92-656803bc7043', 'json_data': {'messages': [{'role': 'user', 'content': '\n Explanation Generation Requirements:\n - Provide a comprehensive analysis of the data visualization\n - Use a structured format with these sections:\n 1. Overview\n 2. Key Features\n 3. Insights and Patterns\n 4. Conclusion\n - Be specific and data-driven\n - Highlight key statistical and visual elements\n\n Specific Prompt: What does this plot visualize?\n\n Formatting Instructions:\n - Use markdown-style headers\n - Include bullet points for clarity\n - Provide quantitative insights\n - Explain the significance of visual elements\n '}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.7}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:43 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14396'), (b'x-ratelimit-remaining-tokens', b'5017'), (b'x-ratelimit-reset-requests', b'24s'), (b'x-ratelimit-reset-tokens', b'9.83s'), (b'x-request-id', b'req_01kncdw51tfjd90ye0fmnk5csv'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f48d2dffedec-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:52 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14396'), (b'x-ratelimit-remaining-tokens', b'3292'), (b'x-ratelimit-reset-requests', b'24s'), (b'x-ratelimit-reset-tokens', b'27.08s'), (b'x-request-id', b'req_01kneb956qfjys0cn4q3tfcehq'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e77188e1818132e-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:43 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14396', 'x-ratelimit-remaining-tokens': '5017', 'x-ratelimit-reset-requests': '24s', 'x-ratelimit-reset-tokens': '9.83s', 'x-request-id': 'req_01kncdw51tfjd90ye0fmnk5csv', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f48d2dffedec-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-601493a3-8f1f-4123-bbf4-ed3e54b88e48', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n Explanation Generation Requirements:\n - Provide a comprehensive analysis of the data visualization\n - Use a structured format with these sections:\n 1. Overview\n 2. Key Features\n 3. Insights and Patterns\n 4. Conclusion\n - Be specific and data-driven\n - Highlight key statistical and visual elements\n\n Specific Prompt: Analyze this plot in detail\n\n Formatting Instructions:\n - Use markdown-style headers\n - Include bullet points for clarity\n - Provide quantitative insights\n - Explain the significance of visual elements\n '}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.7}} -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:52 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14396', 'x-ratelimit-remaining-tokens': '3292', 'x-ratelimit-reset-requests': '24s', 'x-ratelimit-reset-tokens': '27.08s', 'x-request-id': 'req_01kneb956qfjys0cn4q3tfcehq', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e77188e1818132e-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'files': None, 'idempotency_key': 'stainless-python-retry-1a3b6014-6052-43da-adc7-c034744588de', 'json_data': {'messages': [{'role': 'user', 'content': '\n Explanation Generation Requirements:\n - Provide a comprehensive analysis of the data visualization\n - Use a structured format with these sections:\n 1. Overview\n 2. Key Features\n 3. Insights and Patterns\n 4. Conclusion\n - Be specific and data-driven\n - Highlight key statistical and visual elements\n\n Specific Prompt: Analyze this plot in detail\n\n Formatting Instructions:\n - Use markdown-style headers\n - Include bullet points for clarity\n - Provide quantitative insights\n - Explain the significance of visual elements\n '}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.7}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:46 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14395'), (b'x-ratelimit-remaining-tokens', b'4386'), (b'x-ratelimit-reset-requests', b'30s'), (b'x-ratelimit-reset-tokens', b'16.14s'), (b'x-request-id', b'req_01kncdw6w8f90980q6qrsk3by4'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f498da37edec-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:53 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14395'), (b'x-ratelimit-remaining-tokens', b'2928'), (b'x-ratelimit-reset-requests', b'30s'), (b'x-ratelimit-reset-tokens', b'30.72s'), (b'x-request-id', b'req_01kneb961yfcfs0tjwyw3ghnz6'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e7718938c2b132e-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:46 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14395', 'x-ratelimit-remaining-tokens': '4386', 'x-ratelimit-reset-requests': '30s', 'x-ratelimit-reset-tokens': '16.14s', 'x-request-id': 'req_01kncdw6w8f90980q6qrsk3by4', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f498da37edec-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-434ddc74-f91b-4ac2-ac3b-f72c88aee48a', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.3-70b-versatile', 'max_tokens': 1000, 'temperature': 0.4}} -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-e3c5f9b0-f511-428a-98d9-2400a9d5d622', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.4}} -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:53 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14395', 'x-ratelimit-remaining-tokens': '2928', 'x-ratelimit-reset-requests': '30s', 'x-ratelimit-reset-tokens': '30.72s', 'x-request-id': 'req_01kneb961yfcfs0tjwyw3ghnz6', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e7718938c2b132e-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-bdab1162-a415-4b5b-a08d-8e87bdd6cf99', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.3-70b-versatile', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-552cb215-fa75-41d4-aa92-f10e1d7e5dcd', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None -DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= -DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 -DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:47 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'1000'), (b'x-ratelimit-limit-tokens', b'12000'), (b'x-ratelimit-remaining-requests', b'998'), (b'x-ratelimit-remaining-tokens', b'10506'), (b'x-ratelimit-reset-requests', b'2m52.8s'), (b'x-ratelimit-reset-tokens', b'7.47s'), (b'x-request-id', b'req_01kncdw9cvfjdab6dz580x48wv'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=pXcPb9sbX1eYBx0myjzh6VVgzJy2DXv1.CpEnuccIbo-1775312446.8698468-1.0.1.1-a7CiYz5yspDl25vgAnieuKW.F2RvQ6dEB2HbP4C.WTRMMx0gB7N6HfkH5W32dijX.tsd5h4sKVMom6NFtrOF88AUNZ9xQ0uR1rNm3UcCgmDitIjRnTQRGU5Jw_vsWA8k; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:47 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f4a8ed48ef3e-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= +DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete +DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= +DEBUG httpcore.http11:_trace.py:47 send_request_body.complete +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:54 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14394'), (b'x-ratelimit-remaining-tokens', b'1245'), (b'x-ratelimit-reset-requests', b'36s'), (b'x-ratelimit-reset-tokens', b'47.55s'), (b'x-request-id', b'req_01kneb97h4fcgs22spkt383z4v'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=57ndxoLJHlEzRl2VZFsS8FELNmNojQi8j8W7m9KG.ng-1775376834.0703776-1.0.1.1-BbAk9MPIgf5l5doxpa6hVkmIlv5mA4ZMBlZLGDofG58bHtuW02.463OHoTzficIWL03lpRm2UXXi6rUMhwouvLUPPPRUtp_J_km_zn8g8PztFJB4SHG_wBaIOOJwaU6v; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:54 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e77189cef4d60fb-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" +DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= +DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete +DEBUG httpcore.http11:_trace.py:47 response_closed.started +DEBUG httpcore.http11:_trace.py:47 response_closed.complete +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:54 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14394', 'x-ratelimit-remaining-tokens': '1245', 'x-ratelimit-reset-requests': '36s', 'x-ratelimit-reset-tokens': '47.55s', 'x-request-id': 'req_01kneb97h4fcgs22spkt383z4v', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=57ndxoLJHlEzRl2VZFsS8FELNmNojQi8j8W7m9KG.ng-1775376834.0703776-1.0.1.1-BbAk9MPIgf5l5doxpa6hVkmIlv5mA4ZMBlZLGDofG58bHtuW02.463OHoTzficIWL03lpRm2UXXi6rUMhwouvLUPPPRUtp_J_km_zn8g8PztFJB4SHG_wBaIOOJwaU6v; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:54 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e77189cef4d60fb-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:55 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'1000'), (b'x-ratelimit-limit-tokens', b'12000'), (b'x-ratelimit-remaining-requests', b'999'), (b'x-ratelimit-remaining-tokens', b'9717'), (b'x-ratelimit-reset-requests', b'1m26.4s'), (b'x-ratelimit-reset-tokens', b'11.415s'), (b'x-request-id', b'req_01kneb97gqf1h9wmk6zsyd0q64'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=AlHWkioPFpH6lXUSxrnsReDm7cYwzO1C3c_kAbcN99A-1775376834.057053-1.0.1.1-n58EfDur9GXgZtncdYpKEmCxI6GyUkljgdMPAIL3nMI7x_QHXp8rZDx1jrm9OCY2055ezPlmoFKoGSYgPt2kVKNkYTFU13G6l7mAPgdemt84pDgGu3yh_X_rID12WQat; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:55 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e77189cde26ccc0-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:47 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '1000', 'x-ratelimit-limit-tokens': '12000', 'x-ratelimit-remaining-requests': '998', 'x-ratelimit-remaining-tokens': '10506', 'x-ratelimit-reset-requests': '2m52.8s', 'x-ratelimit-reset-tokens': '7.47s', 'x-request-id': 'req_01kncdw9cvfjdab6dz580x48wv', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=pXcPb9sbX1eYBx0myjzh6VVgzJy2DXv1.CpEnuccIbo-1775312446.8698468-1.0.1.1-a7CiYz5yspDl25vgAnieuKW.F2RvQ6dEB2HbP4C.WTRMMx0gB7N6HfkH5W32dijX.tsd5h4sKVMom6NFtrOF88AUNZ9xQ0uR1rNm3UcCgmDitIjRnTQRGU5Jw_vsWA8k; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:47 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f4a8ed48ef3e-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= -DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 -DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:55 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '1000', 'x-ratelimit-limit-tokens': '12000', 'x-ratelimit-remaining-requests': '999', 'x-ratelimit-remaining-tokens': '9717', 'x-ratelimit-reset-requests': '1m26.4s', 'x-ratelimit-reset-tokens': '11.415s', 'x-request-id': 'req_01kneb97gqf1h9wmk6zsyd0q64', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=AlHWkioPFpH6lXUSxrnsReDm7cYwzO1C3c_kAbcN99A-1775376834.057053-1.0.1.1-n58EfDur9GXgZtncdYpKEmCxI6GyUkljgdMPAIL3nMI7x_QHXp8rZDx1jrm9OCY2055ezPlmoFKoGSYgPt2kVKNkYTFU13G6l7mAPgdemt84pDgGu3yh_X_rID12WQat; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:55 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e77189cde26ccc0-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-fa875230-116a-42f4-8ed6-6b28d95bfd29', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.3-70b-versatile', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-5dd34e58-10d1-46c4-af86-cd5fabd9ec86', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:49 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14394'), (b'x-ratelimit-remaining-tokens', b'2879'), (b'x-ratelimit-reset-requests', b'36s'), (b'x-ratelimit-reset-tokens', b'31.21s'), (b'x-request-id', b'req_01kncdwbh2f90seq64e0pnq008'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=QjKIML0VBkgUp133oBCKUqXIGQcm5jwVGVEWPoqHwfY-1775312449.0459406-1.0.1.1-3BtmMpDjrzBvFmCrEbmO79ybNacPL5m_8qBVRgab_mY1UcVOgYtCeyl05cxNcEepzi2AalB51n741LQvWM21kpo9tw7Q80pjx7fJAU4bF9iVwC.JKU98i0vmsRytP73x; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:49 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f4b68d3e955d-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= +DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete +DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= +DEBUG httpcore.http11:_trace.py:47 send_request_body.complete +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 429, b'Too Many Requests', [(b'Date', b'Sun, 05 Apr 2026 08:13:55 GMT'), (b'Content-Type', b'application/json'), (b'Content-Length', b'378'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'retry-after', b'5'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14394'), (b'x-ratelimit-remaining-tokens', b'1212'), (b'x-ratelimit-reset-requests', b'36s'), (b'x-ratelimit-reset-tokens', b'47.88s'), (b'x-request-id', b'req_01kneb991mfjzsja3emf2dsa3y'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=j6xcNrdXiXW618wrjzXR7nMjAIoWvDeyJodT_SYwIs8-1775376835.627033-1.0.1.1-bC3ZpPJ8Yf2MPzJxIeMiCymRnvltcJ2cY3LUConZgQFd5qVnbfSnnSyVgmFhhsYMz5IUC0BJbAw9YQHgArikiAIVIRTO6I__W1Jkx3ppB6JfvtuUo5bGzyOQVD317xzG; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:55 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'CF-RAY', b'9e7718a6ac569488-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 429 Too Many Requests" +DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= +DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete +DEBUG httpcore.http11:_trace.py:47 response_closed.started +DEBUG httpcore.http11:_trace.py:47 response_closed.complete +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "429 Too Many Requests" Headers({'date': 'Sun, 05 Apr 2026 08:13:55 GMT', 'content-type': 'application/json', 'content-length': '378', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'retry-after': '5', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14394', 'x-ratelimit-remaining-tokens': '1212', 'x-ratelimit-reset-requests': '36s', 'x-ratelimit-reset-tokens': '47.88s', 'x-request-id': 'req_01kneb991mfjzsja3emf2dsa3y', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=j6xcNrdXiXW618wrjzXR7nMjAIoWvDeyJodT_SYwIs8-1775376835.627033-1.0.1.1-bC3ZpPJ8Yf2MPzJxIeMiCymRnvltcJ2cY3LUConZgQFd5qVnbfSnnSyVgmFhhsYMz5IUC0BJbAw9YQHgArikiAIVIRTO6I__W1Jkx3ppB6JfvtuUo5bGzyOQVD317xzG; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:55 GMT', 'strict-transport-security': 'max-age=15552000', 'cf-ray': '9e7718a6ac569488-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:1026 Encountered httpx.HTTPStatusError +Traceback (most recent call last): + File "/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/groq/_base_client.py", line 1024, in request + response.raise_for_status() + ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/httpx/_models.py", line 829, in raise_for_status + raise HTTPStatusError(message, request=request, response=self) +httpx.HTTPStatusError: Client error '429 Too Many Requests' for url 'https://api.groq.com/openai/v1/chat/completions' +For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 +DEBUG groq._base_client:_base_client.py:772 Retrying due to status code 429 +DEBUG groq._base_client:_base_client.py:1065 2 retries left +INFO groq._base_client:_base_client.py:1068 Retrying request to /openai/v1/chat/completions in 5.000000 seconds +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:13:56 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'1000'), (b'x-ratelimit-limit-tokens', b'12000'), (b'x-ratelimit-remaining-requests', b'998'), (b'x-ratelimit-remaining-tokens', b'7558'), (b'x-ratelimit-reset-requests', b'2m52.8s'), (b'x-ratelimit-reset-tokens', b'22.209s'), (b'x-request-id', b'req_01kneb990qfjzrxv74y2j38bb2'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=QnYA4iUo7sbZhf8eloWe_uLhvnqMdyzFqhB3RiIEbgQ-1775376835.6019356-1.0.1.1-.g1dM1MRN6mDOhJvmfm9putMRvdqnAhdFGiEL9zhVs3DxmNqfP03Gol2.bN7UMM9mihHBDOQQZbpDx2gdKV4o8vQh1ksYeSFDR8Wr9Fp6.UZRRUXivR_O9BYnjf8xnu8; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:56 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e7718a68b84c51c-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:49 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14394', 'x-ratelimit-remaining-tokens': '2879', 'x-ratelimit-reset-requests': '36s', 'x-ratelimit-reset-tokens': '31.21s', 'x-request-id': 'req_01kncdwbh2f90seq64e0pnq008', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=QjKIML0VBkgUp133oBCKUqXIGQcm5jwVGVEWPoqHwfY-1775312449.0459406-1.0.1.1-3BtmMpDjrzBvFmCrEbmO79ybNacPL5m_8qBVRgab_mY1UcVOgYtCeyl05cxNcEepzi2AalB51n741LQvWM21kpo9tw7Q80pjx7fJAU4bF9iVwC.JKU98i0vmsRytP73x; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:49 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f4b68d3e955d-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-56d2f689-feae-488f-8edc-5f5c17810e85', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.3-70b-versatile', 'max_tokens': 1000, 'temperature': 0.4}} -DEBUG groq._base_client:_base_client.py:484 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-829eb1cb-9280-46ba-b07f-d411b5f49644', 'content': None, 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.4}} -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions -DEBUG groq._base_client:_base_client.py:999 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions -DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None -DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None -DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= -DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 -DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= -DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 -DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:13:56 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '1000', 'x-ratelimit-limit-tokens': '12000', 'x-ratelimit-remaining-requests': '998', 'x-ratelimit-remaining-tokens': '7558', 'x-ratelimit-reset-requests': '2m52.8s', 'x-ratelimit-reset-tokens': '22.209s', 'x-request-id': 'req_01kneb990qfjzrxv74y2j38bb2', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=QnYA4iUo7sbZhf8eloWe_uLhvnqMdyzFqhB3RiIEbgQ-1775376835.6019356-1.0.1.1-.g1dM1MRN6mDOhJvmfm9putMRvdqnAhdFGiEL9zhVs3DxmNqfP03Gol2.bN7UMM9mihHBDOQQZbpDx2gdKV4o8vQh1ksYeSFDR8Wr9Fp6.UZRRUXivR_O9BYnjf8xnu8; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:43:56 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e7718a68b84c51c-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-5dd34e58-10d1-46c4-af86-cd5fabd9ec86', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (4, 4)\nColumns (4): Year, Sales, Profit, Category\n\nColumn Details:\n- Year: numerical (4 unique values), sample: [2020, 2021, 2022]\n Stats: min=2020, max=2023, mean=2021.50, missing=0\n- Sales: numerical (4 unique values), sample: [150, 200, 250]\n Stats: min=150, max=300, mean=225.00, missing=0\n- Profit: numerical (4 unique values), sample: [40, 50, 65]\n Stats: min=40, max=80, mean=58.75, missing=0\n- Category: text/other (2 unique values), sample: [\'A\', \'B\', \'A\']\n\nNumerical Variable Correlations (Pearson):\n Year Sales Profit\nYear 1.0 1.0 1.0\nSales 1.0 1.0 1.0\nProfit 1.0 1.0 1.0\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG httpcore.connection:_trace.py:47 close.started +DEBUG httpcore.connection:_trace.py:47 close.complete DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sun, 05 Apr 2026 08:14:01 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14394'), (b'x-ratelimit-remaining-tokens', b'35'), (b'x-ratelimit-reset-requests', b'36s'), (b'x-ratelimit-reset-tokens', b'59.65s'), (b'x-request-id', b'req_01kneb9e39fk2rhaxj19gkcg1b'), (b'cf-cache-status', b'DYNAMIC'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e7718c6fba0c51c-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" +DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= +DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete +DEBUG httpcore.http11:_trace.py:47 response_closed.started +DEBUG httpcore.http11:_trace.py:47 response_closed.complete +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sun, 05 Apr 2026 08:14:01 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14394', 'x-ratelimit-remaining-tokens': '35', 'x-ratelimit-reset-requests': '36s', 'x-ratelimit-reset-tokens': '59.65s', 'x-request-id': 'req_01kneb9e39fk2rhaxj19gkcg1b', 'cf-cache-status': 'DYNAMIC', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e7718c6fba0c51c-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG httpcore.connection:_trace.py:47 close.started +DEBUG httpcore.connection:_trace.py:47 close.complete +INFO matplotlib.category:category.py:224 Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting. +INFO matplotlib.category:category.py:224 Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting. +DEBUG httpcore.connection:_trace.py:47 close.started +DEBUG httpcore.connection:_trace.py:47 close.complete +DEBUG httpcore.connection:_trace.py:47 close.started +DEBUG httpcore.connection:_trace.py:47 close.complete +INFO matplotlib.category:category.py:224 Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting. +INFO matplotlib.category:category.py:224 Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting. +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-f1b96489-5768-4347-bcac-7832ad405006', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (100, 5)\nColumns (5): date, category, value, count, flag\n\nColumn Details:\n- date: datetime (100 unique values), sample: [Timestamp(\'2020-01-01 00:00:00\'), Timestamp(\'2020-01-02 00:00:00\'), Timestamp(\'2020-01-03 00:00:00\')]\n Range: 2020-01-01 00:00:00 to 2020-04-09 00:00:00, missing=0\n- category: categorical (3 unique values), sample: [\'A\', \'C\', \'B\']\n- value: numerical (100 unique values), sample: [1.4493448012363035, -0.37494468984913537, 0.37305231843417824]\n Stats: min=-2.7779942807285725, max=2.710745527218326, mean=-0.05, missing=0\n- count: numerical (60 unique values), sample: [12, 97, 61]\n Stats: min=3, max=97, mean=50.65, missing=0\n- flag: numerical (2 unique values), sample: [True, False, True]\n Stats: min=False, max=True, mean=0.45, missing=0\n\nNumerical Variable Correlations (Pearson):\n value count\nvalue 1.00 0.02\ncount 0.02 1.00\n\nPotential Groupings (categorical vs numerical):\n - Could group by: [\'category\', \'flag\']\n - To analyze: [\'value\', \'count\']\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.3-70b-versatile', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:480 Request options: {'method': 'post', 'url': '/openai/v1/chat/completions', 'timeout': 30, 'files': None, 'idempotency_key': 'stainless-python-retry-1c891984-9866-4555-933d-ce48671bf56b', 'json_data': {'messages': [{'role': 'user', 'content': '\n You are a data visualization expert analyzing this dataset:\n\n DataFrame Shape: (100, 5)\nColumns (5): date, category, value, count, flag\n\nColumn Details:\n- date: datetime (100 unique values), sample: [Timestamp(\'2020-01-01 00:00:00\'), Timestamp(\'2020-01-02 00:00:00\'), Timestamp(\'2020-01-03 00:00:00\')]\n Range: 2020-01-01 00:00:00 to 2020-04-09 00:00:00, missing=0\n- category: categorical (3 unique values), sample: [\'A\', \'C\', \'B\']\n- value: numerical (100 unique values), sample: [1.4493448012363035, -0.37494468984913537, 0.37305231843417824]\n Stats: min=-2.7779942807285725, max=2.710745527218326, mean=-0.05, missing=0\n- count: numerical (60 unique values), sample: [12, 97, 61]\n Stats: min=3, max=97, mean=50.65, missing=0\n- flag: numerical (2 unique values), sample: [True, False, True]\n Stats: min=False, max=True, mean=0.45, missing=0\n\nNumerical Variable Correlations (Pearson):\n value count\nvalue 1.00 0.02\ncount 0.02 1.00\n\nPotential Groupings (categorical vs numerical):\n - Could group by: [\'category\', \'flag\']\n - To analyze: [\'value\', \'count\']\n\n Recommend 5 insightful visualizations using matplotlib\'s plotting functions.\n For each suggestion, follow this exact format:\n\n Plot Type: \n Variables: \n Rationale: <1-2 sentences explaining why this visualization is useful>\n ---\n\n CRITICAL VARIABLE ORDERING RULES:\n 1. If a suggestion includes both numerical and categorical variables, NUMERICAL VARIABLES MUST COME FIRST.\n - Correct: "income, gender"\n - Incorrect: "gender, income"\n 2. For plots requiring two numerical variables (e.g., scatter), order by analysis priority (dependent variable first).\n 3. For single-variable plots, use natural order (e.g., "age" for a histogram).\n\n GENERAL RULES FOR ALL PLOT TYPES:\n 1. Ensure the plot type is a valid matplotlib function\n 2. The plot type must be appropriate for the variables\' data types\n 3. The number of variables must match what the plot type requires\n 4. Variables must exist in the dataset\n 5. Never combine incompatible variables\n 6. Always specify complete variable sets\n 7. Ensure plot type names are in lowercase and match matplotlib\'s naming conventions eg hist for histogram, bar for barplot\n 8. Ensure the common plot types requirements are met including the data types\n\n COMMON PLOT TYPE REQUIREMENTS (non-exhaustive):\n 1. bar: 1 categorical (x) + 1 numerical (y) → Variables: [numerical], [categorical]\n 2. scatter: Exactly 2 numerical → Variables: [independent], [dependent]\n 3. hist: Exactly 1 numerical → Variables: [numerical]\n 4. boxplot: 1 numerical OR 1 numerical + 1 categorical → Variables: [numerical], [categorical] (if grouped)\n 5. pie: Exactly 1 categorical → Variables: [categorical]\n 6. line: 1 numerical (y) OR 1 numerical (y) + 1 datetime (x) → Variables: [y], [x] (if applicable)\n 7. heatmap: 2 categorical + 1 numerical OR correlation matrix → Variables: [numerical], [categorical], [categorical]\n 8. violinplot: Same as boxplot\n 9. hexbin: Exactly 2 numerical variables\n 10. pairplot: 2+ numerical variables\n 11. jointplot: Exactly 2 numerical variables\n 12. contour: 2 numerical variables for grid + 1 for values\n 13. quiver: 2 numerical variables for grid + 2 for vectors\n 14. imshow: 2D array of numerical values\n 15. errorbar: 1 numerical (x) + 1 numerical (y) + error values\n 16. stackplot: 1 numerical (x) + multiple numerical (y)\n 17. stem: 1 numerical (x) + 1 numerical (y)\n 18. fill_between: 1 numerical (x) + 2 numerical (y)\n 19. pcolormesh: 2D grid of numerical values\n 20. polar: Angular and radial coordinates\n\n If suggesting a plot not listed above, ensure:\n - The function exists in matplotlib\n - Variable types and counts are explicitly compatible\n - The rationale clearly explains the insight provided\n\n Additional Requirements:\n 1. For specialized plots (like quiver, contour), ensure all required components are specified\n 2. Consider the statistical properties and relationships of the variables\n 3. Suggest plots that would reveal meaningful insights about the data\n 4. Include both common and advanced plots when appropriate\n\n Example CORRECT suggestions (NUMERICAL FIRST):\n Plot Type: boxplot\n Variables: income, gender\n Rationale: Compares income distribution across genders\n ---\n Plot Type: scatter\n Variables: age, income\n Rationale: Shows relationship between age and income\n ---\n Plot Type: bar\n Variables: revenue, product_category\n Rationale: Compares revenue across product categories\n\n Example INCORRECT suggestions (REJECT THESE):\n Plot Type: boxplot\n Variables: gender, income # WRONG - categorical listed first\n ---\n Plot Type: scatter\n Variables: price, weight # WRONG - no clear priority order\n Rationale: Should specify independent/dependent variable order\n'}], 'model': 'llama-3.1-8b-instant', 'max_tokens': 1000, 'temperature': 0.4}} +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG groq._base_client:_base_client.py:976 Sending HTTP Request: POST https://api.groq.com/openai/v1/chat/completions +DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None +DEBUG httpcore.connection:_trace.py:47 connect_tcp.started host='api.groq.com' port=443 local_address=None timeout=30 socket_options=None +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= DEBUG httpcore.http11:_trace.py:47 send_request_body.complete DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:50 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'14400'), (b'x-ratelimit-limit-tokens', b'6000'), (b'x-ratelimit-remaining-requests', b'14393'), (b'x-ratelimit-remaining-tokens', b'1688'), (b'x-ratelimit-reset-requests', b'42s'), (b'x-ratelimit-reset-tokens', b'43.12s'), (b'x-request-id', b'req_01kncdwcddfjd8rsypy9zbf9vt'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=10isrjWwoZekdYecNPUlE_MAXobwb3R38rK9XpexDDg-1775312449.9462852-1.0.1.1-fhaT9Vw6mjfXUxS2FQtTiE.DudeWG.QAVFzhkELSd4yV4Yc3IyO8SBxIzs3SKEXgxH71VwZVYkItwDNa46XHy8X2ePntzivvHWEgzFwdBZNFL06UihCCxOQFqi71VnE.; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:50 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f4bc2dde412e-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) -INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 401, b'Unauthorized', [(b'Date', b'Sun, 05 Apr 2026 08:14:12 GMT'), (b'Content-Type', b'application/json'), (b'Content-Length', b'96'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-request-id', b'req_01kneb9sd8fk4vbcw8p82jqaqn'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=b6Tw7zsBdMCGdSuF8CoYYwuw.Vm3M4I8tiVf7LE4uWU-1775376852.383536-1.0.1.1-CA7uBGT1hLthy5bPv8uM4laSgRbd9hvSLgJfSxiAAVoROClMvK0uidnDsSEMAyb.HQ6c2nwOqbML6CD0giRk_e_UrxHrfcCWJqSRCxL8oP1A.v7jW5BdUyOpKju.hKEW; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:44:12 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'CF-RAY', b'9e77190f6ec8cd57-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 401 Unauthorized" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:50 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '14400', 'x-ratelimit-limit-tokens': '6000', 'x-ratelimit-remaining-requests': '14393', 'x-ratelimit-remaining-tokens': '1688', 'x-ratelimit-reset-requests': '42s', 'x-ratelimit-reset-tokens': '43.12s', 'x-request-id': 'req_01kncdwcddfjd8rsypy9zbf9vt', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=10isrjWwoZekdYecNPUlE_MAXobwb3R38rK9XpexDDg-1775312449.9462852-1.0.1.1-fhaT9Vw6mjfXUxS2FQtTiE.DudeWG.QAVFzhkELSd4yV4Yc3IyO8SBxIzs3SKEXgxH71VwZVYkItwDNa46XHy8X2ePntzivvHWEgzFwdBZNFL06UihCCxOQFqi71VnE.; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:50 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f4bc2dde412e-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) -DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Sat, 04 Apr 2026 14:20:50 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-ratelimit-limit-requests', b'1000'), (b'x-ratelimit-limit-tokens', b'12000'), (b'x-ratelimit-remaining-requests', b'997'), (b'x-ratelimit-remaining-tokens', b'8859'), (b'x-ratelimit-reset-requests', b'4m19.2s'), (b'x-ratelimit-reset-tokens', b'15.704s'), (b'x-request-id', b'req_01kncdwccye0tv7pv16jfmc1rg'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=u6fn0xxU7wjtoImCeFPSPuIIAhD1Q6TcJ8PFzxHOxWQ-1775312449.9405215-1.0.1.1-LKDsYWrRTVroSd8bHgrToZolLJX40X4NFZmOAwtSosywbXojKapWhH6QObxdmpppXyAlCCb0O3RXgAcZbONrNcGP6Y6U5qnHtQLkLHx1GNL5woEdpopaX1Ej8Lfxos.p; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:50 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'Content-Encoding', b'gzip'), (b'CF-RAY', b'9e70f4bc18864f00-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) -INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 200 OK" +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "401 Unauthorized" Headers({'date': 'Sun, 05 Apr 2026 08:14:12 GMT', 'content-type': 'application/json', 'content-length': '96', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-request-id': 'req_01kneb9sd8fk4vbcw8p82jqaqn', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=b6Tw7zsBdMCGdSuF8CoYYwuw.Vm3M4I8tiVf7LE4uWU-1775376852.383536-1.0.1.1-CA7uBGT1hLthy5bPv8uM4laSgRbd9hvSLgJfSxiAAVoROClMvK0uidnDsSEMAyb.HQ6c2nwOqbML6CD0giRk_e_UrxHrfcCWJqSRCxL8oP1A.v7jW5BdUyOpKju.hKEW; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:44:12 GMT', 'strict-transport-security': 'max-age=15552000', 'cf-ray': '9e77190f6ec8cd57-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:1026 Encountered httpx.HTTPStatusError +Traceback (most recent call last): + File "/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/groq/_base_client.py", line 1024, in request + response.raise_for_status() + ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/httpx/_models.py", line 829, in raise_for_status + raise HTTPStatusError(message, request=request, response=self) +httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'https://api.groq.com/openai/v1/chat/completions' +For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 +DEBUG groq._base_client:_base_client.py:780 Not retrying +DEBUG groq._base_client:_base_client.py:1043 Re-raising status error +DEBUG httpcore.connection:_trace.py:47 connect_tcp.complete return_value= +DEBUG httpcore.connection:_trace.py:47 start_tls.started ssl_context= server_hostname='api.groq.com' timeout=30 +DEBUG httpcore.connection:_trace.py:47 start_tls.complete return_value= +DEBUG httpcore.http11:_trace.py:47 send_request_headers.started request= +DEBUG httpcore.http11:_trace.py:47 send_request_headers.complete +DEBUG httpcore.http11:_trace.py:47 send_request_body.started request= +DEBUG httpcore.http11:_trace.py:47 send_request_body.complete +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.started request= +DEBUG httpcore.http11:_trace.py:47 receive_response_headers.complete return_value=(b'HTTP/1.1', 401, b'Unauthorized', [(b'Date', b'Sun, 05 Apr 2026 08:14:13 GMT'), (b'Content-Type', b'application/json'), (b'Content-Length', b'96'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'private, max-age=0, no-store, no-cache, must-revalidate'), (b'Server', b'cloudflare'), (b'vary', b'Origin'), (b'x-groq-region', b'lhr'), (b'x-request-id', b'req_01kneb9tgafk5am9hf40vt8h20'), (b'cf-cache-status', b'DYNAMIC'), (b'set-cookie', b'__cf_bm=Jg.JUCKedqzjFRIrOe7zvRkETtHl.7VPQgB9GIV4LdQ-1775376853.49944-1.0.1.1-FGcgcub8xmGpRYz6v0OTRbigw.DNpr9E6KlucA5zDFtGjXOdYxSrb2NBS2lpxg8foiywqh2IYBl0L1NUCvCxEySQ5xyzdCIZ.gS8JTsNetwaxJrGruplxwkAFLpPtBV1; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:44:13 GMT'), (b'Strict-Transport-Security', b'max-age=15552000'), (b'CF-RAY', b'9e77191658969cfb-LHR'), (b'alt-svc', b'h3=":443"; ma=86400')]) +INFO httpx:_client.py:1025 HTTP Request: POST https://api.groq.com/openai/v1/chat/completions "HTTP/1.1 401 Unauthorized" DEBUG httpcore.http11:_trace.py:47 receive_response_body.started request= DEBUG httpcore.http11:_trace.py:47 receive_response_body.complete DEBUG httpcore.http11:_trace.py:47 response_closed.started DEBUG httpcore.http11:_trace.py:47 response_closed.complete -DEBUG groq._base_client:_base_client.py:1037 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "200 OK" Headers({'date': 'Sat, 04 Apr 2026 14:20:50 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-ratelimit-limit-requests': '1000', 'x-ratelimit-limit-tokens': '12000', 'x-ratelimit-remaining-requests': '997', 'x-ratelimit-remaining-tokens': '8859', 'x-ratelimit-reset-requests': '4m19.2s', 'x-ratelimit-reset-tokens': '15.704s', 'x-request-id': 'req_01kncdwccye0tv7pv16jfmc1rg', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=u6fn0xxU7wjtoImCeFPSPuIIAhD1Q6TcJ8PFzxHOxWQ-1775312449.9405215-1.0.1.1-LKDsYWrRTVroSd8bHgrToZolLJX40X4NFZmOAwtSosywbXojKapWhH6QObxdmpppXyAlCCb0O3RXgAcZbONrNcGP6Y6U5qnHtQLkLHx1GNL5woEdpopaX1Ej8Lfxos.p; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sat, 04 Apr 2026 14:50:50 GMT', 'strict-transport-security': 'max-age=15552000', 'content-encoding': 'gzip', 'cf-ray': '9e70f4bc18864f00-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:1014 HTTP Response: POST https://api.groq.com/openai/v1/chat/completions "401 Unauthorized" Headers({'date': 'Sun, 05 Apr 2026 08:14:13 GMT', 'content-type': 'application/json', 'content-length': '96', 'connection': 'keep-alive', 'cache-control': 'private, max-age=0, no-store, no-cache, must-revalidate', 'server': 'cloudflare', 'vary': 'Origin', 'x-groq-region': 'lhr', 'x-request-id': 'req_01kneb9tgafk5am9hf40vt8h20', 'cf-cache-status': 'DYNAMIC', 'set-cookie': '__cf_bm=Jg.JUCKedqzjFRIrOe7zvRkETtHl.7VPQgB9GIV4LdQ-1775376853.49944-1.0.1.1-FGcgcub8xmGpRYz6v0OTRbigw.DNpr9E6KlucA5zDFtGjXOdYxSrb2NBS2lpxg8foiywqh2IYBl0L1NUCvCxEySQ5xyzdCIZ.gS8JTsNetwaxJrGruplxwkAFLpPtBV1; HttpOnly; Secure; Path=/; Domain=groq.com; Expires=Sun, 05 Apr 2026 08:44:13 GMT', 'strict-transport-security': 'max-age=15552000', 'cf-ray': '9e77191658969cfb-LHR', 'alt-svc': 'h3=":443"; ma=86400'}) +DEBUG groq._base_client:_base_client.py:1026 Encountered httpx.HTTPStatusError +Traceback (most recent call last): + File "/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/groq/_base_client.py", line 1024, in request + response.raise_for_status() + ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/home/dyung/Projects/PlotKit/.venv/lib/python3.13/site-packages/httpx/_models.py", line 829, in raise_for_status + raise HTTPStatusError(message, request=request, response=self) +httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'https://api.groq.com/openai/v1/chat/completions' +For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 +DEBUG groq._base_client:_base_client.py:780 Not retrying +DEBUG groq._base_client:_base_client.py:1043 Re-raising status error +DEBUG httpcore.connection:_trace.py:47 close.started +DEBUG httpcore.connection:_trace.py:47 close.complete +DEBUG httpcore.connection:_trace.py:47 close.started +DEBUG httpcore.connection:_trace.py:47 close.complete diff --git a/tests/unit/test_hidden_api_input.py b/tests/unit/test_hidden_api_input.py index b411625..2830bfa 100644 --- a/tests/unit/test_hidden_api_input.py +++ b/tests/unit/test_hidden_api_input.py @@ -7,16 +7,16 @@ class TestHiddenAPIKeyInput: """Tests that API key input is hidden from terminal/logs.""" - + def test_uses_getpass_not_input(self): """Verify that getpass.getpass is used instead of input().""" with patch('getpass.getpass', return_value='hidden_key') as mock_getpass: key = prompt_for_api_key('groq', 'https://example.com', interactive=True) - + # Verify getpass was called mock_getpass.assert_called_once() assert key == 'hidden_key' - + def test_getpass_masks_input(self): """Verify getpass hides input from terminal.""" # getpass.getpass() does not echo input to terminal @@ -24,25 +24,25 @@ def test_getpass_masks_input(self): # and used instead of builtins.input with patch('getpass.getpass', return_value='secret_api_key') as mock_getpass: key = prompt_for_api_key('openai', 'https://openai.com', interactive=True) - + # The getpass function is called with appropriate prompt call_args = mock_getpass.call_args[0][0] assert 'OPENAI' in call_args.upper() assert key == 'secret_api_key' - + def test_hidden_input_with_skip(self): """Hidden input should work with skip_if_missing.""" with patch('getpass.getpass', return_value='') as mock_getpass: key = prompt_for_api_key( - 'anthropic', + 'anthropic', 'https://console.anthropic.com/keys', interactive=True, skip_if_missing=True ) - + mock_getpass.assert_called_once() assert key is None - + def test_hidden_input_required_empty_raises(self): """Empty hidden input should raise when key is required.""" with patch('getpass.getpass', return_value='') as mock_getpass: @@ -53,28 +53,28 @@ def test_hidden_input_required_empty_raises(self): interactive=True, skip_if_missing=False ) - + mock_getpass.assert_called_once() - + def test_hidden_input_strips_whitespace(self): """Hidden input should have whitespace stripped.""" - with patch('getpass.getpass', return_value=' api_key_with_spaces ') as mock_getpass: + with patch('getpass.getpass', return_value=' api_key_with_spaces '): key = prompt_for_api_key('groq', 'https://console.groq.com/keys', interactive=True) - + # Whitespace should be stripped assert key == 'api_key_with_spaces' assert not key.startswith(' ') assert not key.endswith(' ') - + def test_hidden_input_non_interactive_no_getpass(self): """Non-interactive mode should not call getpass.""" with patch('getpass.getpass') as mock_getpass: with pytest.raises(ValueError): prompt_for_api_key('groq', 'https://console.groq.com/keys', interactive=False) - + # getpass should not be called in non-interactive mode mock_getpass.assert_not_called() - + def test_hidden_input_eof_error(self): """Handle EOF when getpass is used.""" with patch('getpass.getpass', side_effect=EOFError): @@ -84,7 +84,7 @@ def test_hidden_input_eof_error(self): 'https://aistudio.google.com/app/apikey', interactive=True ) - + def test_hidden_input_os_error(self): """Handle OSError when getpass is used.""" with patch('getpass.getpass', side_effect=OSError): @@ -94,23 +94,23 @@ def test_hidden_input_os_error(self): 'https://portal.azure.com', interactive=True ) - + def test_hidden_input_multiple_calls(self): """Multiple hidden inputs should each use getpass.""" with patch('getpass.getpass', side_effect=['key1', 'key2', 'key3']): key1 = prompt_for_api_key('groq', 'https://example.com', interactive=True) key2 = prompt_for_api_key('openai', 'https://example.com', interactive=True) key3 = prompt_for_api_key('anthropic', 'https://example.com', interactive=True) - + assert key1 == 'key1' assert key2 == 'key2' assert key3 == 'key3' - + def test_hidden_input_with_special_characters(self): """Hidden input should handle special characters in API keys.""" special_key = 'sk-key_with-special.chars+/=abc123' - + with patch('getpass.getpass', return_value=special_key): key = prompt_for_api_key('openai', 'https://platform.openai.com/api-keys', interactive=True) - + assert key == special_key diff --git a/tests/unit/test_provider_architecture.py b/tests/unit/test_provider_architecture.py index a6f897a..067263d 100644 --- a/tests/unit/test_provider_architecture.py +++ b/tests/unit/test_provider_architecture.py @@ -15,7 +15,7 @@ class TestProviderArchitecture: """Test provider naming and registration.""" - + def test_provider_names_follow_vendor_variant_format(self): """All providers should use vendor_variant naming (e.g., groq_default, openai_chat).""" for vendor, variants in ProviderManager.SUPPORTED_PROVIDERS.items(): @@ -25,7 +25,7 @@ def test_provider_names_follow_vendor_variant_format(self): assert "_" in full_name, f"Provider {full_name} doesn't follow vendor_variant format" vendor_part = full_name.split("_")[0] assert vendor_part == vendor, f"Vendor part mismatch: {vendor_part} != {vendor}" - + def test_all_vendors_have_variants(self): """Every vendor should have at least one variant registered.""" for vendor, variants in ProviderManager.SUPPORTED_PROVIDERS.items(): @@ -35,7 +35,7 @@ def test_all_vendors_have_variants(self): class TestAIModelInterfaceRouting: """Test AIModelInterface correctly routes to providers based on vendor.""" - + def test_vendor_extraction_from_provider_name(self): """Test that vendor is correctly extracted from full provider name.""" test_cases = [ @@ -47,17 +47,17 @@ def test_vendor_extraction_from_provider_name(self): ("azure_default", "azure"), ("ollama_default", "ollama"), ] - + for full_name, expected_vendor in test_cases: vendor = full_name.split("_")[0].lower() assert vendor == expected_vendor, f"Vendor extraction failed for {full_name}" - + def test_groq_vendor_routing(self): """Test that groq_default is routed to Groq handler.""" mock_manager = MagicMock() mock_manager.providers = {"groq_default": MagicMock()} mock_manager.query.return_value = "test response" - + ai = AIModelInterface(mock_manager) result = ai.query_model( provider="groq_default", @@ -66,13 +66,13 @@ def test_groq_vendor_routing(self): ) # Should call manager.query (no ValueError about unknown provider) assert result == "test response" - + def test_openai_chat_routing(self): """Test that openai_chat is routed correctly.""" mock_manager = MagicMock() mock_manager.providers = {"openai_chat": MagicMock()} mock_manager.query.return_value = "test response" - + ai = AIModelInterface(mock_manager) result = ai.query_model( provider="openai_chat", @@ -80,13 +80,13 @@ def test_openai_chat_routing(self): prompt="test" ) assert result == "test response" - + def test_openai_response_routing(self): """Test that openai_response variant is routed correctly.""" mock_manager = MagicMock() mock_manager.providers = {"openai_response": MagicMock()} mock_manager.query.return_value = "test response" - + ai = AIModelInterface(mock_manager) result = ai.query_model( provider="openai_response", @@ -98,53 +98,53 @@ def test_openai_response_routing(self): class TestMessageFormatting: """Test that messages are formatted based on vendor type.""" - + def test_groq_message_format_simple(self): """Groq messages should be simple role/content pairs (text-only).""" mock_manager = MagicMock() ai = AIModelInterface(mock_manager) - + messages = ai._build_messages( vendor="groq", model="llama-3.1-8b-instant", prompt="test prompt" ) - + # Should be simple format without nested content structures assert isinstance(messages, list) assert len(messages) == 1 assert messages[0]["role"] == "user" assert messages[0]["content"] == "test prompt" assert isinstance(messages[0]["content"], str) - + def test_openai_message_format(self): """OpenAI messages should include system message.""" mock_manager = MagicMock() ai = AIModelInterface(mock_manager) - + messages = ai._build_messages( vendor="openai", model="gpt-4", prompt="test prompt" ) - + # Should include system message assert isinstance(messages, list) assert len(messages) >= 2 assert messages[0]["role"] == "system" assert messages[1]["role"] == "user" - + def test_anthropic_message_format(self): """Anthropic messages should follow Claude format.""" mock_manager = MagicMock() ai = AIModelInterface(mock_manager) - + messages = ai._build_messages( vendor="anthropic", model="claude-3-opus", prompt="test prompt" ) - + # Should not have system role (Claude doesn't use it in messages) assert isinstance(messages, list) assert messages[0]["role"] == "user" @@ -152,10 +152,10 @@ def test_anthropic_message_format(self): class TestErrorHandling: """Test error handling in query_model.""" - + def test_no_finally_override_bug(self): """The finally block should NOT always return an error. - + This was a critical bug where finally always returned an error, overriding all successful query responses. """ @@ -164,45 +164,45 @@ def test_no_finally_override_bug(self): mock_provider.query.return_value = "successful response" mock_manager.providers = {"groq_default": mock_provider} mock_manager.query.return_value = "successful response" - + ai = AIModelInterface(mock_manager) result = ai.query_model( provider="groq_default", model="llama-3.1-8b-instant", prompt="test" ) - + # Should return actual response, not "No valid query handler found" error assert "No valid query handler found" not in result assert result == "successful response" - + def test_unknown_provider_raises_error(self): """Unknown providers should raise ValueError early.""" mock_manager = MagicMock() mock_manager.providers = {"groq_default": MagicMock()} - + ai = AIModelInterface(mock_manager) - + with pytest.raises(ValueError, match="Unknown provider"): ai.query_model( provider="nonexistent_provider", model="some-model", prompt="test" ) - + def test_query_exception_returns_error_message(self): """Query exceptions should return error string, not raise.""" mock_manager = MagicMock() mock_manager.providers = {"groq_default": MagicMock()} mock_manager.query.side_effect = RuntimeError("API error") - + ai = AIModelInterface(mock_manager) result = ai.query_model( provider="groq_default", model="llama-3.1-8b-instant", prompt="test" ) - + # Should return error message, not raise assert isinstance(result, str) assert "Error:" in result @@ -211,25 +211,25 @@ def test_query_exception_returns_error_message(self): class TestProviderVariants: """Test handling of multiple variants per vendor.""" - + def test_openai_variants_coexist(self): """OpenAI has chat and response variants - both should be registered.""" variants = ProviderManager.SUPPORTED_PROVIDERS.get("openai", {}) assert "chat" in variants, "OpenAI chat variant missing" assert "response" in variants, "OpenAI response variant missing" assert len(variants) == 2 - + def test_groq_variants_coexist(self): """Groq has native API and OpenAI-compatible variants.""" variants = ProviderManager.SUPPORTED_PROVIDERS.get("groq", {}) assert "default" in variants, "Groq default (native API) variant missing" assert "openai" in variants, "Groq OpenAI-compatible variant missing" assert len(variants) == 2 - + def test_single_variant_vendors(self): """Vendors with single variant should have 'default' as variant name.""" single_variant_vendors = {"anthropic", "gemini", "azure", "ollama"} - + for vendor in single_variant_vendors: variants = ProviderManager.SUPPORTED_PROVIDERS.get(vendor, {}) assert "default" in variants, f"{vendor} missing 'default' variant" @@ -238,16 +238,16 @@ def test_single_variant_vendors(self): class TestMultiProviderConsistency: """Test that all providers work consistently with the new architecture.""" - + def test_all_providers_have_vendor_variant_names(self): """Every registered provider should follow vendor_variant naming.""" providers_to_init = set() - + for vendor, variants in ProviderManager.SUPPORTED_PROVIDERS.items(): for variant_name in variants.keys(): full_name = f"{vendor}_{variant_name}" providers_to_init.add(full_name) - + # Verify all follow the pattern for provider_name in providers_to_init: parts = provider_name.split("_") diff --git a/tests/unit/test_provider_selection.py b/tests/unit/test_provider_selection.py index 645ab11..260792c 100644 --- a/tests/unit/test_provider_selection.py +++ b/tests/unit/test_provider_selection.py @@ -13,7 +13,7 @@ class TestProviderInitialization: """Test provider initialization with api_keys as source of truth.""" - + def test_only_provided_providers_initialized(self, monkeypatch): """Should only initialize providers that have API keys.""" # Mock the provider classes to avoid real API validation @@ -29,11 +29,11 @@ def test_only_provided_providers_initialized(self, monkeypatch): api_keys={'groq': 'groq-key'}, interactive=False ) - + # Only groq should be initialized assert len(manager.providers) > 0 assert any('groq' in p for p in manager.providers.keys()) - + def test_missing_provider_key_not_prompted(self, capsys): """Should not prompt for providers not in api_keys.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -45,15 +45,15 @@ def test_missing_provider_key_not_prompted(self, capsys): } }): # Create manager with only groq key and interactive mode - manager = ProviderManager( + ProviderManager( api_keys={'groq': 'groq-key'}, interactive=True ) - + captured = capsys.readouterr() # Should not prompt for openai since it wasn't provided assert 'Enter OPENAI' not in captured.out - + def test_selected_models_requires_valid_provider(self): """Should raise clear error if selected_models references missing-key provider.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -70,7 +70,7 @@ def test_selected_models_requires_valid_provider(self): interactive=False, selected_providers=['openai'] # openai has no key ) - + def test_restrict_to_filters_available_providers(self): """Should filter models within providers that have keys.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -84,14 +84,14 @@ def test_restrict_to_filters_available_providers(self): interactive=False, selected_providers=['groq'] ) - + # Should successfully restrict to groq (which has a key) assert manager.providers is not None class TestEdgeCases: """Test edge cases and error conditions.""" - + def test_empty_api_keys_no_selected_providers_raises_error(self): """With no api_keys and no selected_providers, should raise error.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', {}): @@ -101,7 +101,7 @@ def test_empty_api_keys_no_selected_providers_raises_error(self): interactive=True, selected_providers=None ) - + def test_selected_provider_with_empty_key_raises_error_non_interactive(self): """Selected provider with empty key and non-interactive should raise error.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -115,7 +115,7 @@ def test_selected_provider_with_empty_key_raises_error_non_interactive(self): interactive=False, selected_providers=['groq'] ) - + def test_unknown_provider_in_restrict_to_raises_error(self): """Should reject unknown providers in restrict_to.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -137,7 +137,7 @@ def test_unknown_provider_in_restrict_to_raises_error(self): class TestSelectedModelsSourceOfTruth: """Test that selected_models determines provider selection, not api_keys.""" - + def test_selected_models_without_keys_interactive_prompts(self): """With selected_providers and no keys, should prompt interactively.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -147,18 +147,18 @@ def test_selected_models_without_keys_interactive_prompts(self): }): with patch('plotsense.core.providers.provider_manager.prompt_for_api_key') as mock_prompt: mock_prompt.return_value = 'gsk_provided' - + manager = ProviderManager( api_keys={}, # No keys interactive=True, selected_providers=['groq'] ) - + # Should have called prompt assert mock_prompt.called # Should have initialized groq assert 'groq_default' in manager.providers - + def test_api_keys_alone_ignored_when_selected_providers_given(self): """When selected_providers is given, only those providers are initialized.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -176,7 +176,7 @@ def test_api_keys_alone_ignored_when_selected_providers_given(self): interactive=False, selected_providers=['openai'] # only openai selected ) - + def test_unselected_provider_never_prompted(self): """Unselected providers should never be prompted for.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -189,18 +189,18 @@ def test_unselected_provider_never_prompted(self): }): with patch('plotsense.core.providers.provider_manager.prompt_for_api_key') as mock_prompt: mock_prompt.return_value = 'gsk_provided' - - manager = ProviderManager( + + ProviderManager( api_keys={}, interactive=True, selected_providers=['groq'] # Only groq selected ) - + # Should only have prompted for groq, not openai calls = [c[0][0].lower() for c in mock_prompt.call_args_list] assert 'groq' in calls assert 'openai' not in calls - + def test_selected_provider_missing_key_non_interactive_error(self): """Selected provider with missing key and non-interactive should raise error.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -214,7 +214,7 @@ def test_selected_provider_missing_key_non_interactive_error(self): interactive=False, selected_providers=['openai'] # openai selected but no key ) - + def test_user_skips_selected_provider_in_interactive_raises_error(self): """If user skips prompt for selected provider, should raise error.""" with patch('plotsense.core.providers.provider_manager.ProviderManager.SUPPORTED_PROVIDERS', { @@ -224,7 +224,7 @@ def test_user_skips_selected_provider_in_interactive_raises_error(self): }): with patch('plotsense.core.providers.provider_manager.prompt_for_api_key') as mock_prompt: mock_prompt.return_value = None # User skipped - + with pytest.raises(ValueError, match="API key required"): ProviderManager( api_keys={}, diff --git a/tests/unit/test_registry_loader.py b/tests/unit/test_registry_loader.py index f656b0e..024129e 100644 --- a/tests/unit/test_registry_loader.py +++ b/tests/unit/test_registry_loader.py @@ -9,52 +9,52 @@ class TestRegistryLoaderBundled: """Tests for bundled registry loading (always available).""" - + def test_bundled_registry_loads(self): """Bundled registry should load successfully without network.""" loader = RegistryLoader() registry = loader.registry - + assert registry is not None assert "providers" in registry assert "modelMetadata" in registry assert loader.loaded_from == "bundled" - + def test_bundled_registry_has_required_structure(self): """Bundled registry must have valid structure.""" loader = RegistryLoader() registry = loader.registry - + # Check providers providers = registry.get("providers", {}) assert isinstance(providers, dict) assert len(providers) > 0 - + # Check each provider has variants for vendor, vendor_data in providers.items(): assert "variants" in vendor_data variants = vendor_data["variants"] assert isinstance(variants, dict) assert len(variants) > 0 - + # Check each variant has models for variant_name, variant_data in variants.items(): assert "models" in variant_data assert isinstance(variant_data["models"], list) assert len(variant_data["models"]) > 0 - + def test_bundled_registry_has_metadata(self): """Bundled registry must have cost and performance metadata.""" loader = RegistryLoader() registry = loader.registry - + metadata = registry.get("modelMetadata", {}) assert "costs" in metadata assert "performance" in metadata - + costs = metadata["costs"] performance = metadata["performance"] - + assert isinstance(costs, dict) assert isinstance(performance, dict) assert len(costs) > 0 @@ -63,33 +63,33 @@ def test_bundled_registry_has_metadata(self): class TestRegistryLoaderValidation: """Tests for registry validation logic.""" - + def test_validate_registry_rejects_invalid_structure(self): """Should reject registries with missing required fields.""" loader = RegistryLoader() - + # Missing providers assert not loader._validate_registry({"modelMetadata": {}}) - + # Missing modelMetadata assert not loader._validate_registry({"providers": {}}) - + # Missing variants in provider assert not loader._validate_registry({ "providers": {"groq": {}}, "modelMetadata": {"costs": {}, "performance": {}} }) - + # Missing models in variant assert not loader._validate_registry({ "providers": {"groq": {"variants": {"default": {}}}}, "modelMetadata": {"costs": {}, "performance": {}} }) - + def test_validate_registry_accepts_valid_structure(self): """Should accept valid registry structures.""" loader = RegistryLoader() - + valid_registry = { "providers": { "groq": { @@ -103,24 +103,24 @@ def test_validate_registry_accepts_valid_structure(self): "performance": {"model1": 10.0} } } - + assert loader._validate_registry(valid_registry) class TestRegistryLoaderCache: """Tests for cache loading/saving.""" - + def test_cache_is_not_used_when_missing(self): """When cache doesn't exist, should load from bundled.""" with patch("plotsense.core.registry_loader.CACHE_FILE") as mock_cache: mock_cache.exists.return_value = False - + loader = RegistryLoader() registry = loader.registry - + assert registry is not None assert loader.loaded_from == "bundled" - + @patch("plotsense.core.registry_loader.CACHE_EXPIRY_HOURS", 24) def test_cache_is_used_when_fresh(self): """Fresh cache should be used instead of remote/bundled.""" @@ -137,22 +137,22 @@ def test_cache_is_used_when_fresh(self): "performance": {"test-model": 8.0} } } - + with patch("plotsense.core.registry_loader.CACHE_FILE") as mock_cache: mock_cache.exists.return_value = True mock_cache.stat.return_value.st_mtime = 9999999999 # Recent time - + with patch("builtins.open", create=True) as mock_open: mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(cache_data) - + # Mock time.time() to return a recent time with patch("plotsense.core.registry_loader.time.time", return_value=10000000000): loader = RegistryLoader() registry = loader.registry - + assert registry is not None assert "test" in registry.get("providers", {}) - + def test_save_to_cache_creates_directory(self): """Saving cache should create directory if needed.""" loader = RegistryLoader() @@ -160,7 +160,7 @@ def test_save_to_cache_creates_directory(self): "providers": {}, "modelMetadata": {"costs": {}, "performance": {}} } - + with patch("plotsense.core.registry_loader.CACHE_DIR") as mock_dir: loader._save_to_cache(test_data) mock_dir.mkdir.assert_called_once() @@ -168,156 +168,156 @@ def test_save_to_cache_creates_directory(self): class TestRegistryLoaderRemote: """Tests for remote registry fetching.""" - + def test_remote_fetch_falls_back_on_failure(self): """Failed remote fetch should fall back to bundled.""" with patch("plotsense.core.registry_loader.urllib.request.urlopen") as mock_urlopen: mock_urlopen.side_effect = Exception("Network error") - + loader = RegistryLoader() registry = loader.registry - + assert registry is not None # Should have loaded from bundled since remote failed assert loader.loaded_from == "bundled" - + def test_remote_fetch_validates_response(self): """Remote response should be validated before use.""" invalid_data = {"invalid": "structure"} - + with patch("urllib.request.urlopen") as mock_urlopen: mock_response = MagicMock() mock_response.read.return_value = json.dumps(invalid_data).encode() mock_urlopen.return_value.__enter__.return_value = mock_response - + loader = RegistryLoader() - registry = loader.registry - + _ = loader.registry + # Should fall back to bundled since remote was invalid assert loader.loaded_from == "bundled" class TestRegistryLoaderAccessors: """Tests for registry data accessor methods.""" - + def test_get_provider_models(self): """Should return correct model list for provider/variant.""" loader = RegistryLoader() - + # Groq default should have models models = loader.get_provider_models("groq", "default") assert isinstance(models, list) assert len(models) > 0 assert "llama-3.1-8b-instant" in models - + def test_get_provider_models_nonexistent(self): """Should return empty list for nonexistent provider.""" loader = RegistryLoader() - + models = loader.get_provider_models("nonexistent", "variant") assert models == [] - + def test_get_all_provider_models(self): """Should return all models by vendor_variant name.""" loader = RegistryLoader() - + all_models = loader.get_all_provider_models() assert isinstance(all_models, dict) assert len(all_models) > 0 - + # Should have vendor_variant keys assert any("_" in key for key in all_models.keys()) - + # Each should map to a list for key, models in all_models.items(): assert isinstance(models, list) - + def test_get_model_costs(self): """Should return model cost map.""" loader = RegistryLoader() - + costs = loader.get_model_costs() assert isinstance(costs, dict) assert len(costs) > 0 - + # Values should be floats for model, cost in costs.items(): assert isinstance(cost, (int, float)) - + def test_get_model_performance(self): """Should return model performance map.""" loader = RegistryLoader() - + performance = loader.get_model_performance() assert isinstance(performance, dict) assert len(performance) > 0 - + # Values should be floats for model, score in performance.items(): assert isinstance(score, (int, float)) - + def test_get_provider_display_name(self): """Should return display name for provider variant.""" loader = RegistryLoader() - + name = loader.get_provider_display_name("groq", "default") assert isinstance(name, str) assert len(name) > 0 assert "Groq" in name or "groq" in name.lower() - + def test_get_provider_display_name_nonexistent(self): """Should return vendor_variant for nonexistent provider.""" loader = RegistryLoader() - + name = loader.get_provider_display_name("nonexistent", "variant") assert name == "nonexistent_variant" class TestRegistryLoaderSingleton: """Tests for global singleton instance.""" - + def test_get_registry_loader_singleton(self): """Should return same instance on multiple calls.""" loader1 = get_registry_loader() loader2 = get_registry_loader() - + assert loader1 is loader2 - + def test_singleton_registry_cached(self): """Singleton should cache loaded registry.""" loader = get_registry_loader() registry1 = loader.registry registry2 = loader.registry - + # Should be same object assert registry1 is registry2 class TestRegistryLoaderIntegration: """Integration tests for registry loading.""" - + def test_provider_models_are_accessible(self): """All providers in registry should have accessible models.""" loader = RegistryLoader() registry = loader.registry - + for vendor, vendor_data in registry.get("providers", {}).items(): for variant_name in vendor_data.get("variants", {}).keys(): models = loader.get_provider_models(vendor, variant_name) assert len(models) > 0, f"No models for {vendor}_{variant_name}" - + def test_cost_performance_coverage(self): """Models in registry should ideally have cost/performance data.""" loader = RegistryLoader() all_models = loader.get_all_provider_models() costs = loader.get_model_costs() - performance = loader.get_model_performance() - + _ = loader.get_model_performance() + # Collect all unique models from all providers all_unique_models = set() for models in all_models.values(): all_unique_models.update(models) - + # Check coverage (not all models need data, but most should) coverage = len(all_unique_models & set(costs.keys())) / len(all_unique_models) assert coverage > 0.5, "Less than 50% of models have cost data"