Metadata-Version: 2.5
Name: shadow-os
Version: 1.0.0
Summary: Official Python SDK for Shadow-OS — build, operate and talk to AI agents.
Project-URL: Homepage, https://shadow-os-ai.vercel.app
Project-URL: Documentation, https://shadow-os-ai.vercel.app/developers
Project-URL: Source, https://github.com/daniel-sha/ai_web
Project-URL: Issues, https://github.com/daniel-sha/ai_web/issues
Author: Shadow-OS
License: MIT
Keywords: agents,ai,chatbot,llm,rag,sdk,shadow-os,whatsapp
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: anyio>=3.6
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Description-Content-Type: text/markdown

# Shadow-OS Python SDK

The official Python client for [Shadow-OS](https://shadow-os-ai.vercel.app) — build, operate and talk to AI agents.

```bash
pip install shadow-os
```

```python
from shadow_os import ShadowOS

with ShadowOS() as client:          # reads $SHADOW_OS_API_KEY
    print(client.chat("Summarise the notes I uploaded yesterday"))
```

- **Typed** — every response is a dataclass, the package ships `py.typed`, and unknown server fields stay reachable via `.raw`.
- **Sync and async** — `ShadowOS` and `AsyncShadowOS` share one definition per endpoint, so they cannot drift.
- **Resilient** — timeouts, exponential backoff with jitter, `Retry-After` support, and automatic retries for cold starts and 5xx.
- **Complete** — everything the platform can do, not just chat: agents, knowledge, access links, customers, escalations, appointments, analytics.

---

## Authentication

Two credentials, for two different jobs.

| Credential | Looks like | Use it for |
|---|---|---|
| **Account key** | `sk-shadow-…` | Your account: chat, documents, and managing agents you own. |
| **Agent token / share code** | `sk-agent-…` / `lnk_…` | Talking *to* one configured agent, as your end-users do. |

```python
client = ShadowOS("sk-shadow-…")            # or set SHADOW_OS_API_KEY
agent  = AgentClient("lnk_…")               # or set SHADOW_OS_AGENT_TOKEN
```

Create an account key at [/developers](https://shadow-os-ai.vercel.app/developers).

---

## Your account assistant

```python
reply = client.chat("What did we agree with Dana?", session_id="conv-42")
print(reply.answer, reply.usage)
```

`session_id` is an **isolation boundary**, not just a label: memory, uploaded files and search are scoped to
it, so one end-user can never see another's. Pass `scope="account"` to opt into the shared account space
(which also has access to connected integrations such as Gmail).

### Documents & semantic search

```python
client.documents.upload("handbook.pdf", session_id="conv-42")
for doc in client.documents.list(session_id="conv-42"):
    print(doc.name)

for hit in client.documents.search("refund policy", top_k=5, session_id="conv-42"):
    print(hit.score, hit.source, hit.text[:120])
```

### Quota

```python
u = client.usage()
print(f"{u.used}/{u.quota} used — {u.remaining} left")
```

---

## Building an agent

```python
from shadow_os import ShadowOS, AgentConfig

client = ShadowOS()

created = client.agents.create(
    name="Nona Pizza",
    template="pizzeria",                       # or persona="..." for full control
    fields={"business_name": "Nona", "hours": "Sun–Thu 11:00–23:00"},
    config=AgentConfig(memory_mode="single", tone="warm and brief"),
)
print(created.id, created.manager_token)       # the token is shown ONCE — store it now
```

Browse what's available before you build:

```python
for t in client.templates():
    print(t.id, t.name, [f["id"] for f in t.fields])

print(client.tool_catalog().groups)            # optional tool bundles you can enable per agent
```

### Teaching it

```python
agent = client.agent(created.id)

agent.knowledge.add_file("menu.pdf", description="Full menu with prices")
agent.knowledge.add_file("storefront.jpg")     # images are understood AND sendable to customers
agent.knowledge.add_url("https://nona.example/about")
agent.knowledge.add_text("We deliver within 4km. Closed on holidays.", name="policies")

agent.knowledge.add_sendable("invoice-template.pdf")   # deliverable, but NOT searchable knowledge
```

### Sharing it

```python
link = agent.share_link()                      # persistent, created once, stable forever
print(link.web, link.whatsapp)

staff = agent.access.create_share_link(role="manager", label="Front desk")
api   = agent.access.create_token(role="member", label="Website widget")
```

### Operating it

```python
for c in agent.customers():
    print(c.display_name, "·", c.profile)

for e in agent.escalations():
    agent.answer_escalation(e.id, "Yes — we're open until 23:00 on Sunday.")

print(agent.analytics().raw)
print(agent.appointments())
print(agent.delivery_status())
```

Talk to it **as its manager** — it sees your customer roster and acts with your authority:

```python
print(agent.talk("Message Dana that her order is ready"))
```

---

## Talking to an agent (what you embed)

```python
from shadow_os import AgentClient

with AgentClient("lnk_…") as agent:
    info = agent.info()
    print(info["agent_name"], info["welcome"])

    reply = agent.send("Are you open on Sunday?", member_key="user-7f3c")
    print(reply.answer, reply.files)
```

`member_key` is the identity of the person you are speaking for. Give each end-user a **stable, unguessable**
id and each gets private, durable memory and history inside the agent — they can never see each other.

```python
dana = agent.for_member("user-7f3c", member_name="Dana")   # shares the connection pool
for conv in dana.conversations():
    print(conv.session_id, conv.title)
for msg in dana.history(session_id="main"):
    print(msg.role, msg.content)
```

---

## Async

Identical surface, awaited:

```python
from shadow_os import AsyncShadowOS

async with AsyncShadowOS() as client:
    reply = await client.chat("hello")
    agents = await client.agents.list()
    stats  = await client.agent(agents[0].id).analytics()
```

---

## Errors

```python
from shadow_os import QuotaExceeded, RateLimited, ShadowOSError

try:
    client.chat("hello")
except QuotaExceeded as e:
    print("out of quota:", e.usage)
except RateLimited as e:
    print("slow down, retry in", e.retry_after)
except ShadowOSError as e:
    print(e.status, e.code, e.message, e.request_id)
```

Every error carries `status`, `code`, `message` and — when the server sent one — `request_id`. Quote that id
in a support request and the exact call can be found in the logs.

| Exception | HTTP |
|---|---|
| `BadRequestError` | 400 / 422 |
| `AuthenticationError` | 401 |
| `QuotaExceeded` | 402 |
| `PermissionDeniedError` | 403 |
| `NotFoundError` | 404 |
| `ConflictError` | 409 |
| `PayloadTooLarge` | 413 |
| `RateLimited` | 429 |
| `ServerError` / `ServiceUnavailable` | 5xx |
| `APITimeoutError` / `APIConnectionError` | no response |

---

## Configuration

```python
client = ShadowOS(
    api_key="sk-shadow-…",       # or $SHADOW_OS_API_KEY
    base_url="https://…",        # or $SHADOW_OS_BASE_URL — for self-hosting or staging
    timeout=120.0,               # per request; generous on purpose (the service can cold-start)
    max_retries=3,               # timeouts, connection errors, 429 and 5xx
)
```

Bring your own `httpx` client (proxies, custom TLS, shared pools):

```python
import httpx
client = ShadowOS(http_client=httpx.Client(proxies="http://…", timeout=60))
```

Retries use exponential backoff with full jitter. `Retry-After` always wins. 4xx responses other than 429
are never retried — they will not become a 200.

---

## Development

```bash
pip install -e ".[dev]"
pytest
```

MIT licensed.
