cevheri commited on
Commit
87dbdac
·
1 Parent(s): f031ca8

feat: conversation/completion_id api implementation completed

Browse files
app/api/chat_api.py CHANGED
@@ -145,7 +145,7 @@ async def retrieve_plot(
145
 
146
 
147
  # get all conversations
148
- @router.get("/conversations", response_model=ConversationResponse, response_model_exclude_none=True)
149
  async def list_conversations(
150
  request: Request,
151
  username: str = Depends(auth_service.verify_credentials),
@@ -155,7 +155,7 @@ async def list_conversations(
155
  """
156
  logger.debug(f"Listing conversations for username: {username}")
157
  try:
158
- return await service.find_all_conversations(username)
159
  except Exception as e:
160
  logger.error(f"Error in list_conversations: {str(e)}")
161
  raise HTTPException(status_code=500, detail=str(e))
 
145
 
146
 
147
  # get all conversations
148
+ @router.get("/conversations", response_model=ConversationResponse, response_model_exclude_none=True)
149
  async def list_conversations(
150
  request: Request,
151
  username: str = Depends(auth_service.verify_credentials),
 
155
  """
156
  logger.debug(f"Listing conversations for username: {username}")
157
  try:
158
+ return await service.find_all_conversations(username)
159
  except Exception as e:
160
  logger.error(f"Error in list_conversations: {str(e)}")
161
  raise HTTPException(status_code=500, detail=str(e))
app/config/security_config.py CHANGED
@@ -16,6 +16,7 @@ class SecurityConfig(BaseSettings):
16
  ENABLED: bool = True
17
  DEFAULT_USERNAME: str = "admin"
18
 
 
19
  @lru_cache()
20
  def get_security_config() -> SecurityConfig:
21
- return SecurityConfig()
 
16
  ENABLED: bool = True
17
  DEFAULT_USERNAME: str = "admin"
18
 
19
+
20
  @lru_cache()
21
  def get_security_config() -> SecurityConfig:
22
+ return SecurityConfig()
app/core/api_response.py CHANGED
@@ -16,6 +16,7 @@ MOCK_DIR = env.str("MOCK_DIR", "resources/mock")
16
  # Deprecated code. because we are using mongomock-motor for database_type=embedded
17
  # TODO: remove this code after we switch to real database
18
 
 
19
  def url_to_filename(url: str, method: str) -> str:
20
  """
21
  Convert API URL to mock filename.
 
16
  # Deprecated code. because we are using mongomock-motor for database_type=embedded
17
  # TODO: remove this code after we switch to real database
18
 
19
+
20
  def url_to_filename(url: str, method: str) -> str:
21
  """
22
  Convert API URL to mock filename.
app/core/initial_setup/setup.py CHANGED
@@ -37,7 +37,7 @@ class InitialSetup:
37
  if db_config.DATABASE_TYPE != "embedded":
38
  logger.info("Skipping initial setup as database type is not embedded")
39
  return
40
-
41
  # if MONGO_URI is not set, it means we are using embedded database
42
  # last check is for the case of using mongomock-motor for database_type=embedded
43
  # so last exit before the bridge :) turkish joke
@@ -46,24 +46,24 @@ class InitialSetup:
46
  logger.warning("Deleting all chat completions in the embedded database")
47
  await self.chat_repository.db.chat_completion.delete_many({})
48
  logger.warning("Deleting all chat completions in the embedded database done")
49
-
50
  chat_completions = self._load_initial_data()
51
  logger.info(f"Loaded {len(chat_completions)} initial chat completions")
52
-
53
  for completion in chat_completions:
54
  try:
55
  found_id = await self.chat_repository.find_by_id(completion.completion_id)
56
  if found_id:
57
  logger.debug(f"Chat completion already exists: {found_id}")
58
  continue
59
-
60
  saved = await self.chat_repository.save(completion)
61
  logger.info(f"Successfully saved chat completion: {saved.completion_id}")
62
-
63
  except Exception as e:
64
  logger.error(f"Error saving chat completion {completion.completion_id}: {e}")
65
  raise
66
-
67
  except Exception as e:
68
  logger.error(f"Setup failed: {e}")
69
  raise
 
37
  if db_config.DATABASE_TYPE != "embedded":
38
  logger.info("Skipping initial setup as database type is not embedded")
39
  return
40
+
41
  # if MONGO_URI is not set, it means we are using embedded database
42
  # last check is for the case of using mongomock-motor for database_type=embedded
43
  # so last exit before the bridge :) turkish joke
 
46
  logger.warning("Deleting all chat completions in the embedded database")
47
  await self.chat_repository.db.chat_completion.delete_many({})
48
  logger.warning("Deleting all chat completions in the embedded database done")
49
+
50
  chat_completions = self._load_initial_data()
51
  logger.info(f"Loaded {len(chat_completions)} initial chat completions")
52
+
53
  for completion in chat_completions:
54
  try:
55
  found_id = await self.chat_repository.find_by_id(completion.completion_id)
56
  if found_id:
57
  logger.debug(f"Chat completion already exists: {found_id}")
58
  continue
59
+
60
  saved = await self.chat_repository.save(completion)
61
  logger.info(f"Successfully saved chat completion: {saved.completion_id}")
62
+
63
  except Exception as e:
64
  logger.error(f"Error saving chat completion {completion.completion_id}: {e}")
65
  raise
66
+
67
  except Exception as e:
68
  logger.error(f"Setup failed: {e}")
69
  raise
app/mapper/base_mapper.py CHANGED
@@ -1,26 +1,27 @@
1
  from abc import ABC, abstractmethod
2
  from typing import TypeVar, Generic, List
3
 
4
- T = TypeVar('T')
5
- U = TypeVar('U')
 
6
 
7
  class BaseMapper(Generic[T, U], ABC):
8
  """Base mapper class for mapping between model and schema objects."""
9
-
10
  @abstractmethod
11
  def to_schema(self, model: T) -> U:
12
  """Map from model to schema."""
13
  pass
14
-
15
  @abstractmethod
16
  def to_model(self, schema: U) -> T:
17
  """Map from schema to model."""
18
  pass
19
-
20
  def to_schema_list(self, models: List[T]) -> List[U]:
21
  """Map a list of models to schemas."""
22
  return [self.to_schema(model) for model in models]
23
-
24
  def to_model_list(self, schemas: List[U]) -> List[T]:
25
  """Map a list of schemas to models."""
26
- return [self.to_model(schema) for schema in schemas]
 
1
  from abc import ABC, abstractmethod
2
  from typing import TypeVar, Generic, List
3
 
4
+ T = TypeVar("T")
5
+ U = TypeVar("U")
6
+
7
 
8
  class BaseMapper(Generic[T, U], ABC):
9
  """Base mapper class for mapping between model and schema objects."""
10
+
11
  @abstractmethod
12
  def to_schema(self, model: T) -> U:
13
  """Map from model to schema."""
14
  pass
15
+
16
  @abstractmethod
17
  def to_model(self, schema: U) -> T:
18
  """Map from schema to model."""
19
  pass
20
+
21
  def to_schema_list(self, models: List[T]) -> List[U]:
22
  """Map a list of models to schemas."""
23
  return [self.to_schema(model) for model in models]
24
+
25
  def to_model_list(self, schemas: List[U]) -> List[T]:
26
  """Map a list of schemas to models."""
27
+ return [self.to_model(schema) for schema in schemas]
app/mapper/conversation_mapper.py CHANGED
@@ -10,9 +10,6 @@ class ConversationMapper(BaseMapper[ChatCompletion, ConversationItemResponse]):
10
 
11
  def to_schema(self, model: ChatCompletion) -> ConversationItemResponse:
12
  """Convert ChatCompletion model to ConversationItem schema."""
13
- # Convert datetime to Unix timestamp
14
- created_timestamp = int(model.created_date.timestamp()) if model.created_date else None
15
- last_updated_timestamp = int(model.last_updated_date.timestamp()) if model.last_updated_date else None
16
 
17
  # Get the first message content as title if title is not set
18
  title = model.title
@@ -23,8 +20,8 @@ class ConversationMapper(BaseMapper[ChatCompletion, ConversationItemResponse]):
23
  return ConversationItemResponse(
24
  completion_id=model.completion_id,
25
  title=title,
26
- create_time=created_timestamp,
27
- update_time=last_updated_timestamp,
28
  is_archived=model.is_archived,
29
  is_starred=model.is_starred,
30
  )
 
10
 
11
  def to_schema(self, model: ChatCompletion) -> ConversationItemResponse:
12
  """Convert ChatCompletion model to ConversationItem schema."""
 
 
 
13
 
14
  # Get the first message content as title if title is not set
15
  title = model.title
 
20
  return ConversationItemResponse(
21
  completion_id=model.completion_id,
22
  title=title,
23
+ create_time=model.created_date,
24
+ update_time=model.last_updated_date,
25
  is_archived=model.is_archived,
26
  is_starred=model.is_starred,
27
  )
app/model/chat_model.py CHANGED
@@ -84,12 +84,13 @@ class ChatCompletion(BaseModel):
84
  """
85
  A chat completion.
86
  """
 
87
  id: Optional[ObjectId] = Field(alias="_id", default_factory=ObjectId, description="MongoDB unique identifier")
88
  completion_id: Optional[str] = Field(None, description="The unique identifier for the chat completion")
89
 
90
  # openai compatible fields
91
  model: Optional[str] = Field(None, description="The model used for the chat completion", examples=["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"])
92
- messages: List[ChatMessage] = Field(..., description="The messages in the chat completion")
93
 
94
  # not implemented yet
95
  # temperature: float = Field(default=0.7,ge=0.0, le=1.0, description="What sampling temperature to use, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.")
@@ -121,14 +122,10 @@ class ChatCompletion(BaseModel):
121
  description="The date and time the chat completion was last updated",
122
  )
123
 
124
-
125
- class Config():
126
  populate_by_name = True
127
  arbitrary_types_allowed = True
128
- json_encoders = {
129
- ObjectId: lambda o: str(o),
130
- datetime: lambda o: o.isoformat()
131
- }
132
 
133
  def __str__(self):
134
  return f"""
 
84
  """
85
  A chat completion.
86
  """
87
+
88
  id: Optional[ObjectId] = Field(alias="_id", default_factory=ObjectId, description="MongoDB unique identifier")
89
  completion_id: Optional[str] = Field(None, description="The unique identifier for the chat completion")
90
 
91
  # openai compatible fields
92
  model: Optional[str] = Field(None, description="The model used for the chat completion", examples=["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"])
93
+ messages: Optional[List[ChatMessage]] = Field(None, description="The messages in the chat completion")
94
 
95
  # not implemented yet
96
  # temperature: float = Field(default=0.7,ge=0.0, le=1.0, description="What sampling temperature to use, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.")
 
122
  description="The date and time the chat completion was last updated",
123
  )
124
 
125
+ class Config:
 
126
  populate_by_name = True
127
  arbitrary_types_allowed = True
128
+ json_encoders = {ObjectId: lambda o: str(o), datetime: lambda o: o.isoformat()}
 
 
 
129
 
130
  def __str__(self):
131
  return f"""
app/repository/chat_repository.py CHANGED
@@ -9,10 +9,13 @@ import pymongo
9
 
10
  class DocumentNotFoundError(Exception):
11
  """Raised when a document is not found in the database."""
 
12
  pass
13
 
 
14
  # TODO: llm_model, llm_provider will come from .env file
15
 
 
16
  class ChatRepository:
17
  def __init__(self):
18
  logger.info("Initializing ChatRepository")
@@ -33,13 +36,13 @@ class ChatRepository:
33
  Exception: If the chat completion is not created
34
  """
35
  logger.info(f"Creating new chat completion for user: {entity.created_by}")
36
-
37
  entity.completion_id = str(uuid.uuid4()) if entity.completion_id is None else entity.completion_id
38
- entity_dict = entity.model_dump(by_alias=True)
39
 
40
  # MongoDB'ye kaydet
41
  insert_result = await self.db.chat_completion.insert_one(entity_dict)
42
-
43
  if not insert_result.inserted_id:
44
  logger.error(f"Failed to create new chat completion with ID: {entity.completion_id}")
45
  raise Exception(f"Failed to create chat completion with ID: {entity.completion_id}")
@@ -50,13 +53,13 @@ class ChatRepository:
50
  async def _update(self, entity: ChatCompletion) -> ChatCompletion:
51
  """
52
  Update an existing chat completion in the database.
53
-
54
  Args:
55
  entity (ChatCompletion): The chat completion entity to update
56
-
57
  Returns:
58
  ChatCompletion: The updated chat completion
59
-
60
  Raises:
61
  ValueError: If completion_id is not provided
62
  DocumentNotFoundError: If the document to update is not found
@@ -65,37 +68,34 @@ class ChatRepository:
65
  raise ValueError("Cannot update chat completion without completion_id")
66
 
67
  logger.info(f"Updating chat completion with ID: {entity.completion_id}")
68
-
69
  # these fields are not updatable
70
  non_updatable_fields = {"created_date", "created_by", "completion_id"}
71
-
72
  # get the model data and remove the non-updatable fields
73
- update_payload = {
74
- k: v for k, v in entity.model_dump(by_alias=True).items()
75
- if k not in non_updatable_fields
76
- }
77
-
78
  if not update_payload:
79
  logger.warning(f"No updatable fields found for chat completion ID: {entity.completion_id}")
80
  return await self.find_by_id(entity.completion_id)
81
-
82
  query = {"completion_id": entity.completion_id}
83
  update = {"$set": update_payload}
84
-
85
  try:
86
  result = await self.db.chat_completion.update_one(query, update)
87
-
88
  if result.matched_count == 0:
89
  logger.error(f"Chat completion with ID {entity.completion_id} not found for update")
90
  raise DocumentNotFoundError(f"Chat completion with ID {entity.completion_id} not found")
91
-
92
  if result.modified_count == 0:
93
  logger.info(f"Chat completion with ID {entity.completion_id} matched but not modified")
94
  else:
95
  logger.info(f"Successfully updated chat completion with ID: {entity.completion_id}")
96
-
97
  return await self.find_by_id(entity.completion_id)
98
-
99
  except Exception as e:
100
  logger.error(f"Error updating chat completion with ID {entity.completion_id}: {str(e)}")
101
  raise
@@ -106,9 +106,8 @@ class ChatRepository:
106
  it will be updated. Otherwise, a new chat completion will be created.
107
  """
108
  logger.debug(f"BEGIN REPO: save chat completion. username: {entity.created_by}, completion_id: {entity.completion_id}")
109
-
110
- try:
111
 
 
112
  result = await self.find_by_id(entity.completion_id)
113
  if result:
114
  return await self._update(entity)
@@ -156,8 +155,8 @@ class ChatRepository:
156
  Example : completion_id = "123"
157
  """
158
  logger.debug(f"BEGIN REPO: find chat completion by id. input parameters: completion_id: {completion_id}, projection: {projection}")
159
-
160
- entity_doc = await self.db.chat_completion.find_one({"completion_id": completion_id}, projection)
161
 
162
  if entity_doc:
163
  logger.trace(f"REPO find_by_id. Found entity_doc: {entity_doc}")
 
9
 
10
  class DocumentNotFoundError(Exception):
11
  """Raised when a document is not found in the database."""
12
+
13
  pass
14
 
15
+
16
  # TODO: llm_model, llm_provider will come from .env file
17
 
18
+
19
  class ChatRepository:
20
  def __init__(self):
21
  logger.info("Initializing ChatRepository")
 
36
  Exception: If the chat completion is not created
37
  """
38
  logger.info(f"Creating new chat completion for user: {entity.created_by}")
39
+
40
  entity.completion_id = str(uuid.uuid4()) if entity.completion_id is None else entity.completion_id
41
+ entity_dict = entity.model_dump(by_alias=True)
42
 
43
  # MongoDB'ye kaydet
44
  insert_result = await self.db.chat_completion.insert_one(entity_dict)
45
+
46
  if not insert_result.inserted_id:
47
  logger.error(f"Failed to create new chat completion with ID: {entity.completion_id}")
48
  raise Exception(f"Failed to create chat completion with ID: {entity.completion_id}")
 
53
  async def _update(self, entity: ChatCompletion) -> ChatCompletion:
54
  """
55
  Update an existing chat completion in the database.
56
+
57
  Args:
58
  entity (ChatCompletion): The chat completion entity to update
59
+
60
  Returns:
61
  ChatCompletion: The updated chat completion
62
+
63
  Raises:
64
  ValueError: If completion_id is not provided
65
  DocumentNotFoundError: If the document to update is not found
 
68
  raise ValueError("Cannot update chat completion without completion_id")
69
 
70
  logger.info(f"Updating chat completion with ID: {entity.completion_id}")
71
+
72
  # these fields are not updatable
73
  non_updatable_fields = {"created_date", "created_by", "completion_id"}
74
+
75
  # get the model data and remove the non-updatable fields
76
+ update_payload = {k: v for k, v in entity.model_dump(by_alias=True).items() if k not in non_updatable_fields}
77
+
 
 
 
78
  if not update_payload:
79
  logger.warning(f"No updatable fields found for chat completion ID: {entity.completion_id}")
80
  return await self.find_by_id(entity.completion_id)
81
+
82
  query = {"completion_id": entity.completion_id}
83
  update = {"$set": update_payload}
84
+
85
  try:
86
  result = await self.db.chat_completion.update_one(query, update)
87
+
88
  if result.matched_count == 0:
89
  logger.error(f"Chat completion with ID {entity.completion_id} not found for update")
90
  raise DocumentNotFoundError(f"Chat completion with ID {entity.completion_id} not found")
91
+
92
  if result.modified_count == 0:
93
  logger.info(f"Chat completion with ID {entity.completion_id} matched but not modified")
94
  else:
95
  logger.info(f"Successfully updated chat completion with ID: {entity.completion_id}")
96
+
97
  return await self.find_by_id(entity.completion_id)
98
+
99
  except Exception as e:
100
  logger.error(f"Error updating chat completion with ID {entity.completion_id}: {str(e)}")
101
  raise
 
106
  it will be updated. Otherwise, a new chat completion will be created.
107
  """
108
  logger.debug(f"BEGIN REPO: save chat completion. username: {entity.created_by}, completion_id: {entity.completion_id}")
 
 
109
 
110
+ try:
111
  result = await self.find_by_id(entity.completion_id)
112
  if result:
113
  return await self._update(entity)
 
155
  Example : completion_id = "123"
156
  """
157
  logger.debug(f"BEGIN REPO: find chat completion by id. input parameters: completion_id: {completion_id}, projection: {projection}")
158
+ query = {"completion_id": completion_id}
159
+ entity_doc = await self.db.chat_completion.find_one(query, projection)
160
 
161
  if entity_doc:
162
  logger.trace(f"REPO find_by_id. Found entity_doc: {entity_doc}")
app/security/auth_service.py CHANGED
@@ -12,12 +12,12 @@ api_key_header = APIKeyHeader(
12
  name="Authorization",
13
  scheme_name="ApiKeyAuth",
14
  description="API key in the format: sk-{username}-{base64_encoded_data}",
15
- auto_error=False # API key olmadığında otomatik hata vermesini engelle
16
  )
17
 
18
 
19
  class AuthService:
20
- def __init__(self):
21
  self.api_key_header = api_key_header
22
  self.security_config = get_security_config()
23
 
@@ -87,20 +87,20 @@ class AuthService:
87
  detail=f"Invalid API key: {str(e)}",
88
  )
89
 
90
- async def verify_credentials(self, api_key: str = Security(api_key_header)) -> str:
91
  """Verify API key and extract username."""
92
  logger.trace(f"BEGIN: api_key: {api_key}")
93
-
94
  if not self.security_config.ENABLED:
95
  logger.info("Security is disabled, using default username: " + self.security_config.DEFAULT_USERNAME)
96
  return self.security_config.DEFAULT_USERNAME
97
-
98
  if not api_key:
99
  raise HTTPException(
100
  status_code=status.HTTP_401_UNAUTHORIZED,
101
  detail="API key is required when security is enabled",
102
  )
103
-
104
  username = self.decode_api_key(api_key)
105
  result = username
106
  logger.trace(f"END: result: {result}")
 
12
  name="Authorization",
13
  scheme_name="ApiKeyAuth",
14
  description="API key in the format: sk-{username}-{base64_encoded_data}",
15
+ auto_error=False, # API key olmadığında otomatik hata vermesini engelle
16
  )
17
 
18
 
19
  class AuthService:
20
+ def __init__(self):
21
  self.api_key_header = api_key_header
22
  self.security_config = get_security_config()
23
 
 
87
  detail=f"Invalid API key: {str(e)}",
88
  )
89
 
90
+ async def verify_credentials(self, api_key: str = Security(api_key_header)) -> str:
91
  """Verify API key and extract username."""
92
  logger.trace(f"BEGIN: api_key: {api_key}")
93
+
94
  if not self.security_config.ENABLED:
95
  logger.info("Security is disabled, using default username: " + self.security_config.DEFAULT_USERNAME)
96
  return self.security_config.DEFAULT_USERNAME
97
+
98
  if not api_key:
99
  raise HTTPException(
100
  status_code=status.HTTP_401_UNAUTHORIZED,
101
  detail="API key is required when security is enabled",
102
  )
103
+
104
  username = self.decode_api_key(api_key)
105
  result = username
106
  logger.trace(f"END: result: {result}")
app/service/chat_service.py CHANGED
@@ -1,13 +1,8 @@
1
  import datetime
2
  from typing import List
3
  from app.repository.chat_repository import ChatRepository
4
- from app.schema.chat_schema import (
5
- ChatCompletionRequest,
6
- ChatCompletionResponse,
7
- ChoiceResponse,
8
- MessageResponse,
9
- )
10
- from app.model.chat_model import ChatCompletion, ChatMessage
11
  from app.mapper.chat_mapper import ChatMapper
12
  from app.mapper.conversation_mapper import ConversationMapper
13
  import uuid
@@ -28,7 +23,7 @@ class ChatService:
28
 
29
  # Convert request to model
30
  entity = self.chat_mapper.to_model(request)
31
-
32
  if entity.completion_id:
33
  entity.completion_id = str(uuid.uuid4())
34
  entity.created_by = username
@@ -65,10 +60,16 @@ class ChatService:
65
  result = self.conversation_mapper.to_schema_list(entities)
66
  return ConversationResponse(items=result, total=len(result), limit=100, offset=0)
67
 
68
-
69
-
70
  async def find_conversation_by_id(self, completion_id: str) -> ConversationResponse:
71
  """Find a conversation by its completion ID."""
72
- entity = await self.chat_repository.find_by_id(completion_id)
73
- result = self.conversation_mapper.to_schema(entity) if entity else None
74
- return ConversationResponse(items=[result], total=1, limit=1, offset=0)
 
 
 
 
 
 
 
 
 
1
  import datetime
2
  from typing import List
3
  from app.repository.chat_repository import ChatRepository
4
+ from app.schema.chat_schema import ChatCompletionRequest, ChatCompletionResponse
5
+ from app.model.chat_model import ChatMessage
 
 
 
 
 
6
  from app.mapper.chat_mapper import ChatMapper
7
  from app.mapper.conversation_mapper import ConversationMapper
8
  import uuid
 
23
 
24
  # Convert request to model
25
  entity = self.chat_mapper.to_model(request)
26
+
27
  if entity.completion_id:
28
  entity.completion_id = str(uuid.uuid4())
29
  entity.created_by = username
 
60
  result = self.conversation_mapper.to_schema_list(entities)
61
  return ConversationResponse(items=result, total=len(result), limit=100, offset=0)
62
 
 
 
63
  async def find_conversation_by_id(self, completion_id: str) -> ConversationResponse:
64
  """Find a conversation by its completion ID."""
65
+ logger.debug(f"BEGIN SERVICE: find_conversation_by_id for completion_id: {completion_id}")
66
+ projection = {"messages": 0, "_id": 0}
67
+ entity = await self.chat_repository.find_by_id(completion_id, projection=projection)
68
+ logger.debug(f"END SERVICE: find_conversation_by_id for completion_id: {completion_id}, entity: {entity}")
69
+
70
+ if entity:
71
+ # Tekil kayıt için doğrudan dönüşüm yapıyoruz
72
+ result = self.conversation_mapper.to_schema(entity)
73
+ return result
74
+ else:
75
+ return None