# Overview Opper is a Unified API that makes it easy to build AI code that is model independent, structured and performant. SDKs are available for Python and Typescript, and they provide a toolkit to perform various common LLM operations. # Key concepts Here are the key concepts of Opper: - A `call` is a primitive but powerful way of interacting with a generative model. It is a structured definition of a task, with clear input and output schemas where schema annotations are used for prompting. Each call has a "name", which allows it to be managed as a function. - A `model` is an LLM. A full list of supported LLMs are available at https://docs.opper.ai/capabilities/models. - A `span` is a log of a call. But unlike ordinary logs, a span is built to be able to hold metrics, evaluations and be tied together with other spans to form what is called a trace. A trace is a chain of spans. - A `metric` is a data point that you can attach to a span. It is a flexible way to provide feedback on the success of a call from code. It can be used for storing results end user feedback such as thumbs up or down, performance timers or the result of small checks. - A `dataset` is a collection of example outputs that can act as ground truth for success of a task. Dataset entries can be used for testing changes to prompts, alternative models and be used as examples for the model. - A `knowledge` base is database equipped with semantic similarity functionality. You can use it to index knowledge and retrieve the semantically relevant parts of it. Good for memory or knowledge applications. # Best practices Here are the best practices for building reliable AI implementations with these primitives: - Break up tasks in to steps and implement calls for each step. This helps build an easy to maintain and easy to optimize pipeline. - Always define clear schemas for the output, with descriptions for fields to improve the understanding for the model - Define a schema for the input with descriptions for fields to improve the understanding for the model, and leave out explanation of this in instructions. - Sparingly use natural language instructions, use schemas and field descriptions to define the instructions as much as possible. - Always start a trace to have subsequent calls and indexing operations as children of the parent span. This will make it easier to understand the flow of the application in the Opper UI. - Leave out the model unless you have a specific reason to do so, model optimization is preferably a later optimization. - Perform small and simple evaluations of outputs of calls in code to verify quality or assumptions, to be able to spot issues early - Include a field `thoughts` as the first item in the output schema to improve quality of the response. # Examples ## Get started We recommend storing your API key for Opper as an environment variable. The SDKs will automatically try to load the key from the environment variable OPPER_API_KEY. You can generate API keys at https://platform.opper.ai ```sh export OPPER_API_KEY="" ``` ## Making calls to models with schemas In this example we complete a task to extract structured room information from a text. Note how we define input and output schemas for the task and use field descriptions as the primary means to prompt the model. ```python from opperai import Opper from pydantic import BaseModel, Field import os opper = Opper(http_bearer=os.getenv("OPPER_API_KEY")) # Preferably define a Pydantic model for the input, especially if you have multiple inputs to a call. class UnstructuredRoomText(BaseModel): text: str = Field(description="A text containing information about a hotel room") # Always define clear Pydantic models for the output class StructuredRoom(BaseModel): thoughts: str = Field(description="The thoughts of the model while extracting the room details") # Recommended to always be present to improve quality of the response beds: int = Field(description="The number of people who can sleep in this room") seaview: bool = Field(description="Whether the room has a view to the sea") description: str = Field(description="A description of the room and its features. Always starting with 'This room features...'") def main(): result = opper.call( name="extractRoom", # required, use a unique name for each call/task instructions="Extract the room details from the text", # required input_schema=UnstructuredRoomText, output_schema=StructuredRoom, # recommended model="anthropic/claude-3.7-sonnet", # optional, otherwise the default model will be used input=UnstructuredRoomText(text="Suite at Grand Hotel with two rooms. A master bedroom with a king sized bed and a bed sofa for one. The room has a view to the sea, a large bathroom and a balcony."), # required ) print(result.json_payload) # {'thoughts': 'I need to extract room details...', 'beds': 2, 'seaview': True, 'description': 'This room features a luxurious suite with two rooms...'} main() ``` ## Performing tracing and logging In this example, we implement tracing of a translation task where an article will be translated to 3 languages by 3 calls. Note how a parent span is created first and then used as parent_span_id for the individual translations. ```python from opperai import Opper from pydantic import BaseModel, Field import os class ArticleTranslationTask(BaseModel): summary: str = Field(description="A concise summary of the main points") target_language: str = Field(description="The target language") class Translation(BaseModel): original_language: str = Field(description="The original language of the text") destination_language: str = Field(description="The target language of translation") translated_text: str = Field(description="The translated text") def main(): opper = Opper(http_bearer=os.getenv("OPPER_API_KEY")) article = "The rise of artificial intelligence has transformed many industries. Companies are now using AI to automate tasks, improve customer service, and make better decisions. However, this rapid adoption also raises important questions about ethics and job displacement." # Start a parent span for this content processing workflow session_span = opper.spans.create( name="translate_article", input=str(article) ) target_languages = ["Swedish", "Danish", "Norwegian"] translated_texts = [] for language in target_languages: translation_result = opper.call( name="translate", instructions="Translate the text", input_schema=ArticleTranslationTask, output_schema=Translation, input=ArticleTranslationTask(summary=article, target_language=language), parent_span_id=session_span.id, ) translated_texts.append(translation_result.json_payload['translated_text']) # Update the parent span with the final output opper.spans.update( span_id=session_span.id, output="\n".join(translated_texts) ) main() ``` ## Performing evals with metrics In this example, we evaluate accuracy of a task and save a metric. Note how this test is fast and small and tests the main instruction. Metric will be filterable in the UI. ```python from opperai import Opper from pydantic import BaseModel, Field import os def main(): opper = Opper(http_bearer=os.getenv("OPPER_API_KEY")) result = opper.call( name="answer-question", instructions="Answer the question with one word", input="What is the capital of France?", ) print(result) # Perform a simple evaluation is_one_word = 1 if len(result.message) == 1 else 0 # Save the evaluation to the span opper.span_metrics.create_metric( span_id=result.span_id, dimension="evaluate_is_one_word", value=is_one_word, comment="Evaluated if the answer is one word (1=True, 0=False)" ) main() ``` ## Performing knowledge indexing and retrieval Here is an example of how to index and retrieve data with the Opper SDK. Note how we pick a descriptive knowledge base name, and how we add metadata to documents to be able to use that information when composing responses. ```python from opperai import Opper import os import time from pydantic import BaseModel, Field class Response(BaseModel): references: list[str] | None = Field(default=None, description="A list of references to the sources of the information in the response") response: str = Field(description="The response to the question with each fact reference as a footnote") def main(): opper = Opper(http_bearer=os.getenv("OPPER_API_KEY")) # Try to get existing knowledge base try: knowledge_base = opper.knowledge.get_by_name(knowledge_base_name="my-knowledge-base") except: knowledge_base = None # We prepare the knowledge base if it doesn't exist if knowledge_base is None: knowledge_base = opper.knowledge.create( name="my-knowledge-base", ) # It takes a while for the knowledge base to be ready time.sleep(3) # Add some text with facts to the knowledge base opper.knowledge.add( knowledge_base_id=knowledge_base.id, content="Reddit was founded in 2005. Reddit's headquarters are in San Francisco. Reddit reported a revenue of $100 million in 2023.", metadata={ "source": "https://www.reddit.com", }, ) # Add another document with Reddit related facts opper.knowledge.add( knowledge_base_id=knowledge_base.id, content="Reddit has over 430 million monthly active users. Reddit was acquired by Condé Nast in 2006. Reddit's mascot is named Snoo.", metadata={ "source": "https://www.wikipedia.com/reddit", }, ) query = "What do you know about Reddit?" # Query the knowledge base with a question related to the facts results = opper.knowledge.query( knowledge_base_id=knowledge_base.id, query=query, top_k=5 # optional number of chunks to return ) # Use the call function with the retrieved knowledge response = opper.call( name="answer_question", instructions="You are provided with knowledge about a topic. Answer the question using only this provided knowledge. If there is no knowledge, just say you dont know.", input={ "question": query, "knowledge": results }, output_schema=Response ) print(response.json_payload) main() ```