Filter Knowledge

Use filter expressions through the AgentOS API for precise knowledge base filtering.

When using the AgentOS API, you can apply filters to precisely control which knowledge base documents your agents search, without changing your agent code. Filter expressions serialize to JSON and are automatically reconstructed server-side for programmatic filtering.

Two Approaches to Filtering

Agno supports two ways to filter knowledge through the API:

  • Use dictionary filters for simple "field = value" lookups
  • Use filter expressions when you need OR/NOT logic or ranges

1. Dictionary Filters (Simple)

Best for straightforward equality matching. Send a JSON object with key-value pairs:

{"docs": "agno", "status": "published"}

2. Filter Expressions (Advanced)

Best for complex filtering with full logical control. Send structured filter objects:

{"op": "AND", "conditions": [
  {"op": "EQ", "key": "docs", "value": "agno"},
  {"op": "GT", "key": "version", "value": 2}
]}

When to use which:

  • Use dict filters for simple queries like filtering by category or status
  • Use filter expressions when you need OR logic, exclusions (NOT), range queries (GT/LT/GTE/LTE), inequality (NEQ), or substring/prefix matching (CONTAINS/STARTSWITH)

Filter Operators

Filter expressions support comparison, string-matching, and logical operators.

Filter expression support is backend-specific. PgVector supports the operators listed below. ValkeyDb translates top-level lists of EQ and IN expressions into TAG filters on its indexed fields; unsupported operators, None values, empty IN lists, or conflicting conditions raise ValueError. Unindexed field names produce a warning and are skipped. Other adapters may drop expressions and run without those filters. Check your adapter before relying on an expression.

Within PgVector, only EQ, IN, GT, LT, AND, OR, and NOT are implemented. NEQ, GTE, LTE, CONTAINS, and STARTSWITH deserialize successfully but raise an error during search, and the run returns no knowledge results.

Dictionary filter support and placement vary by backend. For example, LanceDb applies dictionary filters after candidate retrieval, so restrictive filters can return fewer matches even when other matching documents exist.

Comparison Operators

  • EQ(key, value) - Equality: field equals value
  • NEQ(key, value) - Inequality: field does not equal value
  • GT(key, value) - Greater than: field > value
  • GTE(key, value) - Greater than or equal: field >= value
  • LT(key, value) - Less than: field < value
  • LTE(key, value) - Less than or equal: field <= value
  • IN(key, [values]) - Inclusion: field in list of values

String Matching Operators

  • CONTAINS(key, value) - field contains the substring (case-insensitive)
  • STARTSWITH(key, value) - field starts with the given prefix

Logical Operators

  • AND(*filters) - All conditions must be true
  • OR(*filters) - At least one condition must be true
  • NOT(filter) - Negate a condition

Nesting limit: Filter expressions can be nested up to 10 levels deep. Deeper expressions are rejected during deserialization, fall into the error-handling path, and the request proceeds without filters (with a warning logged).

Python operator overloads

Filter expressions support Python's bitwise operators as shorthand for AND, OR, and NOT:

from agno.filters import AND, EQ, GT

# These two are equivalent
EQ("status", "published") & GT("views", 1000)
AND(EQ("status", "published"), GT("views", 1000))

# OR
EQ("priority", "high") | EQ("urgent", True)

# NOT
~EQ("status", "draft")

Serialization Format

Filter expression objects use a dictionary format with an "op" key that distinguishes them from regular dict filters. Field names differ per operator: IN takes values (plural); NOT takes condition (singular); AND/OR take conditions (plural); everything else takes value.

Comparison and string operators

All comparison operators (EQ, NEQ, GT, GTE, LT, LTE) and string operators (CONTAINS, STARTSWITH) share the same shape:

{"op": "EQ", "key": "status", "value": "published"}
{"op": "GTE", "key": "views", "value": 1000}
{"op": "CONTAINS", "key": "title", "value": "agno"}

IN takes values (plural)

{"op": "IN", "key": "category", "values": ["tech", "science"]}

Passing "value" instead of "values" is rejected by the deserializer.

AND and OR take conditions (plural)

{
  "op": "AND",
  "conditions": [
    {"op": "EQ", "key": "status", "value": "published"},
    {"op": "GT", "key": "views", "value": 1000}
  ]
}

NOT takes condition (singular)

{
  "op": "NOT",
  "condition": {"op": "EQ", "key": "status", "value": "archived"}
}

Round-trip example

from agno.filters import EQ, GT, AND

filter_expr = AND(EQ("status", "published"), GT("views", 1000))
filter_expr.to_dict()
# {
#   "op": "AND",
#   "conditions": [
#     {"op": "EQ", "key": "status", "value": "published"},
#     {"op": "GT", "key": "views", "value": 1000}
#   ]
# }

The presence of the "op" key tells the API to deserialize the filter as a filter expression. Regular dict filters (without "op") continue to work for backward compatibility.

Using Filters Through the API

Install uv pip install -U agno requests for the Python clients. The request fragments assume an AgentOS server on port 7777 with the named agent, a configured knowledge base, and documents indexed with the metadata used in each example. See Manage Knowledge for server setup. Replace the agent ID and metadata with your own; add a bearer header if your server requires authentication.

Pass the serialized JSON string via the knowledge_filters field when creating a run.

Dictionary Filters (Simple Approach)

For basic filtering, send a JSON object with key-value pairs. All conditions are combined with AND logic:

import requests
import json

# Simple dict filter
filter_dict = {"docs": "agno", "status": "published"}

# Serialize to JSON
filter_json = json.dumps(filter_dict)

# Send request
response = requests.post(
    "http://localhost:7777/agents/agno-knowledge-agent/runs",
    data={
        "message": "What are agno's key features?",
        "stream": "false",
        "knowledge_filters": filter_json,
    }
)

result = response.json()

More Dict Filter Examples:

# Filter by single field
{"category": "technology"}

# Filter by multiple fields (AND logic)
{"category": "technology", "status": "published", "year": 2024}

# Filter with different data types
{"active": True, "priority": 1, "department": "engineering"}

Filter Expressions (Advanced Approach)

For complex filtering with logical operators and comparisons:

import requests
import json
from agno.filters import EQ

# Create filter expression
filter_expr = EQ("category", "technology")

# Serialize to JSON
filter_json = json.dumps(filter_expr.to_dict())

# Send request
response = requests.post(
    "http://localhost:7777/agents/my-agent/runs",
    data={
        "message": "What are the latest tech articles?",
        "stream": "false",
        "knowledge_filters": filter_json,
    }
)

result = response.json()

Multiple Filter Expressions

Send multiple filter expressions as a JSON array. PgVector casts values to SQL integers for GT and LT; use integer metadata such as year below, not an ISO date string:

import json
import requests
from agno.filters import EQ, GT

# Create multiple filters
filters = [
    EQ("status", "published"),
    GT("year", 2023)
]

# Serialize list to JSON
filters_json = json.dumps([f.to_dict() for f in filters])

response = requests.post(
    "http://localhost:7777/agents/my-agent/runs",
    data={
        "message": "Show recent published articles",
        "stream": "false",
        "knowledge_filters": filters_json,
    }
)

Error Handling

Invalid Filter Structure

When filters have errors, they're gracefully ignored with warnings:

# Missing required fields
curl ... -F 'knowledge_filters={"op": "EQ", "key": "status"}'
# Result: Filter ignored, warning logged

# Unknown operator
curl ... -F 'knowledge_filters={"op": "UNKNOWN", "key": "status", "value": "x"}'
# Result: Filter ignored, warning logged

# Invalid JSON
curl ... -F 'knowledge_filters={invalid json}'
# Result: Filter ignored, warning logged

# Filter nested deeper than 10 levels
curl ... -F 'knowledge_filters={"op": "NOT", "condition": {"op": "NOT", "condition": ...}}'
# Result: Filter ignored, warning logged (depth limit exceeded)

When filters fail to parse, the search proceeds without filters rather than throwing an error. When a contents database tracks metadata keys, unknown dictionary keys and simple top-level expression keys are also removed; if every key is unknown, retrieval can become unfiltered. Without a contents database, that key-validation step is skipped. Validate operators and metadata against your backend, and enforce authorization separately from retrieval filters.

Client-Side Validation

This helper checks serialization and sends the request. JSON encoding alone does not validate supported operators, tracked metadata keys, or access control:

import json
import requests


def send_filtered_agent_request(base_url, agent_id, message, filter_expr, headers=None):
    filter_json = json.dumps(filter_expr.to_dict())
    response = requests.post(
        f"{base_url.rstrip('/')}/agents/{agent_id}/runs",
        headers=headers,
        data={"message": message, "stream": "false", "knowledge_filters": filter_json},
        timeout=60,
    )
    response.raise_for_status()
    return response.json()

Next Steps