An MCP server for Osprey
at main 262 lines 6.3 kB view raw
1SYNTAX_DATA_EXTRACTION = """\ 2## Data Extraction 3 4### JsonData - Extract values from event JSON 5```python 6FieldName: type = JsonData(path='$.json.path') 7FieldName: Optional[type] = JsonData(path='$.path', required=False) 8``` 9 10### EntityJson - Create typed entities from JSON (for IDs that can have labels) 11```python 12UserId: Entity[str] = EntityJson(type='UserId', path='$.user.id') 13PostId: Entity[str] = EntityJson(type='PostId', path='$.postId') 14``` 15 16### Supported types 17- `str`, `int`, `float`, `bool` 18- `List[str]`, `List[int]`, etc. 19- `Optional[type]` for nullable fields 20- `Entity[type]` for identifiers that can have labels attached 21""" 22 23SYNTAX_RULES = """\ 24## Rules 25 26### Defining a Rule 27```python 28RuleName = Rule( 29 when_all=[ 30 Condition1, 31 Condition2, # ALL conditions must be True 32 ], 33 description='What this rule detects' 34) 35``` 36 37### Rule naming 38- Must be PascalCase (e.g., `SpamDetectionRule`) 39- Cannot start with underscore 40- Description must be a string literal or f-string 41""" 42 43SYNTAX_EFFECTS = """\ 44## Effects and WhenRules 45 46### Wiring rules to effects 47```python 48WhenRules( 49 rules_any=[Rule1, Rule2], # ANY rule triggers ALL effects 50 then=[ 51 DeclareVerdict(verdict='reject'), 52 LabelAdd(entity=UserId, label='spam'), 53 ], 54) 55``` 56 57### Available effects 58- `DeclareVerdict(verdict='...')` - Return verdict to caller 59- `LabelAdd(entity=E, label='L')` - Add label to entity 60- `LabelRemove(entity=E, label='L')` - Remove label from entity 61""" 62 63SYNTAX_OPERATORS = """\ 64## Operators 65 66### Comparison 67- `==`, `!=`, `>`, `>=`, `<`, `<=` 68- `in`, `not in` (for list membership) 69 70### Boolean 71- `and`, `or`, `not` 72- All conditions in `when_all` are implicitly AND-ed 73- Use parentheses for OR: `(Cond1 or Cond2)` 74 75### Null handling 76- `Value != None` - check if not null 77- `Value == None` - check if null 78- Always check for null before using optional fields 79""" 80 81SYNTAX_IMPORTS = """\ 82## File Organization 83 84### Import - Include models/features from other files 85```python 86Import(rules=[ 87 'models/base.sml', 88 'models/record/post.sml', 89]) 90``` 91- Paths must be relative and sorted lexicographically 92- Imported features become available in current file 93 94### Require - Conditionally include rule files 95```python 96Require(rule='rules/spam/check.sml') 97Require(rule='rules/post/links.sml', require_if=EventType == 'post') 98``` 99- Use for conditional rule execution based on event type 100- Required file outputs are NOT available in parent file 101""" 102 103PROJECT_STRUCTURE = """\ 104## Project Structure 105 106Osprey rules projects follow this structure: 107 108``` 109rules/ 110├── main.sml # Entry point - requires index.sml 111├── models/ 112│ ├── base.sml # Common features (UserId, Handle, etc.) 113│ └── record/ 114│ ├── post.sml # Post-specific features 115│ ├── like.sml # Like-specific features 116│ └── profile.sml # Profile-specific features 117└── rules/ 118 ├── index.sml # Routes to event-specific rules 119 └── record/ 120 ├── post/ 121 │ ├── index.sml # Requires post rules 122 │ ├── spam.sml # Spam detection 123 │ └── links.sml # Link abuse detection 124 └── profile/ 125 ├── index.sml 126 └── impersonation.sml 127``` 128 129### Key principles 1301. **models/** - Feature definitions only, no rules 1312. **rules/** - Rule logic with WhenRules effects 1323. **index.sml** - Conditional routing based on event type 1334. Each rule file imports the models it needs 134""" 135 136PATTERN_BASIC_RULE = """\ 137### Basic Rule Pattern 138```python 139Import(rules=[ 140 'models/base.sml', 141 'models/record/post.sml', 142]) 143 144# Define the rule 145SpamLinkRule = Rule( 146 when_all=[ 147 AccountAgeSecondsUnwrapped < Day, 148 PostHasExternal, 149 PostIsReply, 150 ], 151 description='New account posting external links in replies', 152) 153 154# Wire to effects 155WhenRules( 156 rules_any=[SpamLinkRule], 157 then=[ 158 LabelAdd(entity=UserId, label='reply-link-spam'), 159 ], 160) 161``` 162""" 163 164PATTERN_MULTIPLE_RULES = """\ 165### Multiple Rules with Tiered Response 166```python 167# Low severity 168LowRiskRule = Rule( 169 when_all=[Signal1], 170 description='Single signal detected', 171) 172 173# High severity - multiple signals 174HighRiskRule = Rule( 175 when_all=[Signal1, Signal2, Signal3], 176 description='Multiple signals detected', 177) 178 179WhenRules( 180 rules_any=[LowRiskRule, HighRiskRule], 181 then=[ 182 LabelAdd(entity=UserId, label='flagged'), 183 LabelAdd(entity=UserId, label='high-risk', apply_if=HighRiskRule), 184 ], 185) 186``` 187""" 188 189PATTERN_COMPUTED_FEATURES = """\ 190### Computed Features 191```python 192# Compute intermediate values 193FollowRatio = FollowingCount / (FollowersCount + 1) # +1 to avoid division by zero 194IsHighFollowRatio = FollowRatio > 10.0 195 196MessageLength = StringLength(s=PostText) 197IsShortMessage = MessageLength < 10 198 199HasManyMentions = ListLength(list=FacetMentionList) > 5 200 201# Use in rules 202SuspiciousActivity = Rule( 203 when_all=[ 204 IsHighFollowRatio, 205 IsShortMessage, 206 HasManyMentions, 207 ], 208 description='Suspicious activity pattern', 209) 210``` 211""" 212 213PATTERN_NULL_SAFETY = """\ 214### Null-Safe Patterns 215```python 216# For optional fields, always check null first 217OptionalField: Optional[str] = JsonData(path='$.maybe', required=False) 218 219SafeRule = Rule( 220 when_all=[ 221 OptionalField != None, # Guard clause 222 StringLength(s=OptionalField) > 10, 223 ], 224 description='Checks optional field safely', 225) 226 227# Or use ResolveOptional for defaults 228SafeValue: str = ResolveOptional( 229 optional_value=OptionalField, 230 default_value='', 231) 232``` 233""" 234 235 236def get_syntax_reference() -> str: 237 return "\n\n".join( 238 [ 239 "# SML Syntax Reference", 240 SYNTAX_DATA_EXTRACTION, 241 SYNTAX_RULES, 242 SYNTAX_EFFECTS, 243 SYNTAX_OPERATORS, 244 SYNTAX_IMPORTS, 245 ] 246 ) 247 248 249def get_project_structure() -> str: 250 return PROJECT_STRUCTURE 251 252 253def get_patterns_reference() -> str: 254 return "\n\n".join( 255 [ 256 "# Common SML Patterns", 257 PATTERN_BASIC_RULE, 258 PATTERN_MULTIPLE_RULES, 259 PATTERN_COMPUTED_FEATURES, 260 PATTERN_NULL_SAFETY, 261 ] 262 )