Skip to content

Commit 109a670

Browse files
committed
pulumi tool
1 parent 31a7d76 commit 109a670

40 files changed

+2350
-4255
lines changed

.agent/CONTEXT.md

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,130 @@
1-
# 🧠 AI-Optimized Project Context: Antigravity Workspace
1+
# AI-Parrot — Architectural Context
22

3-
## 1. Executive Summary & Core Mission
3+
## What is AI-Parrot
4+
Async-first Python framework for building AI Agents and Chatbots.
5+
Vendor-agnostic: supports OpenAI, Anthropic, Google GenAI, Groq, VertexAI,
6+
HuggingFace via a unified `AbstractClient` interface.
47

5-
**Core Philosophy: "Cognitive-First" & "Artifact-First"**
6-
The agent must not just execute tasks but *think* like a senior engineer. This is achieved through a mandatory "Think-Act-Reflect" loop.
8+
---
9+
10+
## Core Abstractions (always inherit from these)
11+
12+
### AbstractClient
13+
Unified interface for all LLM providers.
14+
Location: `parrot/clients/abstract_client.py`
15+
- Never call provider SDKs directly — always go through AbstractClient
16+
- Implement `async def completion()`, `async def stream()`, `async def embed()`
17+
18+
### AbstractBot / Chatbot / Agent
19+
Location: `parrot/bots/`
20+
- `AbstractBot` — base class for all bots
21+
- `Chatbot` — conversational, stateful, single-LLM
22+
- `Agent` — tool-using, ReAct-style reasoning loop
23+
24+
### AbstractTool / @tool decorator
25+
Location: `parrot/tools/`
26+
- Simple functions: use `@tool` decorator
27+
- Complex collections: inherit `AbstractToolkit`
28+
- Every tool MUST have a docstring — it becomes the LLM's tool description
29+
30+
### AgentCrew
31+
Location: `parrot/bots/orchestration/crew.py`
32+
Three execution modes:
33+
- `run_sequential()` — agents in chain, output feeds next
34+
- `run_parallel()` — agents run concurrently, results merged
35+
- `run_flow()` — DAG-based, dependencies declared via `task_flow()`
36+
37+
### Loaders
38+
Location: `parrot/loaders/`
39+
Transform documents (PDF, HTML, DOCX, etc.) into text chunks for RAG.
40+
Inherit `BaseLoader`, implement `async def load() -> list[Document]`
41+
42+
### Vector Stores
43+
- PgVector: `parrot/vectorstores/pgvector.py` — primary store
44+
- ArangoDB: graph-based, in development
45+
46+
---
47+
48+
## Key Patterns to Follow
49+
50+
### Registering a new component
51+
New bots/tools/clients are registered via decorators:
52+
```python
53+
from parrot.registry import register_agent
54+
55+
@register_agent("my-agent")
56+
class MyAgent(Agent):
57+
...
58+
```
59+
60+
### Async everywhere
61+
```python
62+
# CORRECT
63+
async def process(self, data: str) -> Result:
64+
result = await self.client.completion(data)
65+
return result
66+
67+
# WRONG — never block the event loop
68+
def process(self, data: str) -> Result:
69+
return requests.post(...)
70+
```
71+
72+
### Logging pattern
73+
```python
74+
import logging
75+
76+
class MyComponent:
77+
def __init__(self):
78+
self.logger = logging.getLogger(__name__)
79+
80+
async def method(self):
81+
self.logger.info("Starting operation")
82+
self.logger.debug("Detail: %s", detail)
83+
```
84+
85+
### Pydantic for all structured data
86+
```python
87+
from pydantic import BaseModel, Field
88+
89+
class ToolInput(BaseModel):
90+
query: str = Field(..., description="Used as tool description for LLM")
91+
top_k: int = Field(default=5, ge=1, le=20)
92+
```
93+
94+
---
95+
96+
## What Lives Where
97+
```
98+
parrot/
99+
├── clients/ # LLM provider wrappers (AbstractClient subclasses)
100+
├── bots/ # Bot and Agent implementations
101+
│ └── orchestration/ # AgentCrew, DAG execution
102+
├── tools/ # Tool definitions and toolkits
103+
├── loaders/ # Document loaders for RAG
104+
├── vectorstores/ # PgVector, ArangoDB
105+
├── handlers/ # HTTP handlers (aiohttp-based)
106+
├── memory/ # Conversation memory (Redis-backed)
107+
└── integrations/ # Telegram, MS Teams, Slack, MCP
108+
```
109+
110+
---
7111

8-
1. **Think (Plan):** Before any complex coding, the agent MUST generate a plan in `artifacts/plan_[task_id].md`. This enforces structured thinking.
9-
2. **Act (Execute):** Write clean, modular, and well-documented code following the project's strict standards.
10-
3. **Reflect (Verify):** The agent is responsible for verifying its work, primarily by running `pytest` after making changes. All evidence (logs, test results) is stored in `artifacts/logs/`.
112+
## What NOT to Do
113+
- Never use `requests` or `httpx` — use `aiohttp`
114+
- Never subclass LangChain components — LangChain is removed
115+
- Never store secrets in code — use environment variables
116+
- Never add synchronous blocking code in async methods
117+
- Never modify `abstract_client.py` without discussing first — it's the foundation
11118

12119
---
13120

14-
## 2. Environment, DevOps, and Project Structure
121+
## Current Active Development
122+
Branch: `finance-agents`
123+
Main: `main`
15124

16-
* `.agent/`: Core AI rules and persona. **(Crucial for agent behavior)**.
17-
* `rules.md`: The Agent's Constitution and Directive.
18-
* `rules/python-development.md`: Specific standards for Python development (uv, venv, libraries).
19-
* `artifacts/`: All agent-generated outputs (plans, logs, screenshots).
20-
* `tests/`: The `pytest` test suite.
125+
Active areas (check these before modifying):
126+
- `parrot/bots/orchestration/` — AgentCrew DAG execution
127+
- `parrot/memory/` — Redis-based conversation memory
128+
- `parrot/integrations/mcp/` — MCP server implementation
129+
- `parrot/tools/` — Tool definitions and toolkits
130+
- `parrot/integrations/` — Platform integrations (Whatsapp, Telegram, Slack, MS Teams)

parrot/auth/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Authentication and authorization module for AI-Parrot.
2+
3+
This module provides granular permission control for tools and toolkits.
4+
5+
Public API:
6+
Data Models:
7+
- UserSession: Immutable session with user identity and role claims
8+
- PermissionContext: Request-scoped wrapper for permission checking
9+
10+
Resolvers:
11+
- AbstractPermissionResolver: ABC for custom permission implementations
12+
- DefaultPermissionResolver: RBAC implementation with hierarchy and caching
13+
- AllowAllResolver: Development/testing resolver (allows everything)
14+
- DenyAllResolver: Lockdown resolver (denies restricted tools)
15+
16+
Example:
17+
>>> from parrot.auth import UserSession, PermissionContext, DefaultPermissionResolver
18+
>>> session = UserSession(
19+
... user_id="user-123",
20+
... tenant_id="acme-corp",
21+
... roles=frozenset({'jira.write', 'github.read'})
22+
... )
23+
>>> ctx = PermissionContext(session=session, request_id="req-456")
24+
>>> resolver = DefaultPermissionResolver(role_hierarchy={'jira.write': {'jira.read'}})
25+
>>> await resolver.can_execute(ctx, "create_issue", {'jira.write'})
26+
True
27+
"""
28+
29+
from .permission import PermissionContext, UserSession
30+
from .resolver import (
31+
AbstractPermissionResolver,
32+
AllowAllResolver,
33+
DefaultPermissionResolver,
34+
DenyAllResolver,
35+
)
36+
37+
__all__ = [
38+
# Data models
39+
"UserSession",
40+
"PermissionContext",
41+
# Resolvers
42+
"AbstractPermissionResolver",
43+
"DefaultPermissionResolver",
44+
"AllowAllResolver",
45+
"DenyAllResolver",
46+
]

parrot/auth/permission.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Permission data models for granular tool/toolkit access control.
2+
3+
This module provides foundational data structures for the permission system:
4+
- UserSession: Immutable session carrying user identity and role claims
5+
- PermissionContext: Request-scoped wrapper with session and metadata
6+
7+
These are lightweight structures that flow through the execution chain,
8+
enabling Layer 1 (filtering) and Layer 2 (enforcement) permission checks.
9+
"""
10+
11+
from dataclasses import dataclass, field
12+
from typing import Any, Optional
13+
14+
15+
@dataclass(frozen=True)
16+
class UserSession:
17+
"""Minimal session carrying identity and role claims.
18+
19+
Immutable and hashable — safe for use as cache keys in permission resolvers.
20+
21+
Attributes:
22+
user_id: Unique identifier for the user.
23+
tenant_id: Tenant/organization identifier for multi-tenant deployments.
24+
roles: Set of role claims (e.g., frozenset({'jira.manage', 'github.read'})).
25+
Uses frozenset for immutability and hashability.
26+
metadata: Optional additional session metadata (e.g., auth provider info).
27+
Note: metadata dict contents should be immutable for cache safety.
28+
29+
Example:
30+
>>> session = UserSession(
31+
... user_id="user-123",
32+
... tenant_id="acme-corp",
33+
... roles=frozenset({'jira.write', 'github.read'})
34+
... )
35+
>>> 'jira.write' in session.roles
36+
True
37+
>>> hash(session) # Hashable for cache keys
38+
-1234567890
39+
"""
40+
41+
user_id: str
42+
tenant_id: str
43+
roles: frozenset[str]
44+
metadata: dict[str, Any] = field(default_factory=dict, hash=False, compare=False)
45+
46+
def __post_init__(self) -> None:
47+
"""Validate that roles is a frozenset."""
48+
if not isinstance(self.roles, frozenset):
49+
# Convert to frozenset if necessary (for convenience)
50+
object.__setattr__(self, 'roles', frozenset(self.roles))
51+
52+
def has_role(self, role: str) -> bool:
53+
"""Check if session has a specific role.
54+
55+
Args:
56+
role: Role name to check.
57+
58+
Returns:
59+
True if the role is present in the session's roles.
60+
"""
61+
return role in self.roles
62+
63+
def has_any_role(self, roles: set[str] | frozenset[str]) -> bool:
64+
"""Check if session has any of the specified roles.
65+
66+
Args:
67+
roles: Set of role names to check.
68+
69+
Returns:
70+
True if at least one role is present.
71+
"""
72+
return bool(self.roles & roles)
73+
74+
75+
@dataclass
76+
class PermissionContext:
77+
"""Request-scoped wrapper grouping session with extra context.
78+
79+
This is the primary object passed through the permission checking pipeline.
80+
It wraps an immutable UserSession with mutable request-specific metadata.
81+
82+
Attributes:
83+
session: The underlying UserSession with identity and roles.
84+
request_id: Optional request/correlation ID for tracing.
85+
extra: Additional request-scoped metadata (e.g., source IP, API version).
86+
87+
Example:
88+
>>> session = UserSession(
89+
... user_id="user-123",
90+
... tenant_id="acme-corp",
91+
... roles=frozenset({'admin'})
92+
... )
93+
>>> ctx = PermissionContext(
94+
... session=session,
95+
... request_id="req-456",
96+
... extra={"source": "api", "version": "v2"}
97+
... )
98+
>>> ctx.user_id
99+
'user-123'
100+
>>> ctx.roles
101+
frozenset({'admin'})
102+
"""
103+
104+
session: UserSession
105+
request_id: Optional[str] = None
106+
extra: dict[str, Any] = field(default_factory=dict)
107+
108+
@property
109+
def user_id(self) -> str:
110+
"""Get the user ID from the underlying session."""
111+
return self.session.user_id
112+
113+
@property
114+
def tenant_id(self) -> str:
115+
"""Get the tenant ID from the underlying session."""
116+
return self.session.tenant_id
117+
118+
@property
119+
def roles(self) -> frozenset[str]:
120+
"""Get the roles from the underlying session."""
121+
return self.session.roles
122+
123+
def has_role(self, role: str) -> bool:
124+
"""Check if session has a specific role.
125+
126+
Args:
127+
role: Role name to check.
128+
129+
Returns:
130+
True if the role is present.
131+
"""
132+
return self.session.has_role(role)
133+
134+
def has_any_role(self, roles: set[str] | frozenset[str]) -> bool:
135+
"""Check if session has any of the specified roles.
136+
137+
Args:
138+
roles: Set of role names to check.
139+
140+
Returns:
141+
True if at least one role is present.
142+
"""
143+
return self.session.has_any_role(roles)

0 commit comments

Comments
 (0)