diff --git a/sijapi/routers/llm.py b/sijapi/routers/llm.py index 87c8417..3433140 100644 --- a/sijapi/routers/llm.py +++ b/sijapi/routers/llm.py @@ -94,6 +94,32 @@ async def query_ollama(usr: str, sys: str = LLM_SYS_MSG, model: str = DEFAULT_LL else: L.DEBUG("No choices found in response") return None + +async def query_ollama_multishot( + message_list: List[str], + sys: str = LLM_SYS_MSG, + model: str = DEFAULT_LLM, + max_tokens: int = 200 +): + if len(message_list) % 2 == 0: + raise ValueError("message_list must contain an odd number of strings") + + messages = [{"role": "system", "content": sys}] + + for i in range(0, len(message_list), 2): + messages.append({"role": "user", "content": message_list[i]}) + if i + 1 < len(message_list): + messages.append({"role": "assistant", "content": message_list[i+1]}) + + LLM = Ollama() + response = await LLM.chat(model=model, messages=messages, options={"num_predict": max_tokens}) + L.DEBUG(response) + + if "message" in response and "content" in response["message"]: + return response["message"]["content"] + else: + L.DEBUG("No content found in response") + return None def is_vision_request(content): return False diff --git a/sijapi/routers/locate.py b/sijapi/routers/locate.py index a7a50a4..2ca99b2 100644 --- a/sijapi/routers/locate.py +++ b/sijapi/routers/locate.py @@ -172,7 +172,7 @@ async def localize_datetime(dt, fetch_loc: bool = False): -def find_override_locations(lat: float, lon: float) -> Optional[str]: +async def find_override_locations(lat: float, lon: float) -> Optional[str]: # Load the JSON file with open(NAMED_LOCATIONS, 'r') as file: locations = yaml.safe_load(file) @@ -510,21 +510,21 @@ async def get_last_location() -> Optional[Location]: query_datetime = datetime.now(TZ) L.DEBUG(f"Query_datetime: {query_datetime}") - location = await fetch_last_location_before(query_datetime) + this_location = await fetch_last_location_before(query_datetime) - if location: - L.DEBUG(f"location: {location}") - return location + if this_location: + L.DEBUG(f"location: {this_location}") + return this_location return None @locate.get("/locate", response_model=Location) async def get_last_location_endpoint() -> JSONResponse: - location = await get_last_location() + this_location = await get_last_location() - if location: - location_dict = location.model_dump() - location_dict["datetime"] = location.datetime.isoformat() + if this_location: + location_dict = this_location.model_dump() + location_dict["datetime"] = this_location.datetime.isoformat() return JSONResponse(content=location_dict) else: raise HTTPException(status_code=404, detail="No location found before the specified datetime") diff --git a/sijapi/routers/note.py b/sijapi/routers/note.py index 318faf9..2491cb0 100644 --- a/sijapi/routers/note.py +++ b/sijapi/routers/note.py @@ -647,8 +647,29 @@ async def banner_endpoint(dt: str, location: str = None, mood: str = None, other return jpg_path +async def get_note(date_time: datetime): + date_time = await locate.localize_datetime(date_time); + absolute_path, local_path = assemble_journal_path(date_time, filename = "Notes", extension = ".md", no_timestamp = True) + + if absolute_path.is_file(): + with open(absolute_path, 'r', encoding='utf-8') as file: + content = file.read() + return content if content else None + +async def sentiment_analysis(date_time: datetime): + most_recent_note = await get_note(date_time) + most_recent_note = most_recent_note or await get_note(date_time - timedelta(days=1)) + if most_recent_note: + sys_msg = "You are a sentiment analysis AI bot. Your task is to analyze text and give a one-word description of the mood it contains, such as 'optimistic', 'pensive', 'nostalgic', 'livid', et cetera." + prompt = f"Provide sentiment analysis of the following notes: {most_recent_note}" + multishot_prompt = ["Provide sentiment analysis of the following notes: I am sad today my girlfriend broke up with me", "lonely", "Provide sentiment analysis of the following notes: Work has been so busy lately it is like there are not enough hours in the day", "hectic", prompt] + analysis = await llm.query_ollama_multishot(multishot_prompt, sys_msg, max_tokens = 10) + return analysis + else: + return "" + async def generate_banner(dt, location: Location = None, forecast: str = None, mood: str = None, other_context: str = None): - L.DEBUG(f"Location: {location}, forecast: {forecast}, mood: {mood}, other_context: {other_context}") + # L.DEBUG(f"Location: {location}, forecast: {forecast}, mood: {mood}, other_context: {other_context}") date_time = await locate.localize_datetime(dt) L.DEBUG(f"generate_banner called with date_time: {date_time}") destination_path, local_path = assemble_journal_path(date_time, filename="Banner", extension=".jpg", no_timestamp = True) @@ -662,12 +683,12 @@ async def generate_banner(dt, location: Location = None, forecast: str = None, m display_name = "Location: " if location: lat, lon = location.latitude, location.longitude - override_location = locate.find_override_locations(lat, lon) + override_location = await locate.find_override_locations(lat, lon) display_name += f"{override_location}, " if override_location else "" if location.display_name: display_name += f"{location.display_name}" - elif: + else: display_name += f"{location.road}, " if location.road else "" display_name += f"the {location.neighbourhood} neighbourhood of " if location.neighbourhood else "" display_name += f"the {location.suburb} suburb of " if location.suburb else "" @@ -676,18 +697,43 @@ async def generate_banner(dt, location: Location = None, forecast: str = None, m display_name += f"{location.state} " if location.state else "" display_name += f"{location.country} " if location.country else "" + if display_name == "Location: ": + geocoded_location = await locate.reverse_geocode(lat, lon) + if geocoded_location.display_name or geocoded_location.city or geocoded_location.country: + return await generate_banner(dt, geocoded_location, forecast, mood, other_context) + else: + L.WARN(f"Failed to get a useable location for purposes of generating a banner, but we'll generate one anyway.") if not forecast: forecast = "The weather forecast is: " + await update_dn_weather(date_time) - + + sentiment = await sentiment_analysis(date_time) + mood = sentiment if not mood else mood mood = f"Mood: {mood}" if mood else "" - other_context = f"Additional information: {other_context}" if other_context else "" - + + if mood and sentiment: mood = f"Mood: {mood}, {sentiment}" + elif mood and not sentiment: mood = f"Mood: {mood}" + elif sentiment and not mood: mood = f"Mood: {sentiment}" + else: mood = "" + + events = await calendar.get_events(date_time, date_time) + formatted_events = [] + for event in events: + event_str = event.get('name') + if event.get('location'): + event_str += f" at {event.get('location')}" + formatted_events.append(event_str) + + additional_info = ', '.join(formatted_events) if formatted_events else '' + + other_context = f"{other_context}, {additional_info}" if other_context else additional_info + other_context = f"Additional information: {other_context}" if other_context else "" + prompt = "Generate an aesthetically appealing banner image for a daily note that helps to visualize the following scene information: " prompt += "\n".join([display_name, forecast, mood, other_context]) L.DEBUG(f"Prompt: {prompt}") # sd.workflow(prompt: str, scene: str = None, size: str = None, style: str = "photorealistic", earlyurl: bool = False, destination_path: str = None): - final_path = await sd.workflow(prompt, scene=OBSIDIAN_BANNER_SCENE, size="1080x512", style="romantic", earlyout="local", destination_path=destination_path) + final_path = await sd.workflow(prompt, scene=OBSIDIAN_BANNER_SCENE, size="1080x512", style="romantic", destination_path=destination_path) if not str(local_path) in str(final_path): L.INFO(f"Apparent mismatch between local path, {local_path}, and final_path, {final_path}") @@ -705,7 +751,7 @@ async def note_weather_get( ): try: - date_time = datetime.now() if date == "0" else locate.localize_datetime(date) + date_time = datetime.now() if date == "0" else await locate.localize_datetime(date) L.DEBUG(f"date: {date} .. date_time: {date_time}") content = await update_dn_weather(date_time) #, lat, lon) return JSONResponse(content={"forecast": content}, status_code=200) @@ -735,7 +781,7 @@ async def update_dn_weather(date_time: datetime): lat = place.latitude lon = place.longitude - city = locate.find_override_locations(lat, lon) + city = await locate.find_override_locations(lat, lon) if city: L.INFO(f"Using override location: {city}") @@ -745,7 +791,7 @@ async def update_dn_weather(date_time: datetime): L.INFO(f"City in data: {city}") else: - loc = locate.reverse_geocode(lat, lon) + loc = await locate.reverse_geocode(lat, lon) L.DEBUG(f"loc: {loc}") city = loc.name city = city if city else loc.city @@ -801,7 +847,8 @@ async def update_dn_weather(date_time: datetime): detailed_forecast = ( f"---\n" f"date: {date_str}\n" - f"zip: {zip}\n" + f"latitude: {lat}" + f"longitude: {lon}" f"tags:\n" f" - weather\n" f"updated: {now}\n" diff --git a/sijapi/routers/sd.py b/sijapi/routers/sd.py index 8ae25b4..1069a99 100644 --- a/sijapi/routers/sd.py +++ b/sijapi/routers/sd.py @@ -59,17 +59,18 @@ async def sd_endpoint(request: Request): @sd.get("/v1/images/generations") async def sd_endpoint( request: Request, - prompt: str = Query(..., description="The prompt for image generation") + prompt: str = Query(..., description="The prompt for image generation"), + earlyout: str = Query("output", description="specify web for a redirect, or json for a json with the local path") ): + image_path = await workflow(prompt=prompt, scene="wallpaper", earlyout=earlyout) + web_path = get_web_path(image_path) - earlyout = "web" - image_path = await workflow(prompt=prompt, scene="wallpaper", earlyout="web") if earlyout == "web": - return RedirectResponse(url=image_path, status_code=303) + return RedirectResponse(url=web_path, status_code=303) else: return JSONResponse({"image_url": image_path}) -async def workflow(prompt: str, scene: str = None, size: str = None, style: str = "photorealistic", earlyout: str = "local", destination_path: str = None, downscale_to_fit: bool = False): +async def workflow(prompt: str, scene: str = None, size: str = None, style: str = "photorealistic", earlyout: str = None, destination_path: str = None, downscale_to_fit: bool = False): scene_data = get_scene(scene) if not scene_data: scene_data = get_matching_scene(prompt) @@ -105,14 +106,15 @@ async def workflow(prompt: str, scene: str = None, size: str = None, style: str max_size = max(width, height) if downscale_to_fit else None destination_path = Path(destination_path).with_suffix(".jpg") if destination_path else SD_IMAGE_DIR / f"{prompt_id}.jpg" - if not earlyout: - await generate_and_save_image(prompt_id, saved_file_key, max_size, destination_path) + if earlyout: + asyncio.create_task(generate_and_save_image(prompt_id, saved_file_key, max_size, destination_path)) + L.DEBUG(f"Returning {destination_path}") + return destination_path else: - asyncio.create_task(generate_and_save_image(prompt_id, saved_file_key, max_size, destination_path)) - - await asyncio.sleep(0.5) - return get_web_path(destination_path) if earlyout == "web" else destination_path + await generate_and_save_image(prompt_id, saved_file_key, max_size, destination_path) + L.DEBUG(f"Returning {destination_path}") + return destination_path async def generate_and_save_image(prompt_id, saved_file_key, max_size, destination_path): diff --git a/sijapi/routers/serve.py b/sijapi/routers/serve.py index b5240e7..5452c4a 100644 --- a/sijapi/routers/serve.py +++ b/sijapi/routers/serve.py @@ -33,7 +33,7 @@ from sijapi import ( MAC_UN, MAC_PW, MAC_ID, TS_TAILNET, DATA_DIR, SD_IMAGE_DIR, PUBLIC_KEY, OBSIDIAN_VAULT_DIR ) from sijapi.utilities import bool_convert, sanitize_filename, assemble_journal_path -from sijapi.routers.locate import localize_datetime +from sijapi.routers import note, locate serve = APIRouter(tags=["public"]) @@ -67,9 +67,9 @@ def is_valid_date(date_str: str) -> bool: return False @serve.get("/notes/{file_path:path}") -async def get_file(file_path: str): +async def get_file_endpoint(file_path: str): try: - date_time = await localize_datetime(file_path); + date_time = await locate.localize_datetime(file_path); absolute_path, local_path = assemble_journal_path(date_time, no_timestamp = True) except ValueError as e: L.DEBUG(f"Unable to parse {file_path} as a date, now trying to use it as a local path") diff --git a/sijapi/routers/weather.py b/sijapi/routers/weather.py index 7175684..6f2ab5d 100644 --- a/sijapi/routers/weather.py +++ b/sijapi/routers/weather.py @@ -99,7 +99,7 @@ async def store_weather_to_db(date_time: datetime, weather_data: dict): # Get location details from weather data if available longitude = weather_data.get('longitude') latitude = weather_data.get('latitude') - elevation = locate.get_elevation(latitude, longitude) # 152.4 # default until we add a geocoder that can look up actual elevation; weather_data.get('elevation') # assuming 'elevation' key, replace if different + elevation = await locate.get_elevation(latitude, longitude) # 152.4 # default until we add a geocoder that can look up actual elevation; weather_data.get('elevation') # assuming 'elevation' key, replace if different location_point = f"POINTZ({longitude} {latitude} {elevation})" if longitude and latitude and elevation else None # Correct for the datetime objects