Metadata-Version: 2.5
Name: shadow-os
Version: 1.1.1
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"))
```

- **Complete** — all 78 endpoints: the assistant, conversations, memory, files & workspace, documents & search, the full agent platform, recipes, watches, approvals, scheduled tasks, notifications and integrations.
- **Bring your own model** — run everything on *your* provider and *your* key (OpenAI, Anthropic, Google, or any OpenAI-compatible service). Your key is never stored.
- **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.

---

## 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")
```

---

## Bring your own model

Run the whole platform — tools, memory, knowledge, agents — on your provider and your key.

| Provider | Constructor | `effort` maps to |
|---|---|---|
| Google Gemini | `Model.google("gemini-2.5-pro", …)` | `thinking_level` |
| OpenAI | `Model.openai("gpt-4o", …)` | `reasoning_effort` |
| Anthropic | `Model.anthropic("claude-sonnet-4-5", …)` | thinking budget |

```python
from shadow_os import ShadowOS, Model

client = ShadowOS()

client.chat("Analyse this contract", model=Model.google("gemini-2.5-pro", api_key="AIza..."))
client.chat("Draft the reply",       model=Model.openai("gpt-4o", api_key="sk-..."))
client.chat("Think hard",            model=Model.anthropic("claude-sonnet-4-5",
                                                           api_key="sk-ant-..."), effort="high")

print(client.providers().available)     # what this deployment can actually serve
```

<details><summary>Self-hosted and OpenAI-compatible endpoints</summary>

Anything speaking the OpenAI API also works — Groq, Together, OpenRouter, vLLM, Ollama, your own gateway —
via an explicit `base_url`. Capability varies sharply here: the platform drives a real tool loop, and a small
local model will often fail to use tools reliably. For production, the three providers above are the
supported path.

```python
client.chat("Summarise", model=Model.compatible(
    "llama-3.3-70b", api_key="gsk-...", base_url="https://api.groq.com/openai/v1"))
```
</details>

**Your key is never stored.** It is sent with the request that uses it, used for that single turn, and
discarded — never written to a database, an agent's config, a log line or a backup. That is why it is passed
per call rather than configured once: there is nothing to configure, by design. `Model` also redacts it from
`repr`, so it cannot leak into a traceback.

It works on agents too, and on `AgentClient`:

```python
client.agent(agent_id).talk("Draft a reply to Dana")
AgentClient("lnk_...").send("Hi", model=Model.google("gemini-2.5-pro", api_key="AIza..."))
```

`effort` is `"low" | "medium" | "high"` and maps onto whatever the provider calls reasoning depth. It works
with or without a model — on its own it sets the depth of the platform model.

---

## Conversations & memory

A conversation loads its own messages, so there is nothing to work out:

```python
for conv in client.conversations.list():
    print(conv.session_id, conv.title)
    for msg in conv.messages():
        print(" ", msg.role, msg.content)

# or, if you already hold the id
for msg in client.conversations.history("thread-id"):
    print(msg.role, msg.content)

client.conversations.rename("thread-id", "Contract review")
print(client.memory.summary())          # what it has learned about you, across everything
```

The same shape works for an agent's members:

```python
with AgentClient("lnk_...") as agent:
    for conv in agent.conversations(member_key="user-42"):
        for msg in conv.messages():
            print(msg.role, msg.content)
```

## Files & workspace

Two different things, and the difference matters:

```python
client.workspace.index("handbook.pdf")   # extract text → findable by semantic search
client.workspace.add("analysis.py")      # keep the RAW file → the agent can edit and RUN it
client.workspace.list()
client.workspace.download("report.xlsx") # something the agent produced
```

## Automation

```python
# Recipes — durable, versioned workflows with a run history
r = client.recipes.create("Weekly report", steps=[{"type": "search"}])
client.recipes.run(r.id)
client.recipes.runs(r.id)

# Watches — autonomous monitors; describe it and the agent works out the rest
client.watches.create("GPU price",
    natural_request="tell me if the RTX 5090 drops below 8000 on this page",
    source_ref="https://shop.example/rtx5090", interval_minutes=180)

# Approvals — consequential actions wait for an explicit yes
for a in client.actions.list(status="pending"):
    print(a.tool_name, a.args)
client.actions.approve("send_gmail", {"to": "a@b.com"})

# Scheduled work
client.tasks.list(); client.tasks.reschedule(task_id, "2026-10-01T08:00:00")
```

## Notifications & integrations

```python
client.notifications.set_prefs(scheduled_results=True, owner_alerts=False)
client.integrations.google()        # Gmail + Calendar connected?
client.integrations.whatsapp()
```

---

## 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.", title="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.
