Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
SLACK_SIGNING_SECRET=

# --- Jira connector -------------------------------------------------------
# https://auth.atlassian.com → get the API keys
ATLASSIAN_API=
ATLASSIAN_AUTH=

# Where the OAuth redirect and the Events API URL point. In dev these are the
# ngrok/Cloudflare tunnel hostname, NOT localhost — Slack has to reach it.
# BACKEND_URL must match the Redirect URL registered in the Slack app exactly.
Expand Down
247 changes: 247 additions & 0 deletions backend/docs/jira-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Jira integration

How a Jira issue becomes a memory

## 1. The flow

```text
Jira Cloud
│ User clicks Connect
GET /api/jira/install
Atlassian OAuth 2.0
GET /api/jira/callback
├─ verify state
├─ exchange code for tokens
├─ get Jira Cloud ID
└─ store installation
POST /api/jira/sync
├─ run JQL
├─ fetch issues
├─ record in `source_events`
│ └─ already seen → skip
└─ background: normalize → classify → persist
memories + provenance + embeddings
update `source_events`
````

Two tables are involved:

| table | purpose |
| -------------------- | -------------------------------------------- |
| `jira_installations` | Stores Jira connection and OAuth information |
| `source_events` | Stores raw Jira issues and tracks ingestion |

`source_events` is shared with Slack so every connector uses the same ingestion
and audit system.

---

## 2. What you do on Atlassian

### a. Create the app

Go to:

[https://developer.atlassian.com/console/myapps/](https://developer.atlassian.com/console/myapps/)

Create an app using **OAuth 2.0 (3LO)**.

### b. Add credentials

Add these to `backend/.env`:

```bash
JIRA_CLIENT_ID=...
JIRA_CLIENT_SECRET=...
```

Keep the client secret on the backend.

### c. Configure redirect URL

If using ngrok:

```bash
ngrok http 8000
```

Then set:

```bash
BACKEND_URL=https://your-ngrok-url.ngrok-free.app
FRONTEND_URL=http://localhost:5173
```

In the Atlassian app, add:

```text
https://your-ngrok-url.ngrok-free.app/api/jira/callback
```

This must exactly match `BACKEND_URL + /api/jira/callback`.

### d. Add scopes

For read-only Jira ingestion:

```text
read:jira-work
read:jira-user
offline_access
```

We don't need Jira write permissions.

---

## 3. Connect Jira

From the application:

```text
Sources → Jira → Connect
```

This starts the OAuth flow.

After approval:

```text
Atlassian
/api/jira/callback
jira_installations
Jira connected
```

---

## 4. Sync issues

The connector uses JQL to fetch issues.

Example:

```json
{
"jql": "project = ENG ORDER BY updated DESC",
"max_results": 100
}
```

The backend fetches:

```text
Issue key
Summary
Description
Project
Issue type
Status
Priority
Labels
Components
Reporter
Assignee
Created
Updated
Jira URL
```

Then:

```text
Jira issue
source_events
normalize()
AI classification
memory + embedding
```

---

## 5. Issue updates

Issues can change, so the external ID should include the update timestamp:

```text
jira:{cloud_id}:{issue_key}:{updated_timestamp}
```

Example:

```text
jira:abc123:ENG-123:2026-08-12T14:30:00
```

This means:

```text
Same issue + same update → skip

Same issue + new update → ingest again
```

---

## 6. API endpoints

| endpoint | purpose |
| ----------------------------- | ----------------------- |
| `GET /api/jira/install` | Start OAuth |
| `GET /api/jira/callback` | Handle OAuth callback |
| `GET /api/jira/status` | Check connection |
| `DELETE /api/jira/disconnect` | Disconnect Jira |
| `POST /api/jira/sync` | Fetch and ingest issues |
| `GET /api/jira/activity` | View ingestion activity |

---

## 7. Configuration

```bash
JIRA_CLIENT_ID=...
JIRA_CLIENT_SECRET=...

BACKEND_URL=http://localhost:8000
FRONTEND_URL=http://localhost:5173
```

---

# End improvements

These are **not required for the MVP**, but can be added later:

1. **Pagination** — fetch more than 100 issues safely.
2. **Jira webhooks** — automatically ingest new/updated issues instead of polling.
3. **Token encryption** — encrypt OAuth tokens in production.
4. **Token refresh** — automatically refresh expired access tokens.
5. **Comments** — add Jira comments to the knowledge base.
6. **Attachments** — extract and index PDFs, documents, etc.
7. **Linked issues** — include relationships between Jira issues.
8. **Changelog** — track important issue history.
9. **ACL support** — preserve Jira permissions in the knowledge base.
10. **Background queue** — replace in-process `BackgroundTasks` with a real worker.
11. **Version cleanup** — remove/supersede old embeddings when an issue changes.
12. **Semantic deduplication** — avoid storing duplicate knowledge from similar issues.
13. **Multi-workspace support** — replace the current user-based workspace model.
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"python-dotenv>=1.2.2",
"slack-bolt>=1.30.0",
"uvicorn[standard]>=0.52.1",
"tavily-python>=0.7.27",
]

[project.optional-dependencies]
Expand Down
105 changes: 105 additions & 0 deletions backend/src/ai/agents/ota.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from ai.providers.llm import ChatMessage, ChatProvider, ProviderError, get_provider


class OtaAgent:
"""
Base Observe -> Think -> Act agent.

Subclasses can customize the role prompts and add domain-specific
tools or behavior without changing the OTA interface.
"""

_BASE_PROMPT = """
You are part of an Observe-Think-Act system.
Follow your assigned role, use only the provided information, and do not invent facts.
""".strip()

_OBSERVER_PROMPT = """
Observe the current input or state.
Extract relevant facts, changes, constraints, and uncertainties.
Do not make decisions or take actions.
""".strip()

_THINKER_PROMPT = """
Analyze the observation and determine the next best action.
Use the available context and tools.
Return a concise decision and the information needed to act.
""".strip()

_ACTOR_PROMPT = """
Execute the selected action using the available tools or capabilities.
Do not claim an action succeeded unless it actually did.
Return the result or clearly report why the action could not be completed.
""".strip()

def __init__(self, provider: str):
self.llm = get_provider(provider)

def _build_prompt(
self,
role_prompt: str,
system_prompt: str = "",
) -> str:
parts = [
self._BASE_PROMPT,
role_prompt,
system_prompt.strip(),
]

return "\n\n".join(
part for part in parts if part
)

async def _chat(
self,
message: str,
system_prompt: str,
) -> str:
try:
response = await self.llm.chat(
messages=[ChatMessage("user", message)],
system=system_prompt,
)
return response.content

except ProviderError:
raise

async def observe(
self,
message: str,
system_prompt: str = "",
) -> str:
return await self._chat(
message,
self._build_prompt(
self._OBSERVER_PROMPT,
system_prompt,
),
)

async def think(
self,
observation: str,
system_prompt: str = "",
) -> str:
return await self._chat(
observation,
self._build_prompt(
self._THINKER_PROMPT,
system_prompt,
),
)

async def act(
self,
action: str,
system_prompt: str = "",
) -> str:
return await self._chat(
action,
self._build_prompt(
self._ACTOR_PROMPT,
system_prompt,
),
)
Loading