APPL (A Prompt Programming Language)

repository·main·Indexed 42 days ago

https://github.com/appl-team/appl

A Python extension for integrating Large Language Models (LLMs) into software development. APPL provides a natural and efficient way to write prompts using Python syntax, featuring the @ppl decorator, automatic parallelization, tool calling, and tracing. Version 0.2.2 supports advanced patterns like Tree of Thoughts (ToT), multi-agent chat via resume/generator patterns, and structured conversations using AIRole.

Tokens
28.1K
Snippets
107
Records
128
Agent score
54%

What's inside APPL

  1. What is an APPL Function?

    main

    An APPL function is the fundamental building block of the APPL framework, created by applying the @ppl decorator to a standard Python function. It encapsulates LM (Language Model) prompts and Python workflows into a self-contained module.

    Unlike standard Python functions, APPL functions provide a Prompt Context—an object managed by the framework that stores prompts and conversation history. This allows you to blend Python logic with LM interactions seamlessly.

    from appl import ppl
    
    @ppl
    def my_function():
        """This is a docstring."""
        "This is a prompt."
        return "result"
  2. Automatic parallelization in APPL

    main

    APPL automatically parallelizes LLM calls when you return a list of gen() calls (which return future-like objects) within an APPL function. This allows multiple questions or tasks to be processed concurrently without manual thread management.

    @ppl
    def answer_questions(quotation: str, questions: list[str]):
        quotation
        with AIRole():
            f"The name of the author is {gen(stop='.')}"
        # These calls are automatically parallelized
        return [get_answer(q) for q in questions]
  3. Achieve automatic parallelization in APPL

    main
    APPL supports automatic parallelization due to its asynchronous design. When you call independent gen() functions within a @ppl function (for example, using a list comprehension to call sub-functions), APPL can execute these calls in parallel without requiring explicit asynchronous code or threading. This is highly efficient for tasks like answering multiple independent follow-up questions.
  4. Define an APPL function with the @ppl decorator

    main

    An APPL function is a standard Python function decorated with @ppl. Inside these functions, you construct prompts by simply stating strings (literals or variables). These statements are automatically appended to the prompt context. You then call gen() to trigger the Language Model (LM) to generate a response based on the accumulated prompt.

    Key components:

    • @ppl: Decorator that marks a function as an APPL function.
    • String literals/variables: Act as prompt statements.
    • gen(): Function used to request and retrieve the LM response.
    from appl import gen, ppl
    
    @ppl
    def get_author_name(quotation: str):
        """Extract the name of the author from the quotation below."""
        quotation
        return gen()
    
    quotation = '"Simplicity is the ultimate sophistication." -- Leonardo da Vinci'
    print(get_author_name(quotation))
  5. Understand APPL configuration priority

    main

    APPL uses a hierarchical configuration loading mechanism. When running a script, APPL walks up the directory tree from the script's location to find configuration files.

    Priority Rules

    • Files closer to the executing script have higher priority.
    • If a script in project/main.py finds both project/appl.yaml and a root appl.yaml, the one in project/ overrides the root one.
    • The same logic applies to .env files.

    Best Practices for File Usage

    • .env: Use for sensitive information like API keys. Do not commit to version control.
    • appl.yaml: Use for project settings (models, servers, tracing). Commit this to version control so other developers can reproduce your environment.
  6. Use Prompt Compositors to create structured prompts

    main

    Prompt Compositors are context managers used to programmatically compose text into structured formats like Markdown, XML, or lists. They allow you to nest structures to create complex, hierarchical prompts.

    Common patterns include:

    • NumberedList(): Composes items within its scope into a numbered list.
    • Tagged(tag_name, ...): Wraps content in XML-style tags.

    Key parameters for compositors:

    • indent: Sets the indentation for content inside the compositor.
    • indent_inside: Sets the indentation for content inside a tag (e.g., for Tagged).

    To use them, wrap your prompt text or variables within a with block inside a function decorated with @ppl.

    @ppl
    def compose(items: list[str]):
        with Tagged("div", indent_inside=4):
            "Display the items:"
            with NumberedList(indent=4):
                items
        return records()
  7. Configure context passing between APPL functions

    main

    When one APPL function calls another (the callee), you can control how the prompt context is shared using the ctx parameter in the @ppl decorator. There are four modes:

    ModeBehaviorAnalogy
    new (default)Creates a brand new, empty context for the callee.N/A
    copyThe callee receives a copy of the caller's context. Changes in the callee do not affect the caller.Call by value
    sameThe callee uses the exact same context as the caller. Changes in the callee do affect the caller.Call by reference
    resumeThe context is preserved across calls, making the function stateful. It copies the caller's context for the first call and maintains state thereafter.Stateful/Persistent

    Note: Use resume for multi-agent chat scenarios where you want to continue a conversation.

    from appl import ppl
    
    @ppl(ctx="same")
    def additive_function():
        "This prompt will be added to the caller's context."
        pass
    
    @ppl(ctx="copy")
    def isolated_function():
        "This prompt will NOT affect the caller's context."
        pass
  8. How prompt capture works in APPL functions

    main

    In APPL, expression statements within functions decorated with @ppl are automatically captured as prompts. The capture behavior depends on the type of the value being evaluated (provided the value is not None).

    Captured values are categorized into three behaviors:

    1. Prompts: Basic values like strings or Message objects.
    2. Sequence of Prompts: Collections like list or tuple where elements are captured recursively.
    3. Custom Promptable: User-defined types that implement a specific interface.

    Note that primitive types like int, float, and bool are not captured automatically. To capture them, you must explicitly convert them to strings or include them in a formatted string.

    from appl import ppl, records, AIMessage
    
    @ppl
    def greeting(name: str):
        "Hello world!"
        AIMessage("Nice to meet you.")
        return records()
    
    print(greeting("APPL"))
    # user: Hello world!
    # assistant: Nice to meet you.
  9. Stream structured output

    main

    Streaming behavior in APPL depends on which argument you use for structured output:

    1. Using response_format: Streaming is captured and displayed automatically, and the function returns a complete object once finished.
    2. Using response_model (via Instructor): To enable streaming, you must define the response_model as a Partial or Iterable object. In this mode, the returned object is a generator that yields partial objects as they are being constructed.
  10. Parallelize LM calls using StringFuture and gen()

    main

    APPL enables parallel execution of Language Model (LM) calls by using the gen() function, which starts a new thread or process for the generation without blocking the main thread.

    To manage these non-blocking calls, APPL uses the StringFuture object. A StringFuture acts as a placeholder for a string value that will be available in the future.

    Key behaviors:

    • Lazy Synchronization: StringFuture objects delay synchronization (waiting for the actual result) until the value is strictly required (e.g., when calling print() or converting to str).
    • Concurrent Composition: You can perform operations like .join() on StringFuture objects without waiting for the underlying generations to complete. This allows you to build complex structures of multiple parallel calls that only synchronize at the very end.
    • Forcing Synchronization: If you need to wait for a result immediately, call .results on the generation object or wrap it in str().
    import appl
    from appl import gen, ppl, StringFuture
    
    @ppl
    def mul(x:int, y:int):
        f"{x}*{y}="
        return gen()
    
    # Parallel execution using StringFuture.join
    n = 3
    s = StringFuture("\n").join(
        StringFuture(" ").join(mul(i + 1, j + 1) for j in range(n))
        for i in range(n)
    )
    
    # Synchronization happens here when print is called
    print(s)
  11. Define and reference concepts with Prompt Definitions

    main

    The Definition class allows you to define reusable concepts within a prompt. This makes large prompts more maintainable and allows you to refer to concepts by name rather than repeating descriptions.

    Defining a Concept

    Subclass Definition (or BracketedDefinition) and specify a name. You can also define an fstr to control how the concept is formatted when referenced.

    class InputReq(Definition):
        name = "Input Requirement"
        fstr = "[{}]" # Format used when referring to the concept

    Using a Concept

    1. Instantiation: To include the concept's description in the prompt, instantiate the class: InputReq(desc="...").
    2. Reference: To refer to the concept elsewhere in the prompt, use the class name.
      • Using the class name directly (e.g., f"{InputReq}") uses the defined fstr format.
      • Using the !r flag (e.g., f"{InputReq!r}") retrieves the raw name without formatting.

    This approach provides better IDE support (like VSCode) for navigation and syntax highlighting.

    class InputReq(Definition):
        name = "Input Requirement"
        fstr = "[{}]"
    
    @ppl
    def func():
        InputReq(desc="The input should be two numbers.")
        f"Satisfy the {InputReq}"