88from typing import Optional
99
1010from owl .utils import run_society
11- from pydantic import BaseModel
11+ from pydantic import BaseModel , Field
1212from dotenv import load_dotenv
1313from camel .agents import ChatAgent
1414from camel .logger import get_logger , set_log_level
@@ -120,12 +120,13 @@ def analyze_chat_history(chat_history: list[dict]) -> Optional[str]:
120120 isinstance (assistant_content , str )
121121 and "FINAL SUMMARY REPORT" in assistant_content
122122 ):
123- final_summary = assistant_content
123+ final_summary = assistant_content
124124 if final_summary :
125125 logger .info ("\033 [94m===== RAW SUMMARY FROM AGENTS =====\033 [0m" )
126126 logger .info (final_summary )
127127 else :
128- logger .info ("\033 [91mNo final summary report found in chat history.\033 [0m" )
128+ logger .info (
129+ "\033 [91mNo final summary report found in chat history.\033 [0m" )
129130
130131 queried_urls : list [str ] = []
131132 for turn in chat_history :
@@ -147,31 +148,63 @@ def analyze_chat_history(chat_history: list[dict]) -> Optional[str]:
147148
148149 return final_summary
149150
151+
150152# ------------------ Pydantic Schemas ------------------
151153
152154class PersonalInformation (BaseModel ):
153- name : str
154- positions : list [str ]
155- contact_information : str | None = None
156- social_media : list [str ] | None = None
157- research_keywords : list [str ]
155+ name : str = Field (description = "Full name of the scholar" )
156+ positions : list [str ] = Field (
157+ description = "List of current academic or professional positions" )
158+ contact_information : Optional [str ] = Field (
159+ description = "Primary contact information such as email address or "
160+ "phone number"
161+ )
162+ social_media : Optional [list [str ]] = Field (
163+ description = "List of social media or personal profile URLs (e.g., "
164+ "LinkedIn, Twitter)"
165+ )
166+ research_keywords : list [str ] = Field (
167+ description = "Keywords summarizing key research areas or topics" )
168+ short_introduction : str = Field (
169+ description = "A brief one-paragraph summary of the scholar's profile" )
158170
159171
160172class Biography (BaseModel ):
161- career_history : str
162- research_focus : str
173+ biography : str = Field (
174+ description = "Narrative biography including career trajectory and "
175+ "research journey" )
163176
164177
165178class ResearchInterests (BaseModel ):
166- areas : list [str ]
179+ areas : list [str ] = Field (
180+ description = "List of active or long-term research topics" )
167181
168182
169183class AwardsAndDistinctions (BaseModel ):
170- honors : list [str ]
184+ honors : list [str ] = Field (
185+ description = "List of awards, honors, and professional distinctions "
186+ "received" )
171187
172188
173189class Education (BaseModel ):
174- degrees : list [str ]
190+ degrees : list [str ] = Field (
191+ description = "Academic degrees in reverse chronological order, "
192+ "including institution and year" )
193+
194+
195+ class Links (BaseModel ):
196+ scholarly_identity_links : list [str ] = Field (
197+ description = "Links to unique scholarly identity profiles (e.g., "
198+ "ORCID, IEEE Xplore, DBLP)"
199+ )
200+ related_sites : list [str ] = Field (
201+ description = "Links to institutional or departmental homepages (e.g., "
202+ "KAUST ECE department)"
203+ )
204+ related_links : list [str ] = Field (
205+ description = "Links to academic and professional presence (e.g., "
206+ "Google Scholar, ResearchGate, LinkedIn)"
207+ )
175208
176209
177210class ScholarProfile (BaseModel ):
@@ -180,21 +213,16 @@ class ScholarProfile(BaseModel):
180213 research_interests : ResearchInterests
181214 awards_and_distinctions : AwardsAndDistinctions
182215 education : Education
216+ links : Links
183217
184218
185- def generate_html_profile (input_text : str ,
186- output_file : str = "profile.html" ) -> None :
187- """Pipeline: extract structured profile from a free‑form biography and
188- turn it into a polished HTML page.
189-
190- Parameters
191- ----------
192- input_text : str
193- Raw biography or resume text.
194- output_file : str, optional
195- Path where the final HTML file will be saved, by default
196- "profile.html".
197- """
219+ def generate_html_profile (input_text ,
220+ template_path : str = "template.html" ,
221+ output_file : str = "profile.html" ,
222+ base_url : str = "https://cemse.kaust.edu.sa" ) -> \
223+ None :
224+ """Fill an HTML template with profile data, some with agent rewriting,
225+ and write to file."""
198226 extraction_prompt = (
199227 "You are a scholarly‑profile extraction assistant. "
200228 "Return ONLY JSON that matches the provided schema."
@@ -205,43 +233,112 @@ def generate_html_profile(input_text: str,
205233 input_message = input_text ,
206234 response_format = ScholarProfile ,
207235 )
208- profile : ScholarProfile = extraction_response .msgs [0 ].parsed
209-
210- html_system_prompt = "You are an expert HTML profile page generator."
211- html_agent = ChatAgent (system_message = html_system_prompt )
212-
213- html_user_prompt = f"""
214- Using the following JSON, build a complete HTML5 profile page.
215-
216- • Sections: Personal Information, Biography, Research Interests, Awards
217- and Distinctions, Education.
218- Generate a professional and natural-looking personal profile HTML page
219- based
220- on the provided information, following these detailed requirements:
221- Using standard HTML5 + simple inline CSS, beautiful but not complicated.
222- Layout should be clearly organized using <h1> for the main title, <h2> for
223- section headings, and <p> for paragraph text.
224- Use proper <a href> hyperlinks for any external links (e.g., LinkedIn,
225- ResearchGate, Google Scholar).
226- Write in a natural, flowing narrative style suitable for introducing the
227- person to a broad audience.
228-
229- Please write ALL the input content to the webpage.
230- JSON:
231- { profile .model_dump_json (indent = 2 )}
236+ profile = extraction_response .msgs [0 ].parsed
237+
238+ def to_html_list (items : list [str ]) -> str :
239+ return "<br>" .join (items )
240+
241+ def format_awards_with_agent (awards : list [str ]) -> str :
242+ formatted_awards = []
243+ agent = ChatAgent (
244+ system_message = """Reformat awards as HTML list items using the
245+ structure, Like:
246+ <li class="field__item"><strong>The
247+ NSF CAREER grant in low-power computing and
248+ communication systems</strong>, 2024</li>.""" )
249+ for award in awards :
250+ response = agent .step (f"Reformat this: { award } " )
251+ formatted_awards .append (response .msgs [0 ].content .strip ())
252+ return "\n " .join (formatted_awards )
253+
254+ def format_education_with_agent (degrees : list [str ]) -> str :
255+ agent = ChatAgent (
256+ system_message = """Reformat education degrees into <dl> format.
257+ Like:
258+ <dl class="field__item"><dt>Doctor of Philosophy (Ph.D.)</dt>
259+ <dd class="text-break">Integrated Circuits and Systems,
260+ <em>University of California</em>, United States, 2003</dd></dl>
261+ """ )
262+
263+ items = []
264+ for degree in degrees :
265+ response = agent .step (f"Reformat this: { degree } " )
266+ items .append (response .msgs [0 ].content .strip ())
267+ return "\n " .join (items )
268+
269+ def format_links (links : list [str ], html_template : str ) -> str :
270+ agent = ChatAgent (
271+ system_message = "Reformat link into this html structure: %s ." %
272+ html_template )
273+ items = []
274+ for link in links :
275+ response = agent .step (f"Reformat this: { link } " )
276+ items .append (response .msgs [0 ].content .strip ())
277+ return "\n " .join (items )
278+
279+ engage_link_template = """
280+ <li class="field__item"><a class="text-decoration-none"
281+ href="https://orcid.org/0000-0003-1849-083X" title="Follow Ahmed Eltawil
282+ on ORCID at 0000-0003-1849-083X"><button class="btn btn-orcid px-1 py-0
283+ text-bg-light bg-opacity-10 text-break text-wrap" type="button"
284+ aria-label="ORCID"><i class="bi bi-orcid mx-1"></i><span class="mx-1
285+ text-start">ORCID</span></button></a></li>-->
232286 """
287+ related_link_template = """
288+ <li class="field__item"><a
289+ href="https://scholar.google.com/citations?user=XzW-KWoAAAAJ&hl=en"
290+ class="text-break text-underline-hover">Publications on Google
291+ Scholar</a></li>
292+ """
293+ related_site_template = """
294+ <li class="list-group-item px-0 field__item"><a class="text-break
295+ text-underline-hover" href="https://ece.kaust.edu.sa/" title="Electrical
296+ and Computer Engineering (ECE) Academic Program">Electrical and Computer
297+ Engineering (ECE)</a></li>
298+ """
299+ with open (template_path , "r" , encoding = "utf-8" ) as f :
300+ template = f .read ()
301+
302+ name = profile .personal_information .name
303+ slug = name .replace (" " , '-' ).lower ()
304+ share_url = f"{ base_url } /profiles/{ slug } "
305+
306+ filled = (
307+ template
308+ .replace ("{{ name }}" , name )
309+ .replace ("{{ personal information }}" ,
310+ to_html_list (profile .personal_information .positions ))
311+ .replace ("{{ email }}" ,
312+ profile .personal_information .contact_information )
313+ .replace ("{{ introduction }}" ,
314+ profile .personal_information .short_introduction )
315+ .replace ("{{ biography }}" , profile .biography .biography )
316+ .replace ("{{ research interests }}" ,
317+ to_html_list (profile .research_interests .areas ))
318+ .replace ("{{ awards and distinctions }}" , format_awards_with_agent (
319+ profile .awards_and_distinctions .honors ))
320+ .replace ("{{ education }}" ,
321+ format_education_with_agent (profile .education .degrees ))
322+ .replace ("{{ engage }}" ,
323+ format_links (profile .links .scholarly_identity_links ,
324+ html_template = engage_link_template ))
325+ .replace ("{{ related sites }}" ,
326+ format_links (profile .links .related_sites ,
327+ html_template = related_site_template ))
328+ .replace ("{{ related links }}" ,
329+ format_links (profile .links .related_links ,
330+ html_template = related_link_template ))
331+ .replace ("{{ slug }}" , slug )
332+ .replace ("{{ base_url }}" , base_url )
333+ .replace ("{{ share_url }}" , share_url )
334+ .replace ("{{ summary }}" ,
335+ profile .biography .career_history .replace ('\n ' , '%0A' ))
336+ )
233337
234- html_response = html_agent .step (html_user_prompt )
235- html_code = html_response .msgs [0 ].content
236- html_code_block = re .search (r"```html(.*?)```" , html_code , re .DOTALL )
338+ with open (output_file , "w" , encoding = "utf-8" ) as f :
339+ f .write (filled )
237340
238- if html_code_block :
239- extracted_html = html_code_block .group (1 ).strip ()
240- with open (output_file , "w" , encoding = "utf-8" ) as fp :
241- fp .write (extracted_html )
242- logger .info (f"HTML profile page saved to: { output_file } " )
243- else :
244- logger .warning ("No HTML code block found in the Agent response." )
341+ logger .info (f"HTML profile generated at: { output_file } " )
245342
246343
247344def run_profile_generation (task : str | None = None ,
@@ -250,18 +347,24 @@ def run_profile_generation(task: str | None = None,
250347 the browsing history, and render the final HTML profile.
251348 """
252349 default_task = (
253- "find Bernard‑Ghanem's information based on these websites:\n "
254- "https://www.linkedin.com/in/bernardghanem/\n "
255- "https://scholar.google.com/citations?hl=en&user=rVsGTeEAAAAJ"
256- "&view_op=list_works\n "
257- "https://x.com/bernardsghanem\n "
258- "https://www.researchgate.net/profile/Bernard-Ghanem\n "
350+ """find Bernard‑Ghanem's information based on these websites:
351+ https://www.linkedin.com/in/bernardghanem/
352+ https://scholar.google.com/citations?hl=en&user=rVsGTeEAAAAJ&view_op
353+ =list_works
354+ https://x.com/bernardsghanem
355+ https://www.bernardghanem.com/
356+ https://www.researchgate.net/profile/Bernard-Ghanem
357+ https://www.bernardghanem.com/curriculum-vitae
358+ https://www.bernardghanem.com/home
359+ """
259360
260361 )
261362 section_plan = """
262363 Information summary focus on sections: "
263364 "Personal Information, Biography, Research Interests, Awards and "
264- "Distinctions, Education.
365+ "Distinctions, Education, related links, related sites, Scholarly
366+ Identity Links
367+
265368 Each section need to be as detailed as possible.
266369
267370 Final summary report please note "FINAL SUMMARY REPORT" at the beginning.
0 commit comments