Skip to content

Commit 01f900d

Browse files
authored
Merge pull request #258 from nk-ag/niki/dev/statemanagerfrontend
State manager frontend with demo scenarios
2 parents f84523b + bf86fcc commit 01f900d

68 files changed

Lines changed: 13511 additions & 447 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coverage

-108 KB
Binary file not shown.

api-server/tests/integration/test_api_server_integration.py

Lines changed: 454 additions & 0 deletions
Large diffs are not rendered by default.

exosphere-runtimes/.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Environment files (keep .env.example tracked)
2+
.env
3+
.env.*.local
4+
.env.local
5+
*.env
6+
7+
# Virtual environments
8+
**/.venv/
9+
**/venv/
10+
11+
# Python caches
12+
**/__pycache__/
13+
*.py[cod]

exosphere-runtimes/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

exosphere-runtimes/README.md

Whitespace-only changes.

exosphere-runtimes/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def main():
2+
print("Hello from exosphere-runtimes!")
3+
4+
5+
if __name__ == "__main__":
6+
main()
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Parse List of Docs Runtime
2+
3+
A simplified document processing workflow for parsing multiple documents from S3, generating Q&A pairs, and uploading reports back to S3.
4+
5+
## Workflow Overview
6+
7+
This runtime implements a streamlined document processing pipeline:
8+
9+
1. **ListS3FilesNode** - Lists PDF files from an S3 bucket
10+
2. **ParseSinglePDFNode** - Parses a single PDF file from S3
11+
3. **ChunkDocumentNode** - Splits document content into manageable chunks
12+
4. **GenerateQuestionNode** - Generates questions from document chunks
13+
5. **GenerateAnswerNode** - Generates answers to the questions
14+
6. **VerifyAnswerNode** - Verifies the accuracy of generated answers
15+
7. **CreateReportNode** - Creates final reports from verification results
16+
8. **UploadToS3Node** - Uploads reports back to S3
17+
18+
## Setup
19+
20+
1. Install dependencies:
21+
```bash
22+
pip install -r requirements.txt
23+
```
24+
25+
2. Set up environment variables in `.env`:
26+
```
27+
EXOSPHERE_STATE_MANAGER_URI=http://localhost:8000
28+
EXOSPHERE_API_KEY=your-api-key
29+
```
30+
31+
3. Register the runtime:
32+
```bash
33+
python main.py
34+
```
35+
36+
## Usage
37+
38+
Run the graph template to register and trigger the workflow:
39+
40+
```bash
41+
python graph_template.py
42+
```
43+
44+
This will:
45+
- Register the graph template with the state manager
46+
- Trigger the workflow with sample S3 bucket and prefix
47+
- Process the first PDF file in the list
48+
- Generate Q&A for the first chunk
49+
- Create and upload a report
50+
51+
## Configuration
52+
53+
Update the following in `graph_template.py`:
54+
- API keys and secrets
55+
- S3 bucket names and prefixes
56+
- Chunk size and overlap parameters
57+
58+
## Node Details
59+
60+
### ListS3FilesNode
61+
- **Inputs**: `bucket_name`, `prefix`
62+
- **Outputs**: `file_list`, `bucket_name`
63+
- **Secrets**: AWS credentials
64+
65+
### ParseSinglePDFNode
66+
- **Inputs**: `file_key`, `bucket_name`
67+
- **Outputs**: `extracted_content`, `file_key`, `document_title`
68+
- **Secrets**: AWS credentials
69+
70+
### ChunkDocumentNode
71+
- **Inputs**: `extracted_content`, `file_key`, `chunk_size`, `overlap`
72+
- **Outputs**: `chunks`, `file_key`, `total_chunks`
73+
74+
### GenerateQuestionNode
75+
- **Inputs**: `chunk`
76+
- **Outputs**: `source_chunk`, `generated_question`, `chunk_id`
77+
- **Secrets**: OpenAI API key
78+
79+
### GenerateAnswerNode
80+
- **Inputs**: `source_chunk`, `generated_question`, `chunk_id`
81+
- **Outputs**: `generated_answer`, `question`, `source_chunk`, `chunk_id`
82+
- **Secrets**: OpenAI API key
83+
84+
### VerifyAnswerNode
85+
- **Inputs**: `generated_answer`, `question`, `source_chunk`, `chunk_id`
86+
- **Outputs**: `verification_results`, `chunk_id`, `file_key`
87+
- **Secrets**: OpenAI API key
88+
89+
### CreateReportNode
90+
- **Inputs**: `verification_results`, `chunk_id`, `file_key`
91+
- **Outputs**: `final_report`, `file_key`
92+
93+
### UploadToS3Node
94+
- **Inputs**: `final_report`, `file_key`, `output_bucket`, `output_prefix`
95+
- **Outputs**: `upload_status`, `s3_key`, `file_key`
96+
- **Secrets**: AWS credentials
97+
98+
## Notes
99+
100+
- This is a simplified implementation with simulated S3 operations
101+
- In production, replace simulated functions with actual AWS SDK calls
102+
- The workflow processes one file and one chunk at a time
103+
- For processing multiple files/chunks, you would need to implement loops or parallel processing
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Parse List of Docs Graph Template
4+
5+
This file defines the graph template for the simplified document processing workflow.
6+
It processes a list of documents from S3: list files → parse each PDF → upload each to S3.
7+
"""
8+
9+
import asyncio
10+
import aiohttp
11+
import os
12+
13+
# Graph Template Definition
14+
PARSE_LIST_OF_DOCS_GRAPH_TEMPLATE = {
15+
"secrets": {
16+
"openai_api_key": "testkey",
17+
"aws_access_key_id": "testkey",
18+
"aws_secret_access_key": "testkey",
19+
"aws_region": "us-east-1"
20+
},
21+
"nodes": [
22+
{
23+
"node_name": "ListS3FilesNode",
24+
"namespace": "parse-list-of-docs",
25+
"identifier": "list_files",
26+
"inputs": {
27+
"bucket_name": "initial",
28+
"prefix": "initial",
29+
"files_only": "true",
30+
"recursive": "false"
31+
},
32+
"next_nodes": ["parse_pdf"]
33+
},
34+
{
35+
"node_name": "ParseSinglePDFNode",
36+
"namespace": "parse-list-of-docs",
37+
"identifier": "parse_pdf",
38+
"inputs": {
39+
"bucket_name": "initial",
40+
"key": "${{ list_files.outputs.key }}"
41+
},
42+
"next_nodes": ["upload_to_s3"]
43+
},
44+
45+
{
46+
"node_name": "UploadToS3Node",
47+
"namespace": "parse-list-of-docs",
48+
"identifier": "upload_to_s3",
49+
"inputs": {
50+
"extracted_content": "${{ parse_pdf.outputs.extracted_content }}",
51+
"key": "${{ parse_pdf.outputs.key }}",
52+
"output_bucket": "processed-docs-bucket",
53+
"output_prefix": "processed-documents/"
54+
},
55+
"next_nodes": []
56+
}
57+
]
58+
}
59+
60+
async def create_graph_template(graph_name: str):
61+
"""Create a graph template with simplified document processing nodes"""
62+
namespace = "parse-list-of-docs"
63+
api_key = os.getenv("STATE_MANAGER_API_KEY") # TODO: Replace with your actual API key
64+
65+
graph_request = {
66+
"secrets": {
67+
"openai_api_key": "testkey",
68+
"aws_access_key_id": "testkey",
69+
"aws_secret_access_key": "testkey",
70+
"aws_region": "us-east-1"
71+
},
72+
"nodes": PARSE_LIST_OF_DOCS_GRAPH_TEMPLATE["nodes"]
73+
}
74+
75+
async with aiohttp.ClientSession() as session:
76+
# Create the graph template
77+
url = f"http://localhost:8000/v0/namespace/{namespace}/graph/{graph_name}"
78+
headers = {"X-API-Key": api_key, "Content-Type": "application/json"}
79+
80+
try:
81+
async with session.put(url, json=graph_request, headers=headers) as response:
82+
if response.status == 201:
83+
print(f"Parse list of docs graph template created successfully: {graph_name}")
84+
return graph_name
85+
else:
86+
print(f"Failed to create graph template: {response.status}")
87+
print(await response.text())
88+
return None
89+
except Exception as e:
90+
print(f"Error creating graph template: {e}")
91+
return None
92+
93+
async def trigger_graph_execution(graph_name: str):
94+
"""Trigger the first node using the new trigger graph API endpoint"""
95+
namespace = "parse-list-of-docs"
96+
api_key = "niki" # TODO: Replace with your actual API key
97+
98+
# Trigger graph with the first node
99+
trigger_request = {
100+
"states": [
101+
{
102+
"identifier": "list_files",
103+
"inputs": {
104+
"bucket_name": "my-documents-bucket",
105+
"prefix": "pdfs/"
106+
}
107+
}
108+
]
109+
}
110+
111+
async with aiohttp.ClientSession() as session:
112+
url = f"http://localhost:8000/v0/namespace/{namespace}/graph/{graph_name}/trigger"
113+
headers = {"X-API-Key": api_key, "Content-Type": "application/json"}
114+
115+
try:
116+
async with session.post(url, json=trigger_request, headers=headers) as response:
117+
if response.status == 200:
118+
response_data = await response.json()
119+
print("✅ Parse list of docs graph triggered successfully!")
120+
print(f" - Graph: {graph_name}")
121+
print(f" - Run ID: {response_data['run_id']}")
122+
print(f" - State ID: {response_data['states'][0]['state_id']}")
123+
print(f" - Node: {response_data['states'][0]['node_name']}")
124+
print(f" - Status: {response_data['status']}")
125+
return response_data['states'][0]['state_id'], response_data['run_id']
126+
else:
127+
print(f"❌ Failed to trigger graph: {response.status}")
128+
print(await response.text())
129+
return None, None
130+
except Exception as e:
131+
print(f"❌ Error triggering graph: {e}")
132+
return None, None
133+
134+
135+
async def main():
136+
"""Main function to create graph and trigger workflow"""
137+
print("🚀 Starting Parse List of Docs workflow setup...")
138+
139+
# Step 1: Create the graph template
140+
print("\n📋 Step 1: Creating parse list of docs graph template...")
141+
graph_name = "parse-list-of-docs-workflow"
142+
# await create_graph_template(graph_name)
143+
144+
# Step 2: Trigger the graph execution
145+
print("\n🎯 Step 2: Triggering parse list of docs workflow...")
146+
state_id, run_id = await trigger_graph_execution(graph_name)
147+
148+
if state_id and run_id:
149+
print("\n✅ Parse list of docs workflow initiated successfully!")
150+
print(" The workflow will now execute:")
151+
print(" 1. ListS3FilesNode (list_files) - List PDF files from S3")
152+
print(" 2. ParseSinglePDFNode (parse_pdf) - Parse each PDF file")
153+
print(" 3. UploadToS3Node (upload_to_s3) - Upload processed document to S3")
154+
print(f"\n State ID: {state_id}")
155+
print(f" Run ID: {run_id}")
156+
print(f" Graph: {graph_name}")
157+
else:
158+
print("❌ Failed to trigger parse list of docs workflow.")
159+
160+
if __name__ == "__main__":
161+
# Run the async main function
162+
asyncio.run(main())
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dotenv import load_dotenv
2+
from exospherehost import Runtime
3+
from nodes.list_s3_files import ListS3FilesNode
4+
from nodes.parse_single_pdf import ParseSinglePDFNode
5+
from nodes.chunk_document import ChunkDocumentNode
6+
from nodes.generate_question import GenerateQuestionNode
7+
from nodes.generate_answer import GenerateAnswerNode
8+
from nodes.verify_answer import VerifyAnswerNode
9+
from nodes.create_report import CreateReportNode
10+
from nodes.upload_to_s3 import UploadToS3Node
11+
12+
# Load environment variables from .env file
13+
# EXOSPHERE_STATE_MANAGER_URI is the URI of the state manager
14+
# EXOSPHERE_API_KEY is the key of the runtime
15+
load_dotenv()
16+
17+
# Note on node ordering:
18+
# The order of node classes in the `nodes` list does not define execution sequence.
19+
# Nodes are registered with the state manager; orchestration and dependencies are handled externally.
20+
# Nodes are listed in logical processing order for readability only.
21+
Runtime(
22+
name="parse-list-of-docs-runtime",
23+
namespace="parse-list-of-docs",
24+
nodes=[
25+
ListS3FilesNode,
26+
ParseSinglePDFNode,
27+
ChunkDocumentNode,
28+
GenerateQuestionNode,
29+
GenerateAnswerNode,
30+
VerifyAnswerNode,
31+
CreateReportNode,
32+
UploadToS3Node
33+
]
34+
).start()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from exospherehost import BaseNode
2+
from pydantic import BaseModel
3+
4+
5+
class ChunkDocumentNode(BaseNode):
6+
7+
class Inputs(BaseModel):
8+
extracted_content: str
9+
key: str
10+
chunk_size: str = "1000"
11+
overlap: str = "200"
12+
13+
class Outputs(BaseModel):
14+
chunk: str
15+
16+
class Secrets(BaseModel):
17+
pass
18+
19+
async def execute(self) :
20+
"""
21+
Split document content into chunks for processing.
22+
"""
23+
24+
# Convert string inputs to appropriate types
25+
chunk_size = int(self.inputs.chunk_size)
26+
overlap = int(self.inputs.overlap)
27+
28+
content = self.inputs.extracted_content
29+
chunks = []
30+
31+
# Simple chunking by character count
32+
# In production, you might want more sophisticated chunking
33+
start = 0
34+
chunk_id = 0
35+
36+
while start < len(content):
37+
end = min(start + chunk_size, len(content))
38+
chunk_text = content[start:end]
39+
40+
chunk = {
41+
"chunk_id": chunk_id,
42+
"content": chunk_text,
43+
"start_pos": start,
44+
"end_pos": end,
45+
"key": self.inputs.key
46+
}
47+
48+
chunks.append(chunk)
49+
chunk_id += 1
50+
start = end - overlap if end < len(content) else end
51+
52+
return [
53+
self.Outputs(chunk = chunk)
54+
for chunk in chunks
55+
]

0 commit comments

Comments
 (0)