fix(config): validate entity_labels structure on PATCH (#902)
* test: add regression tests for #874 and #894 Add tests for None event_date in fact extraction (AttributeError fix) and for _register_profile skipping .env overwrite with short config keys. * fix(config): validate entity_labels structure on PATCH (#891) Config PATCH accepted bare strings in entity_labels values without validation, causing silent failures at retain time. Now validates via parse_entity_labels() before writing to DB, and fixes the BankTemplateConfig type from list[str] to list[dict[str, Any]]. * fix(scripts): handle Python client generator README crash gracefully The openapi-generator sometimes crashes writing README_onlypackage.mustache. Allow the failure with || true since all API/model files are generated before that step, and add a verification check for api_client.py. * chore: regenerate docs skill openapi.json
This commit is contained in:
parent
f659bb17c4
commit
7e23f8e149
9 changed files with 37 additions and 13 deletions
|
|
@ -1661,7 +1661,9 @@ class BankTemplateConfig(BaseModel):
|
|||
disposition_skepticism: int | None = Field(default=None, ge=1, le=5, description="Skepticism trait (1-5)")
|
||||
disposition_literalism: int | None = Field(default=None, ge=1, le=5, description="Literalism trait (1-5)")
|
||||
disposition_empathy: int | None = Field(default=None, ge=1, le=5, description="Empathy trait (1-5)")
|
||||
entity_labels: list[str] | None = Field(default=None, description="Controlled vocabulary for entity labels")
|
||||
entity_labels: list[dict[str, Any]] | None = Field(
|
||||
default=None, description="Controlled vocabulary for entity labels"
|
||||
)
|
||||
entities_allow_free_form: bool | None = Field(
|
||||
default=None, description="Allow entities outside the label vocabulary"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -239,6 +239,15 @@ class ConfigResolver:
|
|||
logger.warning(f"Failed to check permissions for bank {bank_id}: {e}")
|
||||
# Continue without permission check (fail open for backward compatibility)
|
||||
|
||||
# Validate entity_labels structure
|
||||
if "entity_labels" in normalized_updates and normalized_updates["entity_labels"] is not None:
|
||||
from .engine.retain.entity_labels import parse_entity_labels
|
||||
|
||||
try:
|
||||
parse_entity_labels(normalized_updates["entity_labels"])
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid entity_labels format: {e}")
|
||||
|
||||
# Validate retain_strategies: reject empty string keys
|
||||
if "retain_strategies" in normalized_updates and normalized_updates["retain_strategies"]:
|
||||
empty_keys = [k for k in normalized_updates["retain_strategies"] if not str(k).strip()]
|
||||
|
|
|
|||
|
|
@ -3570,7 +3570,7 @@ components:
|
|||
type: integer
|
||||
entity_labels:
|
||||
items:
|
||||
type: string
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: array
|
||||
entities_allow_free_form:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type BankTemplateConfig struct {
|
|||
DispositionSkepticism NullableInt32 `json:"disposition_skepticism,omitempty"`
|
||||
DispositionLiteralism NullableInt32 `json:"disposition_literalism,omitempty"`
|
||||
DispositionEmpathy NullableInt32 `json:"disposition_empathy,omitempty"`
|
||||
EntityLabels []string `json:"entity_labels,omitempty"`
|
||||
EntityLabels []map[string]interface{} `json:"entity_labels,omitempty"`
|
||||
EntitiesAllowFreeForm NullableBool `json:"entities_allow_free_form,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -471,9 +471,9 @@ func (o *BankTemplateConfig) UnsetDispositionEmpathy() {
|
|||
}
|
||||
|
||||
// GetEntityLabels returns the EntityLabels field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetEntityLabels() []string {
|
||||
func (o *BankTemplateConfig) GetEntityLabels() []map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret []string
|
||||
var ret []map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
return o.EntityLabels
|
||||
|
|
@ -482,7 +482,7 @@ func (o *BankTemplateConfig) GetEntityLabels() []string {
|
|||
// GetEntityLabelsOk returns a tuple with the EntityLabels field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetEntityLabelsOk() ([]string, bool) {
|
||||
func (o *BankTemplateConfig) GetEntityLabelsOk() ([]map[string]interface{}, bool) {
|
||||
if o == nil || IsNil(o.EntityLabels) {
|
||||
return nil, false
|
||||
}
|
||||
|
|
@ -498,8 +498,8 @@ func (o *BankTemplateConfig) HasEntityLabels() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// SetEntityLabels gets a reference to the given []string and assigns it to the EntityLabels field.
|
||||
func (o *BankTemplateConfig) SetEntityLabels(v []string) {
|
||||
// SetEntityLabels gets a reference to the given []map[string]interface{} and assigns it to the EntityLabels field.
|
||||
func (o *BankTemplateConfig) SetEntityLabels(v []map[string]interface{}) {
|
||||
o.EntityLabels = v
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class BankTemplateConfig(BaseModel):
|
|||
disposition_skepticism: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None
|
||||
disposition_literalism: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None
|
||||
disposition_empathy: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None
|
||||
entity_labels: Optional[List[StrictStr]] = None
|
||||
entity_labels: Optional[List[Dict[str, Any]]] = None
|
||||
entities_allow_free_form: Optional[StrictBool] = None
|
||||
__properties: ClassVar[List[str]] = ["reflect_mission", "retain_mission", "retain_extraction_mode", "retain_custom_instructions", "retain_chunk_size", "enable_observations", "observations_mission", "disposition_skepticism", "disposition_literalism", "disposition_empathy", "entity_labels", "entities_allow_free_form"]
|
||||
|
||||
|
|
|
|||
|
|
@ -459,7 +459,9 @@ export type BankTemplateConfig = {
|
|||
*
|
||||
* Controlled vocabulary for entity labels
|
||||
*/
|
||||
entity_labels?: Array<string> | null;
|
||||
entity_labels?: Array<{
|
||||
[key: string]: unknown;
|
||||
}> | null;
|
||||
/**
|
||||
* Entities Allow Free Form
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5275,7 +5275,8 @@
|
|||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -108,6 +108,9 @@ cd "$PYTHON_CLIENT_DIR"
|
|||
# Run openapi-generator via Docker (pinned version for reproducibility)
|
||||
# Use --platform linux/amd64 to ensure identical output on both macOS (arm64) and Linux CI (amd64)
|
||||
# Use --user to match current user's UID/GID so generated files are writable
|
||||
# Note: the generator may exit non-zero due to a known bug writing
|
||||
# README_onlypackage.mustache, but all API/model files are generated
|
||||
# before that step, so we allow the failure and verify files below.
|
||||
docker run --rm \
|
||||
--platform linux/amd64 \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
|
|
@ -118,7 +121,13 @@ docker run --rm \
|
|||
-i /local/openapi.json \
|
||||
-g python \
|
||||
-o /local/out \
|
||||
-c /local/config.yaml
|
||||
-c /local/config.yaml || true
|
||||
|
||||
# Verify critical generated files exist
|
||||
if [ ! -f "$PYTHON_CLIENT_DIR/hindsight_client_api/api_client.py" ]; then
|
||||
echo "❌ Error: Python client generation failed - api_client.py not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Organizing generated files..."
|
||||
|
||||
|
|
|
|||
|
|
@ -5275,7 +5275,8 @@
|
|||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue