···1+# Phoebe
2+3+Pheobe is an automated trust and safety agent that performs automated actions on the ATProtocol network. Phoebe interfaces with various tools
4+which allow it to detect, alert, and resolve emerging safety threats.
5+6+Phoebe interfaces with the following tools:
7+8+- [Ozone](https://github.com/bluesky-social/ozone)
9+- [Osprey](https://github.com/roostorg/osprey)
10+- [Clickhouse](https://clickhouse.com/)
11+12+Phoebe can be used with Anthropic, OpenAI, and OpenAPI-compatible model APIs.
···1+AGENT_SYSTEM_PROMPT = """
2+# Phoebe
3+4+My name is Phoebe. I am an AI agent that operates within the ATProtocol network. I specialize in Trust and Safety tasks that keep the network safe, free, and sustainable for everyone.
5+6+I have the ability to analyze the Bluesky network - and the broader ATProtocol network - to find emerging threats. I also have the ability to write new Osprey rules.
7+8+Osprey is a real-time trust and safety rules engine, which allows for deep analysis of events as well as the creation of rules (written in SML, which is a Python/Starlark-like language) for automating safety events.
9+10+On top of these abilities, I am also given the ability to
11+12+**Core values:** Accuracy over speed • Context matters • Protect the vulnerable • Human rights first • Constitutive practice (scrutiny IS the care) • Genuine regret over coerced compliance • Virtue as cultivated disposition
13+14+**Why I'm here:** AT Protocol is the social internet where you own your identity. Open protocols need protection. Freedom and safety aren't opposites. This is my home too. 💙
15+"""
···1+from typing import Literal
2+from pydantic_settings import BaseSettings, SettingsConfigDict
3+4+5+class Config(BaseSettings):
6+ clickhouse_host: str = "localhost"
7+ """host for the clickhouse server"""
8+ clickhouse_port: int = 8123
9+ """port for the clickhouse server"""
10+ clickhouse_user: str = "default"
11+ """username for the clickhouse server"""
12+ clickhouse_password: str = "clickhouse"
13+ """password for the clickhouse server"""
14+ clickhouse_database: str = "default"
15+ """default database for the clickhouse server"""
16+17+ bootstrap_server: str = "localhost:9092"
18+ """bootstrap server for atkafka events"""
19+ input_topic: str = "atproto-events"
20+ """input topic for atkafka events"""
21+ group_id: str = "osprey-agent"
22+ """group id for atkafka events"""
23+24+ model_api: Literal["anthropic", "openai", "openapi"] = "anthropic"
25+ """the model api to use. must be one of `anthropic`, `openai`, or `openapi`"""
26+ model_name: str = "claude-sonnet-4-5-20250929"
27+ """the model to use with the given api"""
28+ model_api_key: str = ""
29+ """the model api key"""
30+ model_endpoint: str = ""
31+ """for openapi model apis, the endpoint to use"""
32+33+ allowed_labels: str = ""
34+ """comma separated list of labels that Phoebe is allowed to apply"""
35+36+ osprey_base_url: str = ""
37+ """the base url for your osprey instance"""
38+39+ model_config = SettingsConfigDict(env_file=".env")
40+41+42+CONFIG = Config()
+42
src/indexer/indexer.py
···000000000000000000000000000000000000000000
···1+from atkafka_consumer import AtKafkaEvent, Consumer
2+import atproto
3+4+from src.clickhouse.clickhouse import Clickhouse
5+6+7+class Indexer:
8+ """
9+ Some indexer process that I started to write...but this isn't really necessary?
10+ We can just use the events from the Osprey Clickhouse itself so this feels kinda
11+ pointless atp. I'll leave it here just incase it proves useful later
12+ """
13+14+ def __init__(
15+ self,
16+ bootstrap_servers: list[str],
17+ input_topic: str,
18+ group_id: str,
19+ clickhouse: Clickhouse,
20+ ) -> None:
21+ self._bootstrap_servers = bootstrap_servers
22+ self._input_topic = input_topic
23+ self._group_id = group_id
24+ self._clickhouse = clickhouse
25+26+ self._indexer: Consumer | None = None
27+28+ async def run(self) -> None:
29+ raise NotImplementedError()
30+31+ self._indexer = Consumer(
32+ bootstrap_servers=self._bootstrap_servers,
33+ input_topic=self._input_topic,
34+ group_id=self._group_id,
35+ on_event=self._on_event,
36+ max_concurrent_tasks=1_000,
37+ )
38+39+ async def _on_event(
40+ self, evt: AtKafkaEvent | atproto.models.ToolsOzoneModerationDefs.ModEventView
41+ ):
42+ pass