Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
- O resumo de uso do fim da execução deixa de sair por `print` no stdout e passa ao logger `dataframeit.stats`, em nível INFO. Sem logging configurado, ele sai em stderr; `logging.getLogger("dataframeit.stats").setLevel(logging.WARNING)` o silencia e mantém as colunas de tokens. O aviso de workers reduzidos por rate limit vira `UserWarning`, e a mensagem amigável do provider passa a fazer parte do aviso de falha da linha, ambos filtráveis por `warnings` e `pytest -W` (#181).
- `dataframeit()` declara o tipo de retorno pelo tipo da entrada: DataFrame ou Series do polars devolve DataFrame do polars, e DataFrame ou Series do pandas, `list` e `dict` devolvem DataFrame do pandas. Antes, o retorno era a união dos dois, e o checker de tipos recusava `resultado["coluna"].tolist()` (#155).
- O piso do pydantic sobe para 2.12. Na 2.11, uma `default_factory` que lê outro campo é chamada com os dados validados vazios quando esse campo falhou, e a retomada levantava o `KeyError` dela em vez do erro de campo incompatível (#154).
- Os extras `polars` e `all` passam a exigir `polars>=1.0`. No 0.20, há versões que convertem a coluna de texto com um valor de outro tipo e põem nulo no lugar dele, sem erro, e versões em que o `to_pandas()` de uma coluna `Object`, pelo qual ela volta na retomada, devolve bytes no lugar dos objetos ou falha (#218).
- O extra `codex` passa a exigir `openai-codex==0.159.2`, que traz o runtime 0.159.2. Com o runtime 0.137, que o SDK anterior fixava, a conta do ChatGPT recebia HTTP 400 nos modelos lançados depois dele, como o `gpt-6-luna`, embora o catálogo da conta os anunciasse. O `effort` é conferido contra os valores que o SDK declara, agora com `max` e `ultra`, porque o enum do SDK novo aceita qualquer texto.
- No provider `codex`, os códigos de erro `rateLimitExceeded` e `flexUnavailable` do runtime levantam `ProviderOverloadedError`, e a linha é repetida como no HTTP 429.
- No provider `codex`, as flags `sleep_tool` e `view_image` do runtime ficam desligadas, e as ferramentas de pausa e de leitura de imagem local deixam de ser oferecidas ao modelo.
Expand All @@ -35,6 +36,7 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR

### Corrigido

- Com entrada polars, a saída deixa de falhar depois de todas as chamadas quando um campo do modelo guarda valores que o polars não reúne numa coluna tipada, como `Union[int, str]` com número numa linha e texto em outra, `tuple[int, str]`, `Any` e `dict[str, Any]` com tipos diferentes, lista de `Enum`, inteiro acima de 2**53 ao lado de float, inteiro acima do `Int64` e `datetime` com fuso de deslocamento, como `+05:30`. A coluna sai com o tipo `Object`, com os objetos Python que a entrada pandas devolveria, e um aviso a nomeia. O campo `Enum` sai pelo valor do membro, inclusive na coluna `Object` (#218).
- A coluna `_trace` do agente único fica com as demais colunas de trace na ordem da saída, depois dos campos do modelo. Antes, numa entrada que já a trazia, os campos novos saíam depois dela.
- Com entrada polars, as colunas que a execução não escreve saem do DataFrame de entrada, com o tipo e os valores de lá. Antes, a saída inteira era convertida de volta do pandas: um `Int64` com nulo voltava `Float64`, o que arredondava ids acima de 2**53, um `Date` voltava `Datetime`, e `Enum`, `Decimal`, `Array`, `Null` e o inteiro dentro de `Struct` e `List` também trocavam de tipo; uma coluna `Object` também trocava de tipo ou, com objetos que o Arrow não infere, levantava `ArrowInvalid` depois das chamadas (#195).
- O operando de `in` e `not_in` em `condition` é conferido antes de processar, com `ValueError` quando não é lista, tupla, set ou frozenset. Antes, um operando que não era coleção, como `{"in": 5}`, levantava `TypeError` em cada linha depois das chamadas dos campos anteriores, e um texto, como `{"in": "pfj"}`, fazia busca de substring e aceitava `"pf"` (#182).
Expand Down
2 changes: 2 additions & 0 deletions docs/en/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Returns data in the same format as input with extracted columns added.
| `list` | `pd.DataFrame` with numeric index |
| `dict` | `pd.DataFrame` with keys as index |

With polars input, the input columns that are not model fields and do not have the name of one of the [added columns](#added-columns) come back as they went in, with the same type and the same values. In polars output, an `Enum` field comes out as the member's value. When the answers of a field do not fit a typed polars column, such as a `Union[int, str]` that received a number in one row and text in another, the column comes out with the `Object` type, holding the Python objects the model returned, except an `Enum` member outside a list or dict, which comes out as its value there too, and a warning names the columns; convert them before writing to a file.

### Added Columns

| Column | When it exists | Content |
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Retorna dados no mesmo formato da entrada com colunas extraídas adicionadas.
| `list` | `pd.DataFrame` com índice numérico |
| `dict` | `pd.DataFrame` com chaves como índice |

Com entrada polars, as colunas de entrada que não são campo do modelo nem têm nome de uma das [colunas adicionadas](#colunas-adicionadas) voltam como entraram, com o mesmo tipo e os mesmos valores. Na saída polars, um campo `Enum` sai pelo valor do membro. Quando as respostas de um campo não cabem numa coluna tipada do polars, como um `Union[int, str]` que recebeu número numa linha e texto em outra, a coluna sai com o tipo `Object`, com os objetos Python que o modelo devolveu, exceto o membro de `Enum` fora de lista ou dict, que também nela sai pelo valor, e um aviso diz quais colunas são; converta-as antes de gravar em arquivo.

### Colunas Adicionadas

| Coluna | Quando existe | Conteúdo |
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ codex = [
"openai-codex==0.160.1",
]
polars = [
"polars>=0.20",
"polars>=1.0",
"pyarrow>=10",
]
excel = [
Expand All @@ -110,7 +110,7 @@ all = [
"langchain-tavily>=0.2.12",
"langchain-exa>=1.0.0",
"claude-agent-sdk>=0.1.48",
"polars>=0.20",
"polars>=1.0",
"pyarrow>=10",
"openpyxl>=3.1",
]
Expand Down
86 changes: 69 additions & 17 deletions src/dataframeit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import sys
import types
import typing
import warnings
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
Expand Down Expand Up @@ -229,19 +230,7 @@ def from_pandas(
original_type=ORIGINAL_TYPE_POLARS_DF if was_polars else ORIGINAL_TYPE_PANDAS_DF
)

error_col = "_error_details"

# Remover colunas de status/erro se não houver erros nem linha pendente. A
# linha sem status é a que uma interrupção deixou para `resume=True`, e sem a
# coluna ela não se distingue das processadas.
if status_col in df.columns:
has_errors = (df[status_col] == "error").any()
has_pending = df[status_col].isna().any()
has_error_details = error_col in df.columns and df[error_col].notna().any()

if not has_errors and not has_pending and not has_error_details:
cols_to_drop = [c for c in [status_col, error_col] if c in df.columns]
df = df.drop(columns=cols_to_drop)
df = _drop_idle_control_columns(df, status_col)

# Series, list e dict viram DataFrame sem a coluna interna de texto; a
# pandas.Series recupera o índice original.
Expand All @@ -254,13 +243,76 @@ def from_pandas(
df = _reorder_columns(df, status_col)
if original_type in _POLARS_INPUTS and pl is not None:
untouched = _untouched_columns(conversion_info.polars_frame, status_col, model_columns)
converted = pl.from_pandas(df[[col for col in df.columns if col not in untouched]])
return pl.DataFrame(
[untouched[col] if col in untouched else converted[col] for col in df.columns]
)
columns = []
as_object = []
for col in df.columns:
if col in untouched:
columns.append(untouched[col])
continue
values = _enums_by_value(df[col])
# As chamadas ao provedor já foram feitas, e a coluna que o polars ou o
# pyarrow recusam, seja qual for a exceção (ValueError, TypeError,
# OverflowError, ComputeError), sai Object em vez de derrubar a saída.
# O pânico do polars não é Exception e passa por aqui.
try:
columns.append(pl.from_pandas(values))
except Exception: # noqa: BLE001 (a recusa muda de tipo com os valores)
# Há versões do polars em que o construtor de Series inspeciona os
# valores mesmo com dtype=pl.Object: quando o primeiro valor não nulo
# é membro de Enum, troca cada valor pelo `.value`, e levanta erro no
# que não o tem, como o None. O map_elements guarda o que a função
# devolve, sem inspecionar.
items = values.tolist()
positions = pl.Series(col, range(len(items)), dtype=pl.Int64)
columns.append(positions.map_elements(items.__getitem__, return_dtype=pl.Object))
as_object.append(col)
if as_object:
warnings.warn(
f"As colunas {as_object} têm valores de tipos que o polars não reúne numa "
"coluna tipada e saem com o tipo Object, com os objetos Python que o modelo "
"devolveu, exceto o Enum fora de lista ou dict, que sai pelo valor. Converta-as "
"antes de gravar em arquivo.",
UserWarning,
stacklevel=3,
)
return pl.DataFrame(columns)
return df


def _drop_idle_control_columns(df: pd.DataFrame, status_col: str) -> pd.DataFrame:
"""Remove as colunas de status e de erro quando não há erro nem linha pendente.

A linha sem status é a que uma interrupção deixou para `resume=True`, e sem
a coluna ela não se distingue das processadas.
"""
error_col = "_error_details"
if status_col not in df.columns:
return df
has_errors = (df[status_col] == "error").any()
has_pending = df[status_col].isna().any()
has_error_details = error_col in df.columns and df[error_col].notna().any()
if has_errors or has_pending or has_error_details:
return df
return df.drop(columns=[c for c in [status_col, error_col] if c in df.columns])


def _enums_by_value(values: pd.Series) -> pd.Series:
"""Troca o Enum pelo valor numa coluna de objetos.

O Arrow não tipa o membro de um Enum, e é pelo valor que o checkpoint o
grava. O Enum dentro de lista ou dict fica como está, e a coluna sai Object.
A Series é montada com dtype=object porque `Series.map` voltaria a inferir
o tipo e levaria o inteiro com None a float.
"""
if values.dtype != object:
return values
return pd.Series(
[value.value if isinstance(value, Enum) else value for value in values],
name=values.name,
dtype=object,
)


def _untouched_columns(
source: PolarsDataFrame | None,
status_col: str,
Expand Down
Loading
Loading