Skip to content

Commit afd03f4

Browse files
committed
feat: add course code verification on registration
1 parent 18359da commit afd03f4

6 files changed

Lines changed: 101 additions & 106 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
1212
[![hack.d Lawrence McDaniel](https://img.shields.io/badge/hack.d-Lawrence%20McDaniel-orange.svg)](https://lawrencemcdaniel.com)
1313

14-
A Python command-line application that demonstrates how to use OpenAI Api function calling to drive an automated agentic workflow. Also demonstrates how
15-
to use Docker Compose to containerize your project.
14+
A Python command-line application that demonstrates how to use OpenAI Api function calling to drive an automated agentic workflow. Additionally, this repo demonstrates
15+
16+
- how to use Docker Compose to containerize your project
17+
- how to leverage Pydantic for constructing complex JSON objects
1618

1719
Python code is [located here](./app/)
1820

app/function_schemas.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

app/prompt.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from openai.types.chat import (
1212
ChatCompletion,
1313
ChatCompletionAssistantMessageParam,
14-
ChatCompletionFunctionToolParam,
1514
ChatCompletionMessage,
1615
ChatCompletionMessageFunctionToolCallParam,
1716
ChatCompletionSystemMessageParam,
@@ -90,7 +89,6 @@ def process_tool_calls(message: ChatCompletionMessage) -> list[str]:
9089
return functions_called
9190
for tool_call in message.tool_calls:
9291

93-
# For function calls, access via type checking
9492
if tool_call.type == "function":
9593
function_name = tool_call.function.name
9694
function_args = json.loads(tool_call.function.arguments)
@@ -101,7 +99,7 @@ def process_tool_calls(message: ChatCompletionMessage) -> list[str]:
10199
type="function",
102100
function={
103101
"name": function_name,
104-
"arguments": tool_call.function.arguments, # Keep as string, don't parse
102+
"arguments": tool_call.function.arguments,
105103
},
106104
)
107105
]
@@ -113,10 +111,8 @@ def process_tool_calls(message: ChatCompletionMessage) -> list[str]:
113111
)
114112
logger.info("Function call detected: %s with args %s", function_name, function_args)
115113

116-
# Execute the function
117114
function_result = handle_function_call(function_name, function_args)
118115

119-
# Add the function result to the conversation
120116
tool_message = ChatCompletionToolMessageParam(
121117
role="tool", content=function_result, tool_call_id=tool_call.id
122118
)
@@ -132,20 +128,13 @@ def process_tool_calls(message: ChatCompletionMessage) -> list[str]:
132128
def completion(prompt: str) -> tuple[ChatCompletion, list[str]]:
133129
"""LLM text completion"""
134130

135-
# Set the OpenAI API key
136-
# -------------------------------------------------------------------------
137131
openai.api_key = settings.OPENAI_API_KEY
138-
139-
# setup our text completion prompt
140-
# -------------------------------------------------------------------------
141132
model = settings.OPENAI_API_MODEL
142133
temperature = settings.OPENAI_API_TEMPERATURE
143134
max_tokens = settings.OPENAI_API_MAX_TOKENS
144135
messages.append(ChatCompletionUserMessageParam(role="user", content=prompt))
145136
functions_called = []
146137

147-
# Call the OpenAI API
148-
# -------------------------------------------------------------------------
149138
response = openai.chat.completions.create(
150139
model=model,
151140
messages=messages,
@@ -156,14 +145,10 @@ def completion(prompt: str) -> tuple[ChatCompletion, list[str]]:
156145
)
157146
logger.debug("Initial response: %s", response.model_dump())
158147

159-
# Check if the model wants to call a function
160-
# -------------------------------------------------------------------------
161148
message = response.choices[0].message
162-
163149
while message.tool_calls:
164150
functions_called = process_tool_calls(message)
165151

166-
# Make another API call to get the final response
167152
response = openai.chat.completions.create(
168153
model=model,
169154
messages=messages,

app/response_models.py

Lines changed: 0 additions & 32 deletions
This file was deleted.

app/stackademy.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,53 @@
11
# -*- coding: utf-8 -*-
22
"""Stackademy application with MySQL database integration."""
33

4+
from enum import Enum
45
from typing import Any, Dict, List, Optional
56

67
from openai.types.chat import ChatCompletionFunctionToolParam
8+
from pydantic import BaseModel, Field
79

810
from app.database import db
911
from app.exceptions import ConfigurationException
10-
from app.function_schemas import GetCoursesParams, RegisterCourseParams
1112
from app.logging_config import get_logger, setup_logging
1213

1314

15+
class StackademySpecializationArea(str, Enum):
16+
"""Available specialization areas for courses."""
17+
18+
AI = "AI"
19+
MOBILE = "mobile"
20+
WEB = "web"
21+
DATABASE = "database"
22+
NETWORK = "network"
23+
NEURAL_NETWORKS = "neural networks"
24+
25+
26+
class StackademyGetCoursesParams(BaseModel):
27+
"""Parameters for the get_courses function."""
28+
29+
max_cost: Optional[float] = Field(
30+
None, description="The maximum cost that a student is willing to pay for a course."
31+
)
32+
description: Optional[StackademySpecializationArea] = Field(
33+
None, description="Areas of specialization for courses in the catalogue."
34+
)
35+
36+
37+
class StackademyRegisterCourseParams(BaseModel):
38+
"""Parameters for the register_course function."""
39+
40+
course_code: str = Field(description="The unique code for the course.")
41+
email: str = Field(description="The email address of the new user.")
42+
full_name: str = Field(description="The full name of the new user.")
43+
44+
1445
# Initialize logging
1546
setup_logging()
1647
logger = get_logger(__name__)
1748

1849

19-
class StackademyApp:
50+
class Stackademy:
2051
"""Main application class for Stackademy with database functionality."""
2152

2253
def __init__(self):
@@ -30,7 +61,7 @@ def tool_factory_get_courses(self) -> ChatCompletionFunctionToolParam:
3061
function={
3162
"name": "get_courses",
3263
"description": "returns up to 10 rows of course detail data, filtered by the maximum cost a student is willing to pay for a course and the area of specialization.",
33-
"parameters": GetCoursesParams.model_json_schema(),
64+
"parameters": StackademyGetCoursesParams.model_json_schema(),
3465
},
3566
)
3667

@@ -41,7 +72,7 @@ def tool_factory_register(self) -> ChatCompletionFunctionToolParam:
4172
function={
4273
"name": "register_course",
4374
"description": "Register a student in a course with the provided details.",
44-
"parameters": RegisterCourseParams.model_json_schema(),
75+
"parameters": StackademyRegisterCourseParams.model_json_schema(),
4576
},
4677
)
4778

@@ -108,6 +139,24 @@ def get_courses(self, description: Optional[str] = None, max_cost: Optional[floa
108139
logger.error("Failed to retrieve courses: %s", e)
109140
return []
110141

142+
def verify_course(self, course_code: str) -> bool:
143+
"""
144+
Verify if a course exists in the database.
145+
Args:
146+
course_code (str): The course code to verify
147+
Returns:
148+
bool: True if the course exists, False otherwise
149+
"""
150+
query = "SELECT * FROM courses WHERE course_code = %s"
151+
logger.info("verify_course() course_code: %s", course_code)
152+
try:
153+
result = self.db.execute_query(query, (course_code,))
154+
return len(result) > 0
155+
# pylint: disable=broad-except
156+
except Exception as e:
157+
logger.error("Failed to retrieve courses: %s", e)
158+
return False
159+
111160
def register_course(self, course_code: str, email: str, full_name: str) -> bool:
112161
"""
113162
Register a user for a course.
@@ -121,6 +170,9 @@ def register_course(self, course_code: str, email: str, full_name: str) -> bool:
121170
bool: True if registration is successful, False otherwise
122171
"""
123172
logger.info("Registering %s (%s) for course %s...", full_name, email, course_code)
173+
if not self.verify_course(course_code):
174+
logger.error("Course code %s does not exist.", course_code)
175+
return False
124176
return True
125177

126178

@@ -131,7 +183,7 @@ def main():
131183

132184
try:
133185
# Initialize the application
134-
app = StackademyApp()
186+
app = Stackademy()
135187

136188
# Test database connection
137189
logger.info("Testing database connection...")
@@ -159,7 +211,7 @@ def main():
159211
logger.error("Application error: %s", e)
160212

161213

162-
stackademy_app = StackademyApp()
214+
stackademy_app = Stackademy()
163215

164216
if __name__ == "__main__":
165217
main()

app/structured_outputs.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
11
# -*- coding: utf-8 -*-
2-
"""Example of using OpenAI's structured outputs with Pydantic models."""
2+
"""EXPERIMENTAL: OpenAI's structured outputs with Pydantic models."""
33

44
import json
5-
from typing import Optional
5+
from typing import List, Optional
66

77
import openai
8-
from pydantic import ValidationError
8+
from pydantic import BaseModel, Field, ValidationError
99

1010
from app import settings
11-
from app.function_schemas import (
12-
GetCoursesParams,
13-
RegisterCourseParams,
14-
SpecializationArea,
15-
)
1611
from app.logging_config import get_logger
17-
from app.response_models import Course, CourseSearchResponse, RegistrationResponse
18-
from app.stackademy import stackademy_app
12+
from app.stackademy import (
13+
StackademyGetCoursesParams,
14+
StackademyRegisterCourseParams,
15+
StackademySpecializationArea,
16+
stackademy_app,
17+
)
18+
19+
20+
class Course(BaseModel):
21+
"""A course in the Stackademy catalog."""
22+
23+
course_code: str = Field(description="The unique code for the course")
24+
course_name: str = Field(description="The name of the course")
25+
description: str = Field(description="Course description")
26+
cost: float = Field(description="Cost of the course")
27+
prerequisite_course_code: Optional[str] = Field(description="Prerequisite course code", default=None)
28+
prerequisite_course_name: Optional[str] = Field(description="Prerequisite course name", default=None)
29+
30+
31+
class CourseSearchResponse(BaseModel):
32+
"""Response model for course search results."""
33+
34+
courses: List[Course] = Field(description="List of courses matching the search criteria")
35+
total_count: int = Field(description="Total number of courses found")
36+
37+
38+
class RegistrationResponse(BaseModel):
39+
"""Response model for course registration."""
40+
41+
success: bool = Field(description="Whether the registration was successful")
42+
message: str = Field(description="Human-readable message about the registration result")
43+
registration_id: Optional[str] = Field(description="Unique registration ID if successful", default=None)
1944

2045

2146
logger = get_logger(__name__)
@@ -35,13 +60,13 @@ def get_courses_with_structured_output(
3560
if description:
3661

3762
try:
38-
specialization_area = SpecializationArea(description)
63+
specialization_area = StackademySpecializationArea(description)
3964
except ValueError:
4065
logger.warning("Invalid specialization area: %s", description)
4166
specialization_area = None
4267

4368
# Validate input parameters using Pydantic
44-
params = GetCoursesParams(description=specialization_area, max_cost=max_cost)
69+
params = StackademyGetCoursesParams(description=specialization_area, max_cost=max_cost)
4570

4671
# Get raw course data
4772
courses_data = stackademy_app.get_courses(
@@ -68,7 +93,7 @@ def register_course_with_structured_output(course_code: str, email: str, full_na
6893
"""
6994
try:
7095
# Validate input parameters
71-
params = RegisterCourseParams(course_code=course_code, email=email, full_name=full_name)
96+
params = StackademyRegisterCourseParams(course_code=course_code, email=email, full_name=full_name)
7297

7398
# Attempt registration
7499
success = stackademy_app.register_course(

0 commit comments

Comments
 (0)