strictjson

repository·main·Indexed 18 days ago

https://github.com/tanchongmin/strictjson

A Structured Output Framework for LLM Outputs (version 6.3.0) that provides a concise way to define schemas for structured data, bridging the gap between dictionary definitions and Pydantic models. It features utilities like parse_yaml and strict_json to force LLMs to return data following specific schemas, with built-in integrations for OpenAI and Gemini. The library supports automatic conversion to Pydantic and JSON Schema, asynchronous processing via parse_yaml_async, and tools for Knowledge Graph extraction and context-dependent embeddings.

Tokens
6.5K
Snippets
26
Records
29
Agent score
62%

What's inside strictjson

  1. Define a StrictJSON Schema

    main

    A StrictJSON schema is a dictionary where keys are the field names and values define the constraints and types.

    Value Formats:

    • <description>, <datatype>: e.g., "starting with A, str"
    • <description>, type: <datatype>: e.g., "between 5 to 12, type: int"
    • <datatype>: e.g., "str" (no description provided)
    • <description>: e.g., "Name of party" (datatype defaults to Any)

    Supported Datatypes:

    • Basic: str, int, float, bool
    • Collections: List[type], Dict[key_type, value_type] (Note: list or dict without brackets default to List[Any] and Dict[str, Any])
    • Complex: date, datetime, UUID, Decimal
    • Special: Any, None, Optional[type], Union[type1, type2], and PEP 604 syntax (type1 | type2)
    • Constraints: Enum['A', 'B', 'C'] (limits output to specific values)

    Nesting: You can nest schemas using lists of dictionaries, e.g., [{'key': 'type'}] to ensure a list of specific objects.

  2. Define structured schemas using Enum and type hints

    main

    When defining the output_format dictionary for parse_yaml, you can use specific syntax to constrain the LLM's output:

    • Basic Types: Use 'str', 'int', 'list', or 'dict'.
    • Enums: Use Enum[option1, option2, ...] to restrict a string field to a specific set of allowed values.
    • Complex Nesting: Define lists of dictionaries to represent arrays of objects.

    Example Schema:

    output_format = {
        "Questions": [{
            "Question": "str",
            "Answer": "Enum[1, 2, 3, 4]",
            "Explanation": "str"
        }]
    }
  3. Define output structure using output_format

    main

    The preferred way to define the output structure is via the output_format dictionary. This method is token-efficient as it uses a concise description-type infused dictionary.

    To add field descriptions, include them within the value string alongside the type (e.g., "description, type").

    Example with descriptions:

    output_format = {
        "name": "Name of birthday party, str",
        "date": "Any date in Mar 2026, date",
        "participants": [{'Name': 'starting with A, str', 'Age': 'between 5 to 12, int'}]
    }
    parse_yaml(
        system_prompt = "You are a helpful assistent",
        user_prompt = "Generate a birthday event for Alex",
        output_format = {
            "name": "Name of birthday party, str",
            "date": "Any date in Mar 2026, date",
            "participants": [{'Name': 'starting with A, str', 'Age': 'between 5 to 12, int'}]
        }, 
        llm = llm
    )
  4. Configure environment variables for LLM access

    main

    To use strictjson with LLMs via providers like OpenRouter or OpenAI, you must set up your API keys in a .env file.

    For example, if using OpenAI or OpenRouter, your .env should contain:

    export OPENAI_API_KEY="sk-..."
    export OPENROUTER_API_KEY="sk-..."

    You can then load these into your Python environment using load_dotenv() from the python-dotenv package.

    from dotenv import load_dotenv
    load_dotenv()
  5. Workflow: Answering questions using a Knowledge Graph

    main

    This pattern demonstrates how to use strictjson to implement a Knowledge Graph (KG) based RAG (Retrieval-Augmented Generation) system. This approach can be more precise than raw context if the KG is extracted correctly.

    Step 1: Extract Knowledge Graph

    Use strict_json to convert raw text into a list of triplets (object_1, relation, object_2).

    Step 2: Parse/Filter Knowledge Graph

    Use strict_output to filter the full KG, keeping only the triplets relevant to the user's specific question. This reduces noise for the final LLM call.

    Step 3: Answer Question

    Use strict_json to pass the filtered KG and the question to the LLM, requesting a final structured answer.

    # 1. Extract
    res = strict_json(system_prompt=..., user_prompt=context, output_format=...)
    kg = res['List of triplets']
    
    # 2. Filter
    res = strict_output(system_prompt=..., user_prompt=question, output_format=...)
    parsed_kg = res['Parsed Knowledge Graph']
    
    # 3. Answer
    res = strict_json(system_prompt=..., user_prompt=question, output_format=...)
    print(res['Answer'])
  6. Implement context-dependent embeddings using Approach 2

    main

    To solve the problem where long chunks of similar text cause embeddings to appear more similar than they are, use Approach 2: Modifying text based on context.

    Instead of just prepending or appending context (which the experiments show does not fully solve the problem), use an LLM to refine the text based on the provided context before generating the embedding. This highlights contextually relevant parts of the text without changing its core meaning.

    Workflow:

    1. Use text_conversion(context, text) to refine the text using an LLM.
    2. Use get_embedding_by_context(text, context, model) to automate the conversion and embedding process.
    # Refine text based on context (e.g., 'finance' vs 'water')
    # then get the embedding
    embedding = get_embedding_by_context(
        text='I went to the bank',
        context='finance',
        model='text-embedding-3-large'
    )
  7. Implement a custom LLM provider function

    main

    To use strictjson, you must provide a function that interfaces with your chosen LLM provider (e.g., OpenAI, Anthropic, or OpenRouter). This function must accept system_prompt and user_prompt and return the content string from the LLM response.

    Example using OpenAI/OpenRouter:

    import os
    from openai import OpenAI
    
    def llm(system_prompt: str, user_prompt: str, **kwargs):
        client = OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=os.environ["OPENROUTER_API_KEY"]
        )
        
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({"role": "user", "content": user_prompt})
    
        response = client.chat.completions.create(
            model="google/gemini-2.5-flash",
            messages=messages
        )
        return response.choices[0].message.content
    def llm(system_prompt: str, user_prompt: str, **kwargs):
        from openai import OpenAI
        client = OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=os.environ["OPENROUTER_API_KEY"],
        )
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({"role": "user", "content": user_prompt})
        response = client.chat.completions.create(model=MODEL, messages=messages)
        return response.choices[0].message.content
  8. Convert StrictJSON Schema to Pydantic

    main

    StrictJSON allows you to define schemas concisely using a dictionary format. You can convert these schemas into Pydantic models to use with LLM providers like Gemini, or extract the JSON schema for direct LLM input.

    Note for OpenAI users: Use convert_schema_to_openai_pydantic instead of the standard converter. The OpenAI API does not accept Any or dict datatypes, so this specific function converts them to str and List[] respectively to ensure compatibility.

    from strictjson import convert_schema_to_pydantic
    
    output_format = {"name": "Name of birthday party, str",
                     "date": "Any date in Mar 2026, date",
                     "participants": [{'Name': 'starting with A, str', 
                                       'Age': 'between 5 to 12, int'}]}
    
    # Generates a Pydantic model (preferred for Gemini)
    pydantic_model = convert_schema_to_pydantic(output_format)
    
    # Generates JSON schema if required by the LLM
    json_schema = pydantic_model.model_json_schema()
  9. Use parse_yaml for universal LLM structured output

    main

    The parse_yaml function is a robust, model-agnostic way to get structured output from any LLM (ChatGPT, Claude, Gemini, etc.). It uses YAML for the prompt/output structure to reduce token usage and avoid escaping issues common with JSON. It automatically creates a Pydantic model for type checking and includes a retry mechanism (defaulting to 3 tries, configurable via num_tries) to fix mistakes.

    To use it, you must provide an llm function that accepts system_prompt and user_prompt and returns a string.

    from strictjson import parse_yaml
    
    def my_llm_wrapper(system_prompt: str, user_prompt: str, **kwargs) -> str:
        # Your logic to call any LLM (OpenAI, Anthropic, etc.)
        # Must return the raw string response
        return "raw response string"
    
    result = parse_yaml(
      system_prompt="You are a friendly assistant.",
      user_prompt="Write a blog post idea about AI in education",
      output_format={
        "title": "str",
        "tags": "List[str]",
        "published": "bool"
      },
      llm=my_llm_wrapper
    )