Skip to content

Commit 96da357

Browse files
refactor: the introduced types somewhere central
1 parent c731b11 commit 96da357

File tree

4 files changed

+117
-32
lines changed

4 files changed

+117
-32
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
types for direct lambda invocation - defines contracts for bypassing slack
3+
4+
centralizes all type definitions for direct invocation flow to avoid scattered
5+
inline type hints across handlers and processors.
6+
"""
7+
8+
from typing import Any, TypedDict, Literal
9+
from datetime import datetime, timezone
10+
11+
12+
class DirectInvocationRequest(TypedDict, total=False):
13+
"""payload contract for direct lambda calls - bypasses slack entirely"""
14+
15+
invocation_type: Literal["direct"]
16+
query: str
17+
session_id: str | None # conversation continuity across calls
18+
19+
20+
class DirectInvocationResponseData(TypedDict):
21+
"""successful ai response payload - matches slack handler output format"""
22+
23+
text: str
24+
session_id: str | None
25+
citations: list[dict[str, str]] # [{title: str, uri: str}, ...]
26+
timestamp: str # iso8601 with Z suffix
27+
28+
29+
class DirectInvocationErrorData(TypedDict):
30+
"""error response payload - consistent structure for all failure modes"""
31+
32+
error: str
33+
timestamp: str # iso8601 with Z suffix
34+
35+
36+
class DirectInvocationResponse(TypedDict):
37+
"""complete lambda response envelope - includes status code + payload"""
38+
39+
statusCode: int
40+
response: DirectInvocationResponseData | DirectInvocationErrorData
41+
42+
43+
class AIProcessorResponse(TypedDict):
44+
"""ai processor output - shared between slack and direct invocation"""
45+
46+
text: str
47+
session_id: str | None
48+
citations: list[dict[str, str]]
49+
# TODO: ensure proper typing for bedrock response when refactoring other types in the future
50+
kb_response: dict[str, Any] # raw bedrock data for slack session handling
51+
52+
53+
# type guards for runtime validation
54+
def is_valid_direct_request(event: dict[str, Any]) -> bool:
55+
"""validate direct invocation payload structure"""
56+
return (
57+
event.get("invocation_type") == "direct"
58+
and isinstance(event.get("query"), str)
59+
and bool(event.get("query", "").strip()) # non-empty after whitespace removal
60+
)
61+
62+
63+
def create_success_response(
64+
text: str, session_id: str | None, citations: list[dict[str, str]]
65+
) -> DirectInvocationResponse:
66+
"""factory for successful direct invocation responses"""
67+
return {
68+
"statusCode": 200,
69+
"response": {
70+
"text": text,
71+
"session_id": session_id,
72+
"citations": citations,
73+
"timestamp": datetime.now(timezone.utc).isoformat(),
74+
},
75+
}
76+
77+
78+
def create_error_response(status_code: int, error_message: str) -> DirectInvocationResponse:
79+
"""factory for error responses - ensures consistent timestamp format"""
80+
return {
81+
"statusCode": status_code,
82+
"response": {
83+
"error": error_message,
84+
"timestamp": datetime.now(timezone.utc).isoformat(),
85+
},
86+
}

packages/slackBotFunction/app/handler.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@
66
2. Processes async operations when invoked by itself to avoid timeouts
77
"""
88

9-
from datetime import datetime
109
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
1110
from aws_lambda_powertools.utilities.typing import LambdaContext
1211

12+
from typing import Any
13+
1314
from app.core.config import get_logger
15+
from app.core.types import (
16+
DirectInvocationResponse,
17+
is_valid_direct_request,
18+
create_success_response,
19+
create_error_response,
20+
)
1421
from app.services.app import get_app
1522
from app.slack.slack_events import process_pull_request_slack_action, process_pull_request_slack_event
1623

@@ -62,38 +69,26 @@ def handler(event: dict, context: LambdaContext) -> dict:
6269
return slack_handler.handle(event=event, context=context)
6370

6471

65-
def handle_direct_invocation(event: dict, context: LambdaContext) -> dict:
72+
def handle_direct_invocation(event: dict[str, Any], context: LambdaContext) -> DirectInvocationResponse:
6673
"""direct lambda invocation for ai assistance - bypasses slack entirely"""
6774
try:
68-
query = event.get("query")
69-
session_id = event.get("session_id")
75+
# validate request structure using type guard
76+
if not is_valid_direct_request(event):
77+
return create_error_response(400, "Missing required field: query")
7078

71-
if not query or not query.strip():
72-
return {
73-
"statusCode": 400,
74-
"response": {
75-
"error": "Missing required field: query",
76-
"timestamp": datetime.utcnow().isoformat() + "Z",
77-
},
78-
}
79+
query = event["query"]
80+
session_id = event.get("session_id")
7981

8082
# shared logic: same AI processing as slack handlers use
8183
from app.services.ai_processor import process_ai_query
8284

8385
ai_response = process_ai_query(query, session_id)
8486

85-
return {
86-
"statusCode": 200,
87-
"response": {
88-
"text": ai_response["text"],
89-
"session_id": ai_response["session_id"],
90-
"citations": ai_response["citations"],
91-
"timestamp": datetime.utcnow().isoformat() + "Z",
92-
},
93-
}
87+
return create_success_response(
88+
text=ai_response["text"],
89+
session_id=ai_response["session_id"],
90+
citations=ai_response["citations"],
91+
)
9492
except Exception as e:
9593
logger.error(f"Error in direct invocation: {e}")
96-
return {
97-
"statusCode": 500,
98-
"response": {"error": "Internal server error", "timestamp": datetime.utcnow().isoformat() + "Z"},
99-
}
94+
return create_error_response(500, "Internal server error")

packages/slackBotFunction/app/services/ai_processor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
reformulation and bedrock interaction. single source of truth for AI flows.
66
"""
77

8-
from typing import Dict, Any, Optional
98
from app.services.bedrock import query_bedrock
109
from app.services.query_reformulator import reformulate_query
1110
from app.core.config import get_logger
11+
from app.core.types import AIProcessorResponse
1212

1313
logger = get_logger()
1414

1515

16-
def process_ai_query(user_query: str, session_id: Optional[str] = None) -> Dict[str, Any]:
16+
def process_ai_query(user_query: str, session_id: str | None = None) -> AIProcessorResponse:
1717
"""shared AI processing logic for both slack and direct invocation"""
1818
# reformulate: improves vector search quality in knowledge base
1919
reformulated_query = reformulate_query(user_query)

packages/slackBotFunction/tests/test_direct_invocation.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
from unittest.mock import Mock, patch
44
from app.handler import handle_direct_invocation
5+
from app.core.types import (
6+
DirectInvocationRequest,
7+
DirectInvocationResponse,
8+
)
59

610

711
class TestDirectInvocation:
@@ -16,9 +20,9 @@ def test_successful_direct_invocation_without_session(self, mock_process_ai_quer
1620
"kb_response": {"sessionId": "new-session-123"},
1721
}
1822

19-
event = {"invocation_type": "direct", "query": "How do I authenticate with EPS API?"}
23+
event: DirectInvocationRequest = {"invocation_type": "direct", "query": "How do I authenticate with EPS API?"}
2024

21-
result = handle_direct_invocation(event, Mock())
25+
result: DirectInvocationResponse = handle_direct_invocation(event, Mock())
2226

2327
assert result["statusCode"] == 200
2428
assert result["response"]["text"] == "AI response about EPS API authentication"
@@ -40,7 +44,7 @@ def test_successful_direct_invocation_with_session(self, mock_process_ai_query):
4044

4145
event = {"invocation_type": "direct", "query": "What about rate limits?", "session_id": "existing-session-456"}
4246

43-
result = handle_direct_invocation(event, Mock())
47+
result: DirectInvocationResponse = handle_direct_invocation(event, Mock())
4448

4549
assert result["statusCode"] == 200
4650
assert result["response"]["text"] == "Follow-up response"
@@ -149,9 +153,9 @@ def test_direct_invocation_timestamp_format(self, mock_process_ai_query):
149153

150154
timestamp = result["response"]["timestamp"]
151155
# iso8601 validation: parseable datetime with utc marker
152-
assert timestamp.endswith("Z")
156+
assert timestamp.endswith("+00:00")
153157
assert "T" in timestamp
154158
# format verification: datetime parsing confirms structure
155159
from datetime import datetime
156160

157-
datetime.fromisoformat(timestamp.rstrip("Z"))
161+
datetime.fromisoformat(timestamp)

0 commit comments

Comments
 (0)