From 08c97274aa5a48f30320bd08d6b14eedadfa9781 Mon Sep 17 00:00:00 2001 From: Kavinda Maduranga Date: Fri, 13 Jun 2025 03:28:46 +0530 Subject: [PATCH 1/3] Add chat functionality with session management and message storage --- main.py | 217 +++++++++++++++++- src/config/__pycache__/config.cpython-312.pyc | Bin 4202 -> 4206 bytes src/config/config.py | 9 +- .../answer_generator.cpython-312.pyc | Bin 4063 -> 3919 bytes .../glossary_handler.cpython-312.pyc | Bin 3784 -> 8833 bytes .../knowledge_graph_handler.cpython-312.pyc | Bin 9517 -> 8930 bytes .../__pycache__/query_handler.cpython-312.pyc | Bin 5141 -> 5473 bytes .../__pycache__/mongo_service.cpython-312.pyc | Bin 1437 -> 1369 bytes src/services/storage_service.py | 11 +- 9 files changed, 228 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index f203752..1c7b85e 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ from flask import Flask, request, jsonify +from bson import ObjectId from io import BytesIO @@ -84,15 +85,73 @@ def get_answer(): return jsonify({"error": "Question field is required"}), 400 question = data['question'] + session_id = data.get('session_id') # Optional session_id from frontend + + # If no session_id provided, create a new one using MongoDB ObjectId + if not session_id: + session_id = str(ObjectId()) + logger.info(f"Created new session for query: {session_id}") try: + # Save user question to database + user_message = save_chat_message(session_id, 'user', question) + logger.info(f"Saved user question to session {session_id}") + + # Generate answer using the knowledge graph answer = answer_generator.generate_answer(question) - return jsonify({"answer": str(answer)}) + + # Save LLM response to database + assistant_message = save_chat_message(session_id, 'assistant', str(answer)) + logger.info(f"Saved LLM response to session {session_id}") + + return jsonify({ + "answer": str(answer), + "session_id": session_id # Return session_id to frontend + }) except Exception as e: + logger.error(f"Error in query endpoint: {e}") return jsonify({"error": str(e)}), 500 +# Enhanced chat endpoint that uses knowledge graph for responses +@app.route('/api/chat//query', methods=['POST']) +def chat_with_knowledge_graph(session_id): + """ + Send a question to a specific chat session and get an answer from the knowledge graph. + This endpoint combines chat functionality with knowledge graph querying. + """ + data = request.get_json() + + if 'question' not in data: + return jsonify({"error": "Question field is required"}), 400 + + question = data['question'] + + try: + # Save user question to database + user_message = save_chat_message(session_id, 'user', question) + + # Generate answer using the knowledge graph + answer = answer_generator.generate_answer(question) + + # Save LLM response to database + assistant_message = save_chat_message(session_id, 'assistant', str(answer)) + + logger.info(f"Processed chat query for session {session_id}") + + return jsonify({ + "answer": str(answer), + "session_id": session_id, + "user_message": serialize_message(user_message), + "assistant_message": serialize_message(assistant_message) + }), 200 + + except Exception as e: + logger.error(f"Error in chat query endpoint: {e}") + return jsonify({"error": str(e)}), 500 + + # New endpoint: add glossary items @app.route('/api/glossary/add', methods=['POST']) def add_glossary(): @@ -143,6 +202,162 @@ def delete_glossary(item_id): except Exception as e: logger.error(f"Error deleting glossary item: {e}") return jsonify({"error": str(e)}), 500 + + +# Chat functionality setup +chat_collection = storage_service.chat_collection +datetime = storage_service.datetime + +def save_chat_message(session_id, role, content): + """ + Utility function to save a chat message to the database. + + Args: + session_id (str): The chat session ID + role (str): 'user' or 'assistant' + content (str): The message content + + Returns: + dict: The saved message document + """ + message = { + 'session_id': session_id, + 'role': role, + 'content': content, + 'timestamp': datetime.utcnow() + } + result = chat_collection.insert_one(message) + message['_id'] = result.inserted_id + return message + +serialize_message = lambda msg: { + 'id': str(msg['_id']), + 'session_id': msg['session_id'], + 'role': msg['role'], + 'content': msg['content'], + 'timestamp': msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'] +} + +# Endpoint to list chat sessions +@app.route('/chat/sessions', methods=['GET']) +def list_chat_sessions(): + """ + List all chat sessions with their metadata. + This can be useful for the frontend to show a list of previous chats. + """ + try: + # Get unique session IDs and their latest message timestamps + pipeline = [ + { + '$group': { + '_id': '$session_id', + 'last_message_time': {'$max': '$timestamp'}, + 'message_count': {'$sum': 1} + } + }, + { + '$sort': {'last_message_time': -1} + }, + { + '$limit': 50 # Limit to last 50 sessions + } + ] + + sessions = list(chat_collection.aggregate(pipeline)) + + # Format the response + formatted_sessions = [] + for session in sessions: + formatted_sessions.append({ + 'session_id': session['_id'], + 'last_message_time': session['last_message_time'].isoformat() if isinstance(session['last_message_time'], datetime) else session['last_message_time'], + 'message_count': session['message_count'] + }) + + return jsonify({ + 'sessions': formatted_sessions, + 'total_count': len(formatted_sessions) + }), 200 + + except Exception as e: + logger.error(f"Error listing chat sessions: {e}") + return jsonify({'error': f'Error listing chat sessions: {str(e)}'}), 500 + + +# Endpoint to create a new chat session +@app.route('/chat/create', methods=['POST']) +def create_chat_session(): + """ + Create a new chat session and return the session ID. + The frontend can use this ID to start a new chat thread. + """ + try: + # Generate a unique session ID using MongoDB ObjectId + session_id = str(ObjectId()) + + # Optionally, you can create an initial session document in MongoDB + # to track session metadata (creation time, etc.) + session_metadata = { + 'session_id': session_id, + 'created_at': datetime.utcnow(), + 'status': 'active' + } + + # Store session metadata (optional) + # For now, we'll just return the session ID without storing metadata + # If you want to store session metadata, uncomment the next line: + # chat_collection.insert_one(session_metadata) + + logger.info(f"Created new chat session: {session_id}") + + return jsonify({ + 'session_id': session_id, + 'message': 'Chat session created successfully' + }), 201 + + except Exception as e: + logger.error(f"Error creating chat session: {e}") + return jsonify({'error': f'Error creating chat session: {str(e)}'}), 500 + +# Endpoint to add a message to a specific chat session +@app.route('/chat/', methods=['POST']) +def add_message(session_id): + data = request.get_json() + role = data.get('role') + content = data.get('content') + if not role or not content: + return jsonify({'error': 'role and content are required'}), 400 + + message = { + 'session_id': session_id, + 'role': role, + 'content': content, + 'timestamp': datetime.utcnow() + } + result = chat_collection.insert_one(message) + message['_id'] = result.inserted_id + return jsonify(serialize_message(message)), 201 + +# Endpoint to retrieve chat history for a specific session +@app.route('/chat/', methods=['GET']) +def get_history(session_id): + limit = int(request.args.get('limit', 100)) + skip = int(request.args.get('skip', 0)) + + cursor = chat_collection.find({'session_id': session_id})\ + .sort('timestamp', 1)\ + .skip(skip)\ + .limit(limit) + messages = [serialize_message(msg) for msg in cursor] + return jsonify(messages), 200 + +# Endpoint to delete chat history for a specific session +@app.route('/chat/', methods=['DELETE']) +def delete_history(session_id): + result = chat_collection.delete_many({'session_id': session_id}) + return jsonify({'deleted_count': result.deleted_count}), 200 + + if __name__ == "__main__": diff --git a/src/config/__pycache__/config.cpython-312.pyc b/src/config/__pycache__/config.cpython-312.pyc index e9c3d5016f0b206da590b8c2d5e6d246ae4a6f78..7d0eda0bac5de321ddf00132a8bc8da9886d7547 100644 GIT binary patch delta 266 zcmaE*@J@m6G%qg~0}!Nt63pln-pJ?1%8_honrLihVK6y{)k^o4wrOx=YI0#_XtGaK zl}SOBzooNFihHqTR!*pCk-v9npjk>viMN@HYmQ<5=EJPv90L46ZtgIZL`N9}kLl_}S zL8>%aiZp=KEw+^WlGMDiB4!{5qTLC^f*W~@!zMRBr8FniuE+%_0dxpMu^f>2z|6=v U*;{~5FpiNigz*ytm;!490J>H?Jpcdz delta 229 zcmaE-@JfO2G%qg~0}#x-@I76Zew-? zti@N&sIz%FpC+T7HBe?HLy--TDzX9*tT2Kd$SCFo5-kiLnOIplKFe9LhA=`@f)r`8 t6sb?<7Z8(j1nGbqaErqxH$SB`C)KXVd9uBLoS+vYV+i9X1~3Iy1^^{*HLd^v diff --git a/src/config/config.py b/src/config/config.py index a406f7b..49e802d 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -85,16 +85,15 @@ def __init__(self): # MongoDB self.mongo_uri = os.getenv("MONGO_URI", "mongodb+srv://kavindamadhuranga74:fLaa4T079luktEQv@cluster0.xkdqxqw.mongodb.net/?appName=Cluster0") # self.mongo_glossary_db = os.getenv("GlossaryDB", "your_ai_db") - # self.mongo_glossary_collection = os.getenv("GlossaryCollection", "pdf_store") - - - self.mongo_glossary_db = os.getenv("GlossaryDB", "glossary_database") + # self.mongo_glossary_collection = os.getenv("GlossaryCollection", "pdf_store") self.mongo_glossary_db = os.getenv("GlossaryDB", "glossary_database") self.mongo_glossary_collection = os.getenv("GlossaryCollection", "glossary_collection") - self.mongo_metadata_db = os.getenv("METADATA_DB", "your_ai_db") self.mongo_metadata_collection = os.getenv("METADATA_COLLECTION", "pdf_store") + # Chat configuration + self.mongo_chat_db = os.getenv("CHAT_DB", "chat_database") + self.mongo_chat_collection = os.getenv("CHAT_COLLECTION", "chat_sessions") # Azure self.azure_connection_string = os.getenv("AZURE_CONNECTION_STRING", "DefaultEndpointsProtocol=https;AccountName=researchpdfstore;AccountKey=SQnY5MvTblA+bEu7bPw3orgeZhZzvg6jNTSF4c7yWCFsdk3cwWe5pqAPgPRGdCiwr2EIY/oKK8gR+AStFcG4WQ==;EndpointSuffix=core.windows.net") diff --git a/src/generators/__pycache__/answer_generator.cpython-312.pyc b/src/generators/__pycache__/answer_generator.cpython-312.pyc index 1d1c8e7c5b7ad3cb270c52402b60e5e686dc0447..0bfa19928ca51b49b0255b80551ca670e2af3ae7 100644 GIT binary patch delta 575 zcmX|8O=}Zj5Z;OV5tHm2vb3fan*@cL#a0?L2cZ-{3PF(SK}3pBw%ylF>Si~c-Bfz= z(u1HL!utojcqlpMXL#tvivj6P@FJ!C1I{e1b9k8Nd1i)r=RK((*X-}MUC=1romIO2 z$i5CgMz_~T@0YLTG+7?AL$T=)t4L>eQGM0tKWxqrE&exWiP_Q_!$mQ*b~L|msQJZl z?n0{`lwA7{6Bnk=ldhM(z_>q1U&{U<^ioM`VME6EcRT0}y2R`(S@%+J+e_Hab5jT7 zJ$Ld6e(J)YSyR);-o8ZC6`Bc{>bLm-s;Xgq1ZQ$&t?1?rs=@_zQoIXIB}#KoEpD@R z;wY6-iZ!PN( z?VM1BO4n;f+7C`8v`>D5gT)%0}Kq@S!vZ9p1=3L_nq#0^4IE*n*K%CGf13I-~Kqq zL%o9k4sW!yq^J5?TO0OPOdHu5Z{9Do3mAzy>lOV{yM$2!k-|t$iqkF&RwCsCbNF#_ zbwQB)S*s%094U|R%z7z-wve4WLbg6i%(TUU(a68S)%EhHI#(Au*Y5kh901osbOb^HMTOd6`Va-!Pc9ufeJn<~R8{!U%P75-g) zjZ0&q-ASrd;er}}n>F`t#se-r3|^2)#$_&Lp+&m{(z2m-0~!(kY&@W)2Il9kRm23=F+ z({r8K2Xg$CQ6q8zs7Q<91&-rVN4!$nAkKYNwwn^-y*FhQZz!{vq}Khwjy$pfyMoHU bhIIrSU%(hoQFDscrpTP48vkePB<0wDi{Q3C diff --git a/src/handlers/__pycache__/glossary_handler.cpython-312.pyc b/src/handlers/__pycache__/glossary_handler.cpython-312.pyc index 1976ccf7618e64430846d3f85c4fd8e0ece64396..7413280d4a0041a580eac3182db36d82002bdb71 100644 GIT binary patch literal 8833 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 NuqW8>7#h(r{u^|^`y&7V delta 1734 zcmZ8h|8Epk5P!S-c5m-%yS-jZ356{xb}Kyz@k$gW(F4&$E2IhFMQzU8*JF>pU3|Nx zxw@s05OYM5Qr-_OL1RtS)HIs=7yOOH#84YTvxbCU{q3YC{OY_t3XLy$^Jd=6yqTT( z%w9clcQEsNI-NkUZeMy*Rg;UEUFW_6X$hIg6l_!%N`k<7(H84sNsM^Omg=~KBOcpw zT`4IMFWa%YT2cihBBHGEM`4xXAgLr?>t&j_`-4(aL?@A{TtjATO?+ySYN$s1Z!Q=@ znEB9ks_yZ3-YYw%O(@)`X4xmcRVTnFBW=;Ph~u+Yh2h1)xNwY>g)(qA3F(GZmtOtb#5Ea-2tFe+Vy&cM*Fc+>JLN?6{#)nPJIiKS`IB6r|#vWFz?G z+xJy{i)R+fmQU(ls8HfhQ>XGYcdkM)*b?lmQvxqt70ro(a2$O~5So+NMYW&la-2Ep z&0A@8%*5YFYhcHhfNoSV3PcZ^vZ+|Y1PV}%2mjxybumH9vdeP3XHGF?U)$CPO09?2 zmHit3z9+;!q4h9fk}1ove9Lv%$BB290WhY!*^h~d`9vsHi61JqTdfir%7jvvhD!P3 zMdFwggJGyl`xVEXp*%#Gv>cC6f3jY78lhwpCrov2h&gGQo)M!x{P8|+i%qxUg_u`~ zc0xwaQQ}S8zQ;?Xt9rC(mR(66Rk*9S#(qmqNRziG*+A-rzDHW_fo800#sjUet`%D5 zhIX{6u+LJukYn?y0kMVHFSvy#kb&7|`t{S#BN^fj9(U5mPzRIh5Qh!|F>sV_s8pt@ z=h9Bv5>K0dIZq8TTgViX28iAnAXfk&8J)4%XuO?3-MMCp=GbCazovjP>iEH^v?6`&?8{$Gv{58=o~>h$3vNVy5;Epd7^(YO~7inQufFwZ@=RKxGoXn*^@A- zdS@&=?qIR;p7gw4ah(bbt^?cDKX2#`nXxU0z*Q`zk4ZN~n}4m69K*}kPCi$|8nRt|4u z4>XT&c4Zf4ug`uREPwfD*U09e{@vNzXIDOH9o`rmyP0^@-M^Gs%&fe)(zilaM^^XT z+c$Q<`$#*9dWUxZ(~S;{HvNUh^~Q4lM%U0i^|jW1AT$R_jT;FzuD>+j3w(%e%kyEz ztcp*E;ew9xTgme&9Ru!X6uHnWSH-tU5*Y6-AlJ}i5zA+Ub}A>I5>{~=@w3&t6`k#T zLe8z?Ed)nZu&1st)qeW2gKcvSS)RqK6c!&xQ W(cnXLke%MYFMU)fJVrnw=l=kh*syj0 diff --git a/src/handlers/__pycache__/knowledge_graph_handler.cpython-312.pyc b/src/handlers/__pycache__/knowledge_graph_handler.cpython-312.pyc index b32d2991b7f76d37a5a536e804ac69506e5026c5..9fd09a76dbcdb67e125074d178aafdbfe7fd3490 100644 GIT binary patch delta 1312 zcmY*YO>7%Q6y90yde`3Urgj~>C2f2M2`N8PleDF2(w0(!)JkYo#aek6d&BIm zJ8PFFs#FP82nm5y%>juUN0f3XJ|GU<;KT)SsL)of5I0T*SK@}md%GbOBkecu&HLuP zneXjrf@>)%d9o?T zwnWpOVk)sMQ`O6unb=O#tf!e;Y%6rc(@j0LRcd&n=4fnZ=n*ex=3+YwIrHXHK6W&E z)GL^U*dC!{UePQPK@^DRb=v(e#NQ-Cf^d|!nS0Y=OyM6ZDyr{C6}M{^dSrm{N!Ti-ak08Xd^Odkxk$lo?r z$=Uv=#)TAF<_~kTWR3rmn|a|yU`1)G({X$|lAAOLok-dVT)(EVI>gu+gtPoYe&XCY zQWsR-`Z|?9pz>)-fni*LOk<%>OFG%oZ`l3cy~SKPVOX_xWSwdjDbm z3E58Ju58$$Znx2OZgp827HUddI#QYSL9)7y85+pdk=2MdbR7mKZuk86*?9CT@5>E_B_Xj zE5@>7afNMrhr18r=YX0H}YjpPKwde6teM5xNK{F?*IXl?XO+qvx>QMw2-fh{J#r zqQHlzb-~%2hZ<$RS}{nUU#>XhB>$$eaAye1NYDnt8v|i#AY2>>CkDb2|7&JG*A~fD KG8Ez|PWCT@ay60w delta 1893 zcmZuxNo-q17@qOIFLCT_PHHDbWC`^SH;_PFkX~hhkSfHfkRCVy67!GKbP-STH{ZX`KjZ(OUtjz3 zZ0y@;G=${$mHAh7ZRT!lxU-58`UvSr#|)AFiV0S-i@4P4CT_KQh)1q2<~4l8hr!p) z{6>HT6!);85h5YQy)0}*NJMcTiyAQ!Q{2zuMuH?153r;$Ksp0T2(pwhNCp)Tv9ysP z8O6hF$QUNWibvRpF-k^_F*2s)D9ak-WL)u>++>1GC?03qj2y`+o?v-nl1wU|WK+g8 znO1y&Z8v7f3`QM!ij=Hmmdq-{AlqRSNI~&5+i4U@5hE9(g>|?#`H@{xD$-q7Nnd^z z*X3QpN9J5;3F(>JNFQ2vZR+ORxG?-X2p0?3Nxj}`G_))104-^jW!HGKRjo<=z^gUQ zF7c*e+AGvBnPyX|jlW^@OD)@M*(Hrzl#2wO87;Lhv`VKJ&o?!la_JO2O{uxuSmm17 zhYy|$GifO6O`A5Z7hF#8lxEQjO`S5wyI7}8ml2_hwE5E2C9avUb!dg&w70T_mHKjB zuTr~f<6k_npe)_->AZfOavm2~d`EFgeC%6D41yuRm{G3s<~1G?zxl@Ttnm6j#8ch7 z{&i1yxAfq100UwvvKE~Ox<^7Gz!$_1k;T*@DOdpsxy_Fs7KP|BT<=z+XWaOhxR;p0 zXT(p5X}nMTlNj5v04dG_oRi>0Onp@zw$d_a!*&9uR;A@e%itHpiDVXE6z?QQeTTt& zQLH8R71{Ba*@yMJo5C=9<>37Tu!_S*S}S|#nnxCHh18v z=4F|FOeAuC@p&eW?|1KKc6sql@o;P&7ezXo4_}8OZUEdAC$m#STlZb?^|!LYwkF=o z?hmVsT*|km#0SZY_&yuOUw3=iA6@5)$_wCqfHc6bV%*Kv@lrt0FH}T`QanQSh`*|R`u*Y#VEyrK$ zXjYBYuQ)!lUU`>Meo`9wWeLtO(;C%EO{+J`l_sZiyg!`O;&3!4^Mct_^J#JTmz?nz zj7>GCWm65R#l7G&q)Rqs^K0W|K(A0FD(PoA7l=w40*n#Il^_G*g zcxA5AY^>I+n+wlC$jCE#)~r_RjjH@#z>N<<@GLmRUa+V}RV{M|e74%i2^=+=dW+HH z{1a)w(0W!&!XIvo@dFfmfMO3&RFQBGE%eZV9y;_8oqC9l_t0DqZR?@s9@^bQ6F;Nv ZJ+!xnj!02_Hd#!*<;E*`1F1#r_#bLr^GyH% diff --git a/src/handlers/__pycache__/query_handler.cpython-312.pyc b/src/handlers/__pycache__/query_handler.cpython-312.pyc index 971e615381976ede802bb90213021b2b0add9674..2c11db6f71627d2e6917ac770860b6ce756ee5ca 100644 GIT binary patch delta 1098 zcmZuwO>7%Q6n?Y+yUzM2ZcR#m;?zx(HAzgW7EoPUDG5yvD9}a~AF3&FP+p_V*KmO`<7tr(YH0sV=Z;V-5M*^gr>&EqR;s_~%wHG1k7e9!W&wH|@T@s8`Op7rohH zCFl9x?4^pwmRmBb_${SS%KMJRzp+z+aM|_qi}|WYXZdgTuqq}X&f~%JO!OQi3dzMmLxhT{+S9{eo*ae6}v?C#mi6Slpg4;I~7&FH=IL8d(*7 zlsCYUr>c7fr=QCX_YFGl*0a;unYRwUmppYmn@!C)RVACvjyh6HVqQxwSLPgv^c`<}5@HiB?zV!Z5Gj`<5$vbb}4~;jC z@lEu=l%w;N?&P@>sI!y+QTz*FXN2DACN72vz#)LL7@`$uOCxHaJxBV~So?JnR%4ro zBz1ypWrWhcO)6r)ocAB4yP9J|2>8D*<#)*XztrWeJ>^wTe mF_upuY{-O=bvU*TqwCPS4v8PY=Q|50y4E@Le delta 778 zcmYk4%}X0W7{=e(-Px~XH&NHEsc5u8Yi!X9T8bYnq+*IlgkqrtdWdc`3HfL@T2MAf z!Gi~(4MU-&w4TM2sW=ZSj|aFA4X zSqNQmFIAqTH*>k6FLD<(R9xpE3YN41mb}aT^L(pHSpC677*)%k%swq@CmyFJ6U&MD zL@`NCHI^(4XIz&m`EDw)9JLEih=ztxC%J=y!m1F)l@!{BS}>Ke3%&Uny6wHx3!H?^O24SHwMS1{DIFKG30~x0a_m#m@$AO zA&Go#HY+E-59rc3ZLQ*kR3e?XM`&%|WEdv^=te&Ro!`q1ZyP0d;Nwd+PJ33!A4gwIvX|lfraWolkar@@yrRO{6WTxhoY`o0G%*ZwQ8uMyrF`(>9 zh9YqwRU`@`*kJ@GkWtJ5Bw83gGqJOVF+vo9dpl zHdc^k&dE}&GR9zYq(OYR*|#`sa`RJ4b5iY!6oC?qKwNAHBt9@RGBVy}ke{5-s>K3Q F4FFk)GG71y delta 350 zcmcb~HJ6+BG%qg~0}yO0_?|v(BCjN4+eCHE`V@u~<{Xw>)+km+1|XXyhb@;qik*>x zi6NDBHAp!SL~#Il?5P}CLJ&SOxtbBei{fHpaA!ziZDB}ZTg?O&~UVrLCwgeU@u_-XRoVlSx71({#O3>3H}1m)=!gDfeEPb|nRVwpUjMVyg! z@^%(^Ha4K5BCg4ISY#~07RZ43a7%A-*yQG?l;)(`6)6KHKt3!s2NEBc85tSxGRWU$ Q(74N>GFh8div^?y096D>4gdfE diff --git a/src/services/storage_service.py b/src/services/storage_service.py index eb3a38a..34034b9 100644 --- a/src/services/storage_service.py +++ b/src/services/storage_service.py @@ -24,13 +24,18 @@ def __init__(self): logger.info("Successfully connected to Azure storage container") except Exception as e: logger.error(f"Failed to connect to Azure storage container: {e}") - raise - - # Mongo config + raise # Mongo config self.mongo_client = MongoClient(config.mongo_uri) self.metadata_db = self.mongo_client[config.mongo_metadata_db] self.metadata_collection = self.metadata_db[config.mongo_metadata_collection] + # Chat collection setup + self.chat_db = self.mongo_client[config.mongo_chat_db] + self.chat_collection = self.chat_db[config.mongo_chat_collection] + + # For datetime reference in main.py + self.datetime = datetime + try: logger.info("Connected to MongoDB") except Exception as e: From ff98b1e5232db5172ba3857a0e06159ca2cbf4f6 Mon Sep 17 00:00:00 2001 From: Kavinda Maduranga Date: Mon, 16 Jun 2025 01:04:35 +0530 Subject: [PATCH 2/3] Refactor chat API endpoints to use a consistent '/api' prefix and update configuration parameters --- main.py | 14 +++++++------- src/config/__pycache__/config.cpython-312.pyc | Bin 4206 -> 4432 bytes src/config/config.py | 7 ++++--- .../answer_generator.cpython-312.pyc | Bin 3919 -> 3919 bytes .../__pycache__/query_handler.cpython-312.pyc | Bin 5473 -> 5466 bytes .../storage_service.cpython-312.pyc | Bin 5278 -> 5563 bytes 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index 1c7b85e..729e119 100644 --- a/main.py +++ b/main.py @@ -5,8 +5,8 @@ from src.handlers.glossary_handler import GlossaryHandler from src.handlers.knowledge_graph_handler import KnowledgeGraphHandler -from src.generators.answer_generator import AnswerGenerator from src.services.storage_service import StorageService +from src.generators.answer_generator import AnswerGenerator from src.config.config import Config from src.config.logging_config import setup_logging @@ -16,9 +16,9 @@ config = Config() logger = setup_logging(config.logging_config) -answer_generator = AnswerGenerator(config) storage_service = StorageService() glossary_handler = GlossaryHandler() +answer_generator = AnswerGenerator(config) app = Flask(__name__) @@ -239,7 +239,7 @@ def save_chat_message(session_id, role, content): } # Endpoint to list chat sessions -@app.route('/chat/sessions', methods=['GET']) +@app.route('/api/chat/sessions', methods=['GET']) def list_chat_sessions(): """ List all chat sessions with their metadata. @@ -285,7 +285,7 @@ def list_chat_sessions(): # Endpoint to create a new chat session -@app.route('/chat/create', methods=['POST']) +@app.route('/api/chat/create', methods=['POST']) def create_chat_session(): """ Create a new chat session and return the session ID. @@ -320,7 +320,7 @@ def create_chat_session(): return jsonify({'error': f'Error creating chat session: {str(e)}'}), 500 # Endpoint to add a message to a specific chat session -@app.route('/chat/', methods=['POST']) +@app.route('/api/chat/', methods=['POST']) def add_message(session_id): data = request.get_json() role = data.get('role') @@ -339,7 +339,7 @@ def add_message(session_id): return jsonify(serialize_message(message)), 201 # Endpoint to retrieve chat history for a specific session -@app.route('/chat/', methods=['GET']) +@app.route('/api/chat/', methods=['GET']) def get_history(session_id): limit = int(request.args.get('limit', 100)) skip = int(request.args.get('skip', 0)) @@ -352,7 +352,7 @@ def get_history(session_id): return jsonify(messages), 200 # Endpoint to delete chat history for a specific session -@app.route('/chat/', methods=['DELETE']) +@app.route('/api/chat/', methods=['DELETE']) def delete_history(session_id): result = chat_collection.delete_many({'session_id': session_id}) return jsonify({'deleted_count': result.deleted_count}), 200 diff --git a/src/config/__pycache__/config.cpython-312.pyc b/src/config/__pycache__/config.cpython-312.pyc index 7d0eda0bac5de321ddf00132a8bc8da9886d7547..ed2187f51991aea5ec5de65db219aa84206ac630 100644 GIT binary patch delta 387 zcmaE-a6yUhG%qg~0}yW=6Tm?^ulalv0#eGl5hvFw`)_ zPZng>;8#geMG_L4e1TD2S}jE#NlX}`fq|itK~rP04eKS7d@pBHaQBqTBxAS8e1FUGWY3C_)L_3Nr{KVdfHJqdN-t-Z%}#7VtQzdj9*!aLE>5@j z!3<}AA0Joe5Kn)W=5sS?^ukbl~Yt!Gl5hvFw`)_ z3qn{743!L;s*_i+UNQ+wigYw9@r}w%%k>NmFD)}Nt?~+YPpM2Yc8koh%m|1|vUKwg z%Q7$v^D0jYEp$oG$q3xMjxC6FvL??TMxn`Syq_l9@y%f@pM0C|3nSO$3;dd_U)UIw zR3`u7S7qdw>@ASaYXh`(B}0)dkSemCyk9_-7tCY@@-vcV3SBG%qg~0}x2v_S?wq!VdsEOa&(Z delta 20 acmX>vcV3SBG%qg~0}y0?65Pn`!VdsF+yznq diff --git a/src/handlers/__pycache__/query_handler.cpython-312.pyc b/src/handlers/__pycache__/query_handler.cpython-312.pyc index 2c11db6f71627d2e6917ac770860b6ce756ee5ca..d43310f6001be5a0dda430838566fff1e7f0034d 100644 GIT binary patch delta 54 zcmaE;bxVu)G%qg~0}#yo=ao^pkynnJ)sBgQfot+ZZjs3@+!I(d6{@)=PvjAwyqt%3 JGc!*dCjg{g4xRu2 delta 61 zcmcbm^-zoVG%qg~0}$kY63l4W$ScRq>dM5xz%^NrM`W@K_XKVog}nTfR80kiYOcu# Qc|<4At zqe%T+C{}U!rEis{1;)HYKSS7HCgM}j`@bY|Y*FQq?Y5|G1vluibkcT)``>KXc8rv* z$PJ>|XKo@CG3yQ)Dd^Xm=8pyEIB7!7F(}14LwE<7JIGMQ1uJ= zTp%7%AblBJM%5}!hA1+fOuZe*pnLdL;4La)Lr9`&yeFLSEJFM&U9!dRUJH~<`=iZ_CfM}`C8Ny$_c iiXGnu`EQ)%%I;|DjrwLzzm9XEHUAQgW)CUgAoK_EL!|To delta 543 zcmdn3Jx`PGG%qg~0}v=yeovnux{=R^iK&EXa}3i0Mn=xbyvz!lceD60G74|zVv}c_ zoX6hE^n+pYSN1?gp2_YUZXzr!oI-bag!;WZy)Q`UUFFgL@kMmDMnAvjq}NJi2E$k*h!#gda*TvDVA8W~2Ir&LNY#`ZS5Rm{RRx%WYfY@pvLJ&p>0~y8YK%#}=6O$0D(ANS+23GyM zEQ+^Tgl@C&-ewWJ%VPA2MVFQTa~vZ-t1iS0K?b1NnvzASljB7s7*i%!h)feeIEWu) rxcy`~QDvz#5EE|gEe@O9{FKt1RJ)?w$ Date: Wed, 18 Jun 2025 00:49:25 +0530 Subject: [PATCH 3/3] Refactor answer generation and response handling in chat functionality; add glossary management endpoints --- main.py | 185 ++++++++++++------ src/config/__pycache__/config.cpython-312.pyc | Bin 4432 -> 4699 bytes src/config/config.py | 6 +- .../answer_generator.cpython-312.pyc | Bin 3919 -> 4690 bytes .../explanation_handler.cpython-312.pyc | Bin 0 -> 1279 bytes .../__pycache__/query_handler.cpython-312.pyc | Bin 5466 -> 7288 bytes .../storage_service.cpython-312.pyc | Bin 5563 -> 5563 bytes 7 files changed, 125 insertions(+), 66 deletions(-) create mode 100644 src/handlers/__pycache__/explanation_handler.cpython-312.pyc diff --git a/main.py b/main.py index 729e119..631baf2 100644 --- a/main.py +++ b/main.py @@ -98,14 +98,15 @@ def get_answer(): logger.info(f"Saved user question to session {session_id}") # Generate answer using the knowledge graph - answer = answer_generator.generate_answer(question) + reponse = answer_generator.generate_answer(question) # Save LLM response to database - assistant_message = save_chat_message(session_id, 'assistant', str(answer)) + assistant_message = save_chat_message(session_id, 'assistant', str(reponse["answer"]), reponse["explanation"]) logger.info(f"Saved LLM response to session {session_id}") return jsonify({ - "answer": str(answer), + "answer": str(reponse["answer"]), + "explanation": reponse["explanation"], "session_id": session_id # Return session_id to frontend }) except Exception as e: @@ -133,18 +134,21 @@ def chat_with_knowledge_graph(session_id): user_message = save_chat_message(session_id, 'user', question) # Generate answer using the knowledge graph - answer = answer_generator.generate_answer(question) + response = answer_generator.generate_answer(question) + + logger.info(f"Generated answer for session {session_id}: {response}") # Save LLM response to database - assistant_message = save_chat_message(session_id, 'assistant', str(answer)) + assistant_message = save_chat_message(session_id, 'assistant', str(response["answer"]), response["explanation"]) logger.info(f"Processed chat query for session {session_id}") return jsonify({ - "answer": str(answer), + "answer": str(response["answer"]), "session_id": session_id, "user_message": serialize_message(user_message), - "assistant_message": serialize_message(assistant_message) + "assistant_message": serialize_message(assistant_message), + "explanation": response["explanation"] }), 200 except Exception as e: @@ -152,63 +156,13 @@ def chat_with_knowledge_graph(session_id): return jsonify({"error": str(e)}), 500 -# New endpoint: add glossary items -@app.route('/api/glossary/add', methods=['POST']) -def add_glossary(): - data = request.get_json() - if not isinstance(data, list): - return jsonify({"error": "Expected a list of glossary items."}), 400 - try: - result = glossary_handler.add_glossary_items(data) - return jsonify(result), 200 - except Exception as e: - logger.error(f"Error adding glossary items: {e}") - return jsonify({"error": str(e)}), 500 - - -# New endpoint: get all glossary items -@app.route('/api/glossary/list', methods=['GET']) -def list_glossary(): - try: - items = glossary_handler.get_all_glossary_items() - return jsonify(items), 200 - except Exception as e: - logger.error(f"Error retrieving glossary items: {e}") - return jsonify({"error": str(e)}), 500 - - -@app.route('/api/glossary/update/', methods=['PATCH']) -def update_glossary(item_id): - data = request.get_json() - if not data or not isinstance(data, dict): - return jsonify({"error": "Expected a JSON object with update data."}), 400 - try: - result = glossary_handler.update_glossary_item(item_id, data) - if "error" in result: - return jsonify(result), 400 - return jsonify(result), 200 - except Exception as e: - logger.error(f"Error updating glossary item: {e}") - return jsonify({"error": str(e)}), 500 - - -@app.route('/api/glossary/delete/', methods=['DELETE']) -def delete_glossary(item_id): - try: - result = glossary_handler.delete_glossary_item(item_id) - if "error" in result: - return jsonify(result), 400 - return jsonify(result), 200 - except Exception as e: - logger.error(f"Error deleting glossary item: {e}") - return jsonify({"error": str(e)}), 500 - + # Chat functionality setup chat_collection = storage_service.chat_collection datetime = storage_service.datetime -def save_chat_message(session_id, role, content): +def save_chat_message(session_id, role, answer, explanation = []): """ Utility function to save a chat message to the database. @@ -223,7 +177,8 @@ def save_chat_message(session_id, role, content): message = { 'session_id': session_id, 'role': role, - 'content': content, + 'content': answer, + 'explanation': explanation, 'timestamp': datetime.utcnow() } result = chat_collection.insert_one(message) @@ -235,7 +190,8 @@ def save_chat_message(session_id, role, content): 'session_id': msg['session_id'], 'role': msg['role'], 'content': msg['content'], - 'timestamp': msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'] + 'timestamp': msg['timestamp'].isoformat() if isinstance(msg['timestamp'], datetime) else msg['timestamp'], + 'explanation': msg["explanation"] } # Endpoint to list chat sessions @@ -252,7 +208,9 @@ def list_chat_sessions(): '$group': { '_id': '$session_id', 'last_message_time': {'$max': '$timestamp'}, - 'message_count': {'$sum': 1} + 'message_count': {'$sum': 1}, + 'first_user_message': {'$first': {'$cond': [{'$eq': ['$role', 'user']}, '$content', None]}}, + 'last_message': {'$last': '$content'} } }, { @@ -268,10 +226,15 @@ def list_chat_sessions(): # Format the response formatted_sessions = [] for session in sessions: + # Generate topic suggestion from first user message or last message + topic = generate_topic_suggestion(session.get('first_user_message') or session.get('last_message', '')) + formatted_sessions.append({ 'session_id': session['_id'], 'last_message_time': session['last_message_time'].isoformat() if isinstance(session['last_message_time'], datetime) else session['last_message_time'], - 'message_count': session['message_count'] + 'message_count': session['message_count'], + 'suggested_topic': topic, + 'preview': truncate_text(session.get('last_message', ''), 100) }) return jsonify({ @@ -283,6 +246,51 @@ def list_chat_sessions(): logger.error(f"Error listing chat sessions: {e}") return jsonify({'error': f'Error listing chat sessions: {str(e)}'}), 500 +def generate_topic_suggestion(message): + """ + Generate a topic suggestion based on the message content. + """ + if not message: + return "New Chat" + + # Simple keyword-based topic generation + message_lower = message.lower() + + # Define topic keywords + topic_keywords = { + "Technical Support": ["error", "bug", "issue", "problem", "fix", "troubleshoot"], + "Data Analysis": ["data", "analysis", "chart", "graph", "statistics", "metrics"], + "Knowledge Query": ["what is", "how to", "explain", "define", "meaning"], + "Document Processing": ["pdf", "document", "file", "upload", "process"], + "API Integration": ["api", "endpoint", "request", "response", "integration"], + "Database": ["database", "query", "mongodb", "neo4j", "collection"], + "Configuration": ["config", "setup", "install", "configure", "environment"] + } + + # Check for keyword matches + for topic, keywords in topic_keywords.items(): + if any(keyword in message_lower for keyword in keywords): + return topic + + # Extract first few meaningful words as fallback + words = message.split()[:3] + if words: + return " ".join(words).title() + + return "General Discussion" + +def truncate_text(text, max_length=100): + """ + Truncate text to specified length with ellipsis. + """ + if not text: + return "" + + if len(text) <= max_length: + return text + + return text[:max_length].rsplit(' ', 1)[0] + "..." + # Endpoint to create a new chat session @app.route('/api/chat/create', methods=['POST']) @@ -351,6 +359,7 @@ def get_history(session_id): messages = [serialize_message(msg) for msg in cursor] return jsonify(messages), 200 + # Endpoint to delete chat history for a specific session @app.route('/api/chat/', methods=['DELETE']) def delete_history(session_id): @@ -358,7 +367,57 @@ def delete_history(session_id): return jsonify({'deleted_count': result.deleted_count}), 200 +# New endpoint: add glossary items +@app.route('/api/glossary/add', methods=['POST']) +def add_glossary(): + data = request.get_json() + if not isinstance(data, list): + return jsonify({"error": "Expected a list of glossary items."}), 400 + try: + result = glossary_handler.add_glossary_items(data) + return jsonify(result), 200 + except Exception as e: + logger.error(f"Error adding glossary items: {e}") + return jsonify({"error": str(e)}), 500 + + +# New endpoint: get all glossary items +@app.route('/api/glossary/list', methods=['GET']) +def list_glossary(): + try: + items = glossary_handler.get_all_glossary_items() + return jsonify(items), 200 + except Exception as e: + logger.error(f"Error retrieving glossary items: {e}") + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/glossary/update/', methods=['PATCH']) +def update_glossary(item_id): + data = request.get_json() + if not data or not isinstance(data, dict): + return jsonify({"error": "Expected a JSON object with update data."}), 400 + try: + result = glossary_handler.update_glossary_item(item_id, data) + if "error" in result: + return jsonify(result), 400 + return jsonify(result), 200 + except Exception as e: + logger.error(f"Error updating glossary item: {e}") + return jsonify({"error": str(e)}), 500 + +@app.route('/api/glossary/delete/', methods=['DELETE']) +def delete_glossary(item_id): + try: + result = glossary_handler.delete_glossary_item(item_id) + if "error" in result: + return jsonify(result), 400 + return jsonify(result), 200 + except Exception as e: + logger.error(f"Error deleting glossary item: {e}") + return jsonify({"error": str(e)}), 500 + if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/src/config/__pycache__/config.cpython-312.pyc b/src/config/__pycache__/config.cpython-312.pyc index ed2187f51991aea5ec5de65db219aa84206ac630..0e75f0686d2e527b8205ad07a0af860f5c956588 100644 GIT binary patch delta 1103 zcmZuw-)mGy6uvjv)XkR78j=lYC3{SZx+PhEw$Km?(WDTnHH|3}T9C=TXYUT)ow?4; z-DIV7`&bb4VSEu0eYE;i7N7kC`Vwd#MEm3)Ahq?)2YcpT69e_W%y8yA=X~FH&fS`N z_Q}Za<#NfRZ{xp;0Jm^o^7lo4}ABXU8zyUn(NAHpy9Xw($4xO`4 z{MtuH!uqbK@f_rNU9bLW|NhW~-#c9Jyb`?q)?^AyAus{BnV>SjwC36{VL&Gsa2E5c z7y=gr(Hp3^h#-i$-~o$4p+=^H7Fthrl--o7%^adJwcYxE z=ME|R`h%m#DTDG1szjl2HR;;cS>@+H-xSh-k+6=`S{e-NvtY07nJj##QQ7-@z8DrE zDkmefwc@_gTUx}(E*GmkDiBI=H3FG*fjTKnkQs<#sWnqwic@RyfOI<cf zF-;FxlSlwdlDO+^-m##CaWd^joJcX6cbf&+w!n6VFZMobz_OAZ9+E?}6!~2yFCkFhgJygC{s)UFMjasq(gYq%oe`KGH#0Op#y-B#cc5Cfk@9%-J z;jw=Q$0x^sDg0U7|LO*3?42)%f&$CBXS6{c$3b$Q!ib*OTta8=D@^n*=%JDxfk4GB)v+MjBYz3wtFuEIv!5m;5Gw(5E^z>d3UNUJmh8Hmq~y33`c!oQX2$Mm+5 zBOODbmjWAW4s(JfBq(l%)#BY6NSUD~f!AVshPvHD`ya*RQLD;tb-L;FcSQ2wzRTrrMY$4Nc)7vvf zIbH@y{Tcl$z0^5WfqAE8c7oWjm}o%3Fku{UwdqnuBQcD~0;%MIxRd-eGO=+7%Q6rS~Z*K2$I8`rT7u{Vj+)NQh9R6<)471Yp%fD|Y>pq7K#dMEa}>s@zt z(=^2)Q1uXjIE3k~MdDNyg3192aYdC15(nZol7qzoB#uRL+Xx8>BxdZ`u@D`}Z|1%C z&G+8SdprIqc|Yy{(dY93GVXtTrf@L4?vF9Qt`A=vP=V^v!=;{cJq&OF7}}s1GR{Sa z)&nEOgX?#mIT|I3ujA?^S^!2im|bYh0cL>ey#`cq!?A0NFZ(k7PncQ1Z@Qq^FGEWyo+tEZVDp#ZOb}YW*awa_#mDmw66lCTo zzAhY(oR+{n$vqQd?_~yOMD}};n+f6HgkI)P{FgBPyiFptMv-f;_U3lkR#-Q%`L@02 z#>d<#Z?h$+t?0v|yKmK5aiUO#Ee6`Sy;*g%3(kP2AlNPhD0Zs4$5Eu0@}E(0keK9W zFsgI4!l>NM4$TT@?`xwgoJ}jy9PKVcVuky^&RF4|k|5ueAY0*7*GG#8ldEgtLv`%T4B4TDpXuIz^7ezauulV71w*NC1wd+X3v5pCgZ_tp0(90W3!T? zTc(sNz}y9>N_GKCdEK-u1uaVpCXx`;;U&eeB^#oWC107JGnYoB+2N9uhXzE7tr>Yq zk#c6Kj9>v8mUanBieW85B%N0*LTMT#gx+FS2tOh=(x@gXDK1+x4Oy0P&^xeNoE;`x zcUcKWA^Tc9Hu+3xg!7sKtqxfyvt2UEB`qMc)C_gOFqd?w<_RNG$^}BDb`B9YpNNcrMyZFpxCqfQdXBC`T|u&z4a*jqpmrWX zYf-oB{sqlY^eoZJreQ%e214ZPr2(YffezzC;>3~D#ALDL<$E7AoMZ0r@6mx;bg~+q z{Pxs?=*hL29Uk0|gsU*_aE$rsV2KL<~U1n4jo{q))8hGp& zhJ`5i>UPP$vTT4`J>En%mpR`kYU1)0hP@?{{mY0 BuHXOw delta 1098 zcmZ8gO=uHA6rS0iY<9DoG@EwYwA#itYC>Bv)Z$OIYD%qQ6-1QkrG{i16Wa~WZmpHA zs1&JQG`ypF5K)j`1P_V`VnGp9{6!^Rz57$IqBF5ue;&*?^S<}KnKy5CU&miG2cK%1 zf~Ylq+v3zj`$E8aM5=I2Bc<~l-hAUT$g5iI#t+1 z(B*#uvb+W|awks01$li(e*!&9$lId)qg30rTK;jZNY~kgD^!p1QRL~EKM&vKR8U5a zP~|BUCCKm*uoN}mU*rO?jEg{{qC|OGxa#qL=3zjRS{qvfJ2 z{;NBKa=t0?q&R^m&~g3%n!rg3J_lythuXTNX{%*Vm?V}rEt6y%GZz~vk+Im|;I0iZ zxT-D18a&Vv5?Mk|B}?I_)|n*jTCO8d5Va1Fwi_;&uKGn8hzsuVrqT} zCq$iW_2Rpe4KQ7EsIh)DW974lGX-lnTO#JFG1Im)dDA8m#KUd$^WGkgMP;h0gl&>t zMVmBI$S+Jpfz#oZJps07Jxd9Wmh$<6m8V;=wBOzDX5lUd)#A3WRkaLOaK*oItdtuc dHG9b*b=kjd?}x8p0~CFIXdlM<3NoYg&6S2kXs45ur-0f(E1qF$9KnXPTtl?5?vLHKEWR zaTSKu+gGbGaR#%vB1%xG-!t?yW_71liW zk>v_UDx3COz>Q$AQ+EtRhg8Tq;h-g_3CN23G%|uv`T;03 zc6_I`)!NzE+W3-XA59jsAIy5-^5Wm+9yroc%yJy2r0ppoMFPF3aMI^68#cag7w8^nXjT=X4&0xFBT>{sUsTtFI6MOdiJlfM;uyaWfOf42t wQ95k;HMf$qii=)>$VsEfyqjRXr>;Q&H$T8>9;GOyKgiT?{UXgB6A*FEUr~l4MF0Q* literal 0 HcmV?d00001 diff --git a/src/handlers/__pycache__/query_handler.cpython-312.pyc b/src/handlers/__pycache__/query_handler.cpython-312.pyc index d43310f6001be5a0dda430838566fff1e7f0034d..b7109f28dcbb2b897daee7d5886dd827f8810a48 100644 GIT binary patch delta 2758 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 CQXeh= diff --git a/src/services/__pycache__/storage_service.cpython-312.pyc b/src/services/__pycache__/storage_service.cpython-312.pyc index e573766e0f17b03294a755277ed800d406a1fd0d..47adafbead43452cd75a7771542f4e7dc26cafee 100644 GIT binary patch delta 20 acmdn3y<406G%qg~0}$M74BW`QK@