33import argparse
44import requests
55import uvicorn
6+ from datetime import date , timedelta
67from starlette .applications import Starlette
78from starlette .routing import Route
89from starlette .responses import Response
@@ -58,13 +59,27 @@ async def list_tools() -> list[Tool]:
5859 "required" : ["name_query" ]
5960 }
6061 ),
61- # NEW TOOL ADDED HERE
6262 Tool (
6363 name = "list_organization_skills" ,
6464 description = "Retrieves a full list of all available skills/competences defined in the organization." ,
6565 inputSchema = {
6666 "type" : "object" ,
67- "properties" : {}, # No arguments needed
67+ "properties" : {},
68+ }
69+ ),
70+ # NEW TOOL: Availability Checker
71+ Tool (
72+ name = "check_staff_availability" ,
73+ description = "Checks who is free and views current allocations for a date range. Can filter by skill." ,
74+ inputSchema = {
75+ "type" : "object" ,
76+ "properties" : {
77+ "start_date" : {"type" : "string" , "description" : "Start date in YYYY-MM-DD format" },
78+ "end_date" : {"type" : "string" , "description" : "End date in YYYY-MM-DD format" },
79+ "skill_filter" : {"type" : "string" , "description" : "Optional: Filter for employees with this skill (e.g. 'React')" },
80+ "min_availability" : {"type" : "integer" , "description" : "Optional: Minimum free percentage to show (0-100). Default 0." }
81+ },
82+ "required" : ["start_date" , "end_date" ]
6883 }
6984 )
7085 ]
@@ -75,10 +90,16 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent | ImageConte
7590 return [TextContent (type = "text" , text = find_experts_logic (arguments ["skill_name" ]))]
7691 if name == "get_employee_competence_profile" :
7792 return [TextContent (type = "text" , text = profile_logic (arguments ["name_query" ]))]
78-
79- # NEW LOGIC HANDLER
8093 if name == "list_organization_skills" :
8194 return [TextContent (type = "text" , text = list_skills_logic ())]
95+ # NEW HANDLER
96+ if name == "check_staff_availability" :
97+ return [TextContent (type = "text" , text = check_availability_logic (
98+ arguments ["start_date" ],
99+ arguments ["end_date" ],
100+ arguments .get ("skill_filter" ),
101+ arguments .get ("min_availability" , 0 )
102+ ))]
82103
83104 raise ValueError (f"Unknown tool: { name } " )
84105
@@ -138,7 +159,6 @@ def profile_logic(name_query: str) -> str:
138159
139160 return "\n \n " .join (results ) if results else f"No employee found for '{ name_query } '."
140161
141- # NEW LOGIC FUNCTION
142162def list_skills_logic () -> str :
143163 try :
144164 url = f"{ BASE_URL } /v1/skill"
@@ -151,24 +171,114 @@ def list_skills_logic() -> str:
151171 if not skills :
152172 return "No skills defined in the library."
153173
154- # Group skills by category for better readability
155174 categories = {}
156175 for s in skills :
157- # Default to "Other" if category is missing/null
158176 cat = s .get ('category' ) or "Uncategorized"
159- if cat not in categories :
160- categories [cat ] = []
177+ if cat not in categories : categories [cat ] = []
161178 categories [cat ].append (s .get ('name' , 'Unnamed Skill' ))
162179
163180 output = ["**Organization Skill Library**\n " ]
164-
165- # Sort categories alphabetically
166181 for cat in sorted (categories .keys ()):
167182 skill_names = sorted (categories [cat ])
168- # Format: "### Category Name \n - Skill 1, Skill 2..."
169183 output .append (f"### { cat } " )
170184 output .append (", " .join (skill_names ))
171- output .append ("" ) # Empty line for spacing
185+ output .append ("" )
186+
187+ return "\n " .join (output )
188+
189+ def check_availability_logic (start_date : str , end_date : str , skill_filter : str = None , min_availability : int = 0 ) -> str :
190+ # 1. Fetch all employees first
191+ try :
192+ emp_url = f"{ BASE_URL } /v1/employee"
193+ emp_response = requests .get (emp_url , headers = get_headers (), timeout = 15 )
194+ emp_response .raise_for_status ()
195+ all_employees = emp_response .json ()
196+ except Exception as e :
197+ return f"Error fetching employees: { str (e )} "
198+
199+ # 2. Filter employees if skill_filter is active
200+ relevant_employee_ids = set ()
201+ relevant_employees = {}
202+
203+ for emp in all_employees :
204+ emp_id = emp .get ('id' )
205+ if not emp_id : continue
206+
207+ # If skill filter applies, check skills
208+ if skill_filter :
209+ skills = emp .get ('skills' , [])
210+ has_skill = any (skill_filter .lower () in s .get ('name' , '' ).lower () for s in skills )
211+ if not has_skill :
212+ continue
213+
214+ relevant_employee_ids .add (emp_id )
215+ relevant_employees [emp_id ] = emp
216+
217+ if not relevant_employee_ids :
218+ return f"No employees found matching skill: { skill_filter } " if skill_filter else "No employees found."
219+
220+ # 3. Fetch allocations for the period
221+ try :
222+ alloc_url = f"{ BASE_URL } /v1/allocation_reporting"
223+ params = {"startDate" : start_date , "endDate" : end_date }
224+ alloc_response = requests .get (alloc_url , headers = get_headers (), params = params , timeout = 15 )
225+ alloc_response .raise_for_status ()
226+ allocations = alloc_response .json ()
227+ except Exception as e :
228+ return f"Error fetching allocations: { str (e )} "
229+
230+ # 4. Aggregate allocations by employee
231+ # Map: employee_id -> list of project allocations
232+ emp_workload = {eid : [] for eid in relevant_employee_ids }
233+ emp_total_alloc = {eid : 0.0 for eid in relevant_employee_ids }
234+
235+ for alloc in allocations :
236+ eid = alloc .get ('employeeId' )
237+ # Only process if this employee is in our filtered list
238+ if eid in relevant_employee_ids :
239+ # According to schema, 'allocation' is the percentage
240+ percentage = alloc .get ('allocation' , 0 )
241+ project_name = alloc .get ('projectName' , 'Unknown Project' )
242+
243+ emp_total_alloc [eid ] += percentage
244+ emp_workload [eid ].append (f"{ project_name } ({ percentage } %)" )
245+
246+ # 5. Format Output
247+ output = []
248+ output .append (f"**Availability Report ({ start_date } to { end_date } )**" )
249+ if skill_filter :
250+ output .append (f"Filter: Skill matching '{ skill_filter } '\n " )
251+
252+ # Sort by availability (most free first)
253+ sorted_eids = sorted (relevant_employee_ids , key = lambda x : emp_total_alloc [x ])
254+
255+ for eid in sorted_eids :
256+ total_load = emp_total_alloc [eid ]
257+ availability = 100 - total_load
258+
259+ # Skip if below requested availability
260+ if availability < min_availability :
261+ continue
262+
263+ emp = relevant_employees [eid ]
264+ full_name = f"{ emp .get ('firstName' , '' )} { emp .get ('lastName' , '' )} " .strip ()
265+ title = emp .get ('title' , 'No Title' )
266+
267+ # Status Icon
268+ if availability >= 80 :
269+ icon = "🟢" # Mostly free
270+ elif availability >= 20 :
271+ icon = "🟡" # Partially booked
272+ else :
273+ icon = "🔴" # Busy
274+
275+ # Work details
276+ work_desc = ", " .join (emp_workload [eid ]) if emp_workload [eid ] else "No allocations"
277+
278+ output .append (f"{ icon } **{ full_name } ** ({ title } )" )
279+ output .append (f" Availability: **{ availability :.0f} %** (Allocated: { total_load :.0f} %)" )
280+ output .append (f" Current Work: { work_desc } " )
281+ output .append ("" )
172282
173283 return "\n " .join (output )
174284
0 commit comments